- lib3mf 集成系列(一):编译与示例测试
- lib3mf 集成系列(二):Flutter FFI 插件创建与 Android 端集成
- lib3mf 集成系列(三):在 Flutter 中解析 3MF 文件信息
- 未完待续
前言
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文件信息的功能。