跳至内容
返回

lib3mf 集成系列(二):Flutter FFI 插件创建与 Android 端集成

发布于:  at  06:00 下午

前言

lib3mf 集成系列(一):编译与示例测试中介绍了lib3mf的编译与示例测试, 本篇将介绍如何创建Flutter FFI插件, 以及Android端的集成.

创建Flutter FFI插件

flutter 内置了一个创建ffi插件的命令, 形式如下

flutter create --template=plugin_ffi --platforms=macos native_lib3mf
flutter create --template=plugin_ffi --platforms=android native_lib3mf

由于我的目标平台是macOS和Android, 所以指定平台为macOS和android. 读者可以根据自己的目标平台指定平台.

模版为我们生成了sum函数, 定义如下

FFI_PLUGIN_EXPORT int sum(int a, int b) { return a + b; }

也为我们在dart层绑定了调用

int sum(int a, int b,) {
  return _sum(a, b, );
}
late final _sumPtr =
    _lookup<ffi.NativeFunction<ffi.Int Function(ffi.Int, ffi.Int)>>('sum');
late final _sum = _sumPtr.asFunction<int Function(int, int)>();

我们创建一个简单的路由来测试调用

import 'package:flutter/material.dart';
import 'package:native_lib3mf/native_lib3mf.dart' as nn;

class NativeLib3mfRoute extends StatelessWidget {
  const NativeLib3mfRoute({super.key});

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.white,
      constraints: BoxConstraints.expand(),
      child: Center(
        child: Material(color: Colors.transparent,child: Text('Native Lib3mf: 2+3=${nn.sum(2, 3)}')),
      ),
    );
  }
}

至此, 一个最小的Flutter FFI插件已经创建完成, 接下来介绍Android端的集成.

Android 端集成

最初我天真地以为,只要把 native 代码扔进 Flutter 项目,Flutter 就能自动为各平台编译出对应的库文件(比如 Android 生成 .so,macOS 生成 .dylib)。结果发现事情远没那么简单——对于插件自身的简单 C 代码,Flutter 确实可以自动编译;但一旦引入像 lib3mf 这样的第三方 C++ 库,就需要手动处理交叉编译、ABI 兼容性、依赖管理等复杂问题。这也就是为什么之前执行命令时需要指定 —platforms=android 和 —platforms=macos——插件模板为我们搭建了跨平台编译的骨架,但集成第三方库的细节仍需我们自己填充。

如果你希望进一步自动化,可以尝试将 lib3mf 作为 CMake 子项目(add_subdirectory)直接集成到插件中,这样 lib3mf 就会和插件代码一起由 Flutter 的构建系统自动编译。但需要注意,lib3mf 依赖 libzip 等库,需要确保这些依赖在 Android NDK 和 macOS 下也能正确编译。本教程为了聚焦核心集成流程,采用了预编译库的方式,先解决“如何让 lib3mf 跑起来”的问题,再考虑“如何更自动化”的优化。感兴趣的读者可以自行探索。

编译安卓平台lib3mf.so

make默认情况下使用的是系统环境中的编译工具链, 要想编译的到安卓平台的so文件, 需要使用android ndk提供的编译工具链. 关于android ndk的安装, 可以参考Install NDK, 本文不再赘述. 默认环境中已经配置好ANDROID_NDK_HOME环境变量, 所以可以直接使用

mkdir build && cd build
cmake .. \
  -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake \
  -DANDROID_ABI=arm64-v8a \
  -DANDROID_PLATFORM=android-21 \
  -DCMAKE_BUILD_TYPE=Release
参数作用
CMAKE_TOOLCHAIN_FILE指向 NDK 自带的 CMake 工具链文件
ANDROID_ABI指定架构,目前主流为 arm64-v8a
ANDROID_PLATFORM最小 API 等级,建议设为 android-21 (5.0+)

编译报错了, 提示找不到#include <sys/attr.h>, 这是因为该头文件是 Apple 特有的 修改Libraries/libzip/Source/unix/zip_source_file_stdio_named.c

#if defined(__APPLE__) && !defined(__ANDROID__)
#include <sys/attr.h>
#include <sys/clonefile.h>
#endif

再次编译成功, 得到lib3mf.so —arm64-v8a版本

修改 -DANDROID_ABI 参数为armeabi-v7a得到lib3mf.so — armeabi-v7a版本

分别复制到对应的文件夹 /native_lib3mf/android/jniLibs/armeabi-v7a /native_lib3mf/android/jniLibs/arm64-v8a

其他abi同理

复制头文件

上一步生成so的时候, 已经生成好头文件. 位于/lib3mf/Autogenerated/Bindings/Cpp

lib3mf_abi.hpp
lib3mf_implicit.hpp
lib3mf_types.hpp

将这些文件复制到/native_lib3mf/src

编写调用代码

参考/lib3mf/SDK/Examples/Cpp/Source/ExtractInfo.cpp, 编写extract_info.cpp 暂时不处理解析的部分, 只打印版本,保证移植成功能够运行起来.

注意:Android 上 std::cout 不会输出到 logcat,因此后续会替换为 __android_log_print。这里先保留原样,便于读者对比。

#include <algorithm>
#include <iostream>
#include <lib3mf_implicit.hpp>
#include <string>

#include "native_lib3mf.h"
using namespace Lib3MF;

void printVersion(PWrapper wrapper) {
  Lib3MF_uint32 nMajor, nMinor, nMicro;
  wrapper->GetLibraryVersion(nMajor, nMinor, nMicro);
  std::cout << "lib3mf version = " << nMajor << "." << nMinor << "." << nMicro;
  std::string sReleaseInfo, sBuildInfo;
  if (wrapper->GetPrereleaseInformation(sReleaseInfo)) {
    std::cout << "-" << sReleaseInfo;
  }
  if (wrapper->GetBuildInformation(sBuildInfo)) {
    std::cout << "+" << sBuildInfo;
  }
  std::cout << std::endl;
}

extern "C" {
FFI_PLUGIN_EXPORT int32_t extract_info(const char* path) {
  PWrapper wrapper = CWrapper::loadLibrary();
  std::cout
      << "------------------------------------------------------------------"
      << std::endl;
  std::cout << "3MF Read example" << std::endl;
  printVersion(wrapper);
  std::cout
      << "------------------------------------------------------------------"
      << std::endl;
  return 0;
}
}

编写CMakeList.txt脚本

cmake_minimum_required(VERSION 3.10)

# 迁移到了CXX, 默认是C的
project(native_lib3mf_library VERSION 0.0.1 LANGUAGES C CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

message(STATUS "DEBUG: CMAKE_CURRENT_SOURCE_DIR is: ${CMAKE_CURRENT_SOURCE_DIR}")

# 确定当前路径位置
get_filename_component(PLUGIN_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/.." ABSOLUTE)
# 手动指定so文件位置
set(LIB3MF_SO_PATH "${PLUGIN_ROOT}/android/jniLibs/${ANDROID_ABI}/lib3mf.so")

message(STATUS "NativeLib3mf: Target library path: ${LIB3MF_SO_PATH}")

add_library(native_lib3mf SHARED
  "native_lib3mf.c" "extract_info.cpp"
)

if(EXISTS "${LIB3MF_SO_PATH}")
    message(STATUS "NativeLib3mf: File physical check SUCCESS.")
    target_link_libraries(native_lib3mf PRIVATE ${LIB3MF_SO_PATH} dl
    log)
else()
    message(FATAL_ERROR "NativeLib3mf: File NOT found at ${LIB3MF_SO_PATH}")
endif()



target_include_directories(native_lib3mf PRIVATE .)

set_target_properties(native_lib3mf PROPERTIES
  PUBLIC_HEADER native_lib3mf.h
  OUTPUT_NAME "native_lib3mf"
)


target_compile_definitions(native_lib3mf PUBLIC DART_SHARED_LIB)

if (ANDROID)
  # Support Android 15 16k page size
  target_link_options(native_lib3mf PRIVATE "-Wl,-z,max-page-size=16384")
  target_link_libraries(native_lib3mf PRIVATE dl log)
endif()

结果因为缺少x86的so,所以编译报错中断了

尝试修改项目根目录下的android/build.gradle.kts的配置

    defaultConfig {
        // 	无关配置
        ndk {
            abiFilters += listOf("arm64-v8a", "armeabi-v7a")
        }
    }

结果他还是尝试编译x86的.

202602270025突然想起来是需要为native_lib3mf 安卓子module配置, 配置以后就不用区分平台了

android {
    defaultConfig {
        ndk {
            // 设置支持的 SO 库架构
            abiFilters 'armeabi-v7a', 'arm64-v8a'
        }
    }
}

为了解决这个问题,需要主动处理

修改CMakeLists.txt, 找不到就跳过

# The Flutter tooling requires that developers have CMake 3.10 or later
# installed. You should not increase this version, as doing so will cause
# the plugin to fail to compile for some customers of the plugin.
cmake_minimum_required(VERSION 3.10)

project(native_lib3mf_library VERSION 0.0.1 LANGUAGES C CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)


add_library(native_lib3mf SHARED
  "native_lib3mf.c" "extract_info.cpp"
)

message(STATUS "DEBUG: CMAKE_CURRENT_SOURCE_DIR is: ${CMAKE_CURRENT_SOURCE_DIR}")

get_filename_component(PLUGIN_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/.." ABSOLUTE)
set(LIB3MF_SO_PATH "${PLUGIN_ROOT}/android/jniLibs/${ANDROID_ABI}/lib3mf.so")

message(STATUS "NativeLib3mf: Target library path: ${LIB3MF_SO_PATH}")


if(EXISTS "${LIB3MF_SO_PATH}")
    message(STATUS "NativeLib3mf: Found library for ${ANDROID_ABI}")
    target_link_libraries(native_lib3mf PRIVATE "${LIB3MF_SO_PATH}" dl log)
else()
    # 对于没有 lib3mf.so 的架构(如 x86),只链接系统库
    # 配合代码里的 #if 宏,链接器就不会报 undefined symbol 了
    message(WARNING "NativeLib3mf: No lib3mf.so for ${ANDROID_ABI}, building stub version.")
    target_link_libraries(native_lib3mf PRIVATE dl log)
endif()

target_include_directories(native_lib3mf PRIVATE .)

set_target_properties(native_lib3mf PROPERTIES
  PUBLIC_HEADER native_lib3mf.h
  OUTPUT_NAME "native_lib3mf"
)


target_compile_definitions(native_lib3mf PUBLIC DART_SHARED_LIB)

if (ANDROID)
  # Support Android 15 16k page size
  target_link_options(native_lib3mf PRIVATE "-Wl,-z,max-page-size=16384")
  target_link_libraries(native_lib3mf PRIVATE dl log)
endif()

同时代码里面添加宏用来处理x86等平台情况, 并且将std::cout替换成android的打印, 这样就能看到输出了

#include <algorithm>
#include <iostream>
#if defined(__arm__) || defined(__aarch64__)
#include <lib3mf_implicit.hpp>
#endif
#include <string>

#ifdef __ANDROID__
#include <android/log.h>
#define LOG_TAG "NativeLib3mf"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#else
#include <cstdio>
#define LOGI(...)      \
  printf(__VA_ARGS__); \
  printf("\n")
#endif

#include "native_lib3mf.h"
#if defined(__arm__) || defined(__aarch64__)
using namespace Lib3MF;

void printVersion(PWrapper wrapper) {
  Lib3MF_uint32 nMajor, nMinor, nMicro;
  wrapper->GetLibraryVersion(nMajor, nMinor, nMicro);

  // 获取预发布和构建信息
  std::string sReleaseInfo, sBuildInfo;
  bool hasRelease = wrapper->GetPrereleaseInformation(sReleaseInfo);
  bool hasBuild = wrapper->GetBuildInformation(sBuildInfo);

  // 格式化输出
  if (hasRelease && hasBuild) {
    LOGI("lib3mf version = %u.%u.%u-%s+%s", nMajor, nMinor, nMicro,
         sReleaseInfo.c_str(), sBuildInfo.c_str());
  } else if (hasRelease) {
    LOGI("lib3mf version = %u.%u.%u-%s", nMajor, nMinor, nMicro,
         sReleaseInfo.c_str());
  } else if (hasBuild) {
    LOGI("lib3mf version = %u.%u.%u+%s", nMajor, nMinor, nMicro,
         sBuildInfo.c_str());
  } else {
    LOGI("lib3mf version = %u.%u.%u", nMajor, nMinor, nMicro);
  }
}
#endif

extern "C" {
FFI_PLUGIN_EXPORT int32_t extract_info(const char* path) {
#if defined(__arm__) || defined(__aarch64__)
  PWrapper wrapper = CWrapper::loadLibrary();
  std::cout
      << "------------------------------------------------------------------"
      << std::endl;
  std::cout << "3MF Read example" << std::endl;
  printVersion(wrapper);
  std::cout
      << "------------------------------------------------------------------"
      << std::endl;
  return 0;
#else
  return -999;
#endif
}
}

导出ffi函数

参考模版, 为我们的extractInfo编写对应的查找和导出

  int extractInfo(String path) {
    final Pointer<Utf8> charPointer = path.toNativeUtf8();
    int ret = _extractInfo(charPointer);
    malloc.free(charPointer);
    return ret;
  }

  late final _extractInfoPtr =
      _lookup<
        ffi.NativeFunction<
          ffi.Int32 Function(ffi.Pointer<Utf8>) // CPP 签名
        >
      >('extract_info');

  late final _extractInfo = _extractInfoPtr
      .asFunction<
        int Function(ffi.Pointer<Utf8>) // Dart 签名
      >();

编写测试页面

import 'package:flutter/material.dart';
import 'package:native_lib3mf/native_lib3mf.dart' as nn;

class Extract3mfInfoRoute extends StatelessWidget {
  const Extract3mfInfoRoute({super.key});

  @override
  Widget build(BuildContext context) {
    return Container(
      color: Colors.white,
      constraints: BoxConstraints.expand(),
      child: Center(
        child: Material(
          color: Colors.transparent,
          child: FilledButton(
            onPressed: () {
              debugPrint(nn.extractInfo("a").toString());
            },
            child: Text('lib3mf extractInfo'),
          ),
        ),
      ),
    );
  }
}

实际运行输出

[+5453 ms] D/ProfileInstaller(24149): Installing profile for com.chaosgoo.metasequoia.metasequoia
[+5565 ms] I/NativeLib3mf(24149): lib3mf version = 2.4.1
[   +4 ms] I/flutter (24149): 0

从日志输出来看,lib3mf 已成功加载并返回版本号, 下一步是对接真实的lib3mf函数.

总结

本文中成功创建Flutter FFI 插件,还为 Android 编译 lib3mf.so,使用 CMake 配置并处理多 ABI 兼容,最终在 Flutter 中调用了 lib3mf 的版本查询函数。 下一篇将完善Extract3mfInfo函数,实现读取3mf文件信息的功能。


在以下平台分享此文章: