Flutter 通过ffi调用Rust编译生成的产物.so文件(Android), .a/.dylib文件(iOS/macOS)和.dll文件(windows)接口方法;
拾用本文您将获取以下技能:
Rust编译.so文件的能力;Rust编译.a文件的能力;Rust编译.dylib文件的能力;Rust编译.dll文件的能力;Flutter调用.so文件的能力;Flutter调用.a文件的能力;Flutter调用.dylib文件的能力;Flutter调用.dll文件的能力;
附加Buff:
Flutter环境安装指南;Rust环境安装指南;Android不同架构(v7a/v8a)的.so文件加载方式;iOS/macOS不同设备(真机/模拟器)的.a/.dylib文件加载方式;windoes的.dll文件加载方式;
- 开发电脑:
Apple M3 Max - Flutter IDE:
Android Studio Koala | 2024.1.1 Patch 2 - iOS IDE:
Xcode Version 15.4 - Rust IDE:
RustRover 2024.1.7 - Flutter:
Flutter 3.24.0 / Dart 3.5.0 - Rust:
rustc 1.80.1 / rustup 1.27.1
- 开发电脑: 自行解决
- Flutter IDE: https://developer.android.com/studio
- iOS IDE: https://developer.apple.com/cn/xcode
- Rust IDE: https://www.jetbrains.com/zh-cn/rust/
- Flutter: https://docs.flutter.dev/get-started/install
- Rust: https://www.rust-lang.org/learn/get-started
使用Rust IDE创建一个Library的工程.
在Cargo.toml文件中配置库的类型.
[lib]的描述说明可以参考: https://doc.rust-lang.org/cargo/reference/cargo-targets.html#library
cdylib用于输出动态库, 比如.so.dylib.dllstaticlib用于输出静态库, 比如.a.lib
如果您还想了解更多类型可以参考: https://doc.rust-lang.org/reference/linkage.html
之后在src/lib.rs文件里面写上非常牛逼自信的高级算法.
工程准备就绪之后, 就可以着手编译了.
rustup target add aarch64-linux-android armv7-linux-androideabi
aarch64-linux-android用于输出arm64-v8a的.so文件armv7-linux-androideabi用于输出armeabi-v7a的.so文件
您可以通过rustup target list查看所有支持的工具链.
cargo install cargo-ndk
cargo-ndk用来编译so文件
cargo ndk -t armeabi-v7a -t arm64-v8a build --release
关于cargo ndk更多用法可以参考: https://github.com/bbqsrc/cargo-ndk
arm64-v8a平台的so文件输出在target/aarch64-linux-android/release/xxx.soarmeabi-v7a平台的so文件输出在target/armv7-linux-androideabi/release/xxx.so
之后将产物分别复制到Flutter工程中的android/src/main/jniLibs/arm64-v8a和android/src/main/jniLibs/armeabi-v7a这样在Android平台上,就会根据CPU的架构自动加载对应的so文件, 这一点在iOS平台上需要手动处理, 在介绍iOS时, 会提及.
然后在Flutter端使用DynamicLibrary.open('librust_api_test.so')加载动态库即可.
到这为止, Android平台的产物so文件就已经输出了. 接下来编译iOS.
与上述一致.
rustup target add aarch64-apple-ios aarch64-apple-ios-sim
aaarch64-apple-ios用于输出iPhone真机的.a文件aarch64-apple-ios-sim用于输出iPhone模拟器的.a文件
您可以通过rustup target list查看所有支持的工具链.
cargo install cargo-lipo
cargo-lipo用来编译.a文件
cargo lipo --targets aarch64-apple-ios --releasecargo lipo --targets aarch64-apple-ios-sim --release
这里要分开2个命令编译不同的文件.
关于cargo lipo更多用法可以参考: https://github.com/TimNN/cargo-lipo
iPhone真机的a文件输出在target/aarch64-apple-ios/release/xxx.aiPhone模拟器的a文件输出在target/aarch64-apple-ios-sim/release/xxx.a
之后将产物分别复制到Flutter工程中的ios/iphoneos(iPhone真机)和ios/iphonesimulator(iPhone模拟器).
之后在iOS工程中的xxx.podspec文件中加入:
s.vendored_libraries = '$(PLATFORM_NAME)/librust_api_test.a' #引入文件
...
s.pod_target_xcconfig = {
'DEFINES_MODULE' => 'YES',
'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' ,
'OTHER_LDFLAGS' => '-lc++ -force_load $(PODS_TARGET_SRCROOT)/$(PLATFORM_NAME)/librust_api_test.a' #强制加载符号表, 否则会被`死亡代码`优化掉.
}
...
${PLATFORM_NAME}是用来自动加载iPhone真机或iPhone模拟器的关键.
如果要区分i386或arm64架构, 可以使用${ARCHS_STANDARD_INCLUDING_64_BIT}环境变量.
之后在Flutter工程中的example/ios文件夹中使用pod install命令.
到这为止, iOS平台的产物.a文件就已经输出了.
在Flutter端使用DynamicLibrary.executable()加载静态库即可.
与上述一致.
ios平台使用cargo lipo --targets aarch64-apple-ios --release编译.macOS平台直接使用cargo build --release编译.
- 在
flutter_rust_ffi.podspec文件中加入s.vendored_libraries = 'librust_api_test2.dylib' - 在
Flutter端使用DynamicLibrary.open('librust_api_test2.dylib')加载动态库即可.
- 需要编译好的
.dylib文件 - 需要创建一个
Info.plist文件 - 需要使用
install_name_tool(关键步骤) - 封装成
framework(是一个普通文件夹) - 封装成
xcframework(是一个特殊文件夹)
#!/bin/bash
# 配置变量
LIB_NAME="rust_api_test2"
IPHONE_OS_LIB="target/aarch64-apple-ios/release/librust_api_test2.dylib"
# 如果有模拟器版本也可以加入
OUTPUT="output/ios"
rm -rf "${OUTPUT}/${LIB_NAME}.framework"
rm -rf "${OUTPUT}/${LIB_NAME}.xcframework"
# 1. 准备 Framework 目录
mkdir -p "${OUTPUT}/${LIB_NAME}.framework"
# 2. 修复 Install Name (关键:让 dyld 知道去 @rpath 找)
install_name_tool -id "@rpath/${LIB_NAME}.framework/${LIB_NAME}" "${IPHONE_OS_LIB}"
# 3. 移动文件并改名
cp "${IPHONE_OS_LIB}" "${OUTPUT}/${LIB_NAME}.framework/${LIB_NAME}"
# 4. 生成 Info.plist
cat <<EOF > "${OUTPUT}/${LIB_NAME}.framework/Info.plist"
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleExecutable</key>
<string>${LIB_NAME}</string>
<key>CFBundleIdentifier</key>
<string>com.angcyo.${LIB_NAME}</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>
EOF
# 5. 封包成 XCFramework
xcodebuild -create-xcframework \
-framework "${OUTPUT}/${LIB_NAME}.framework" \
-output "${OUTPUT}/${LIB_NAME}.xcframework"- 将生成的放到任意目录, 然后在
flutter_rust_ffi.podspec文件中加入s.vendored_frameworks = 'Frameworks/rust_api_test2.xcframework' - 在
Flutter端使用DynamicLibrary.open('rust_api_test2.framework/rust_api_test2');加载动态库即可(这里不需要用xcframework).
macOS上的framework与iOS上的framework在结构上会有不同.
- 需要编译好的
.dylib文件 - 需要创建一个
Info.plist文件 - 需要创建一个
Versions/A/Resources目录 - 需要创建一个
Versions/A/Headers目录 - 需要创建软链接 (关键步骤)
- 封装成
framework(是一个普通文件夹) - 封装成
xcframework(是一个特殊文件夹)
#!/bin/bash
# 配置变量
FW_NAME="rust_api_test2"
MAC_OS_LIB="target/release/librust_api_test2.dylib"
# 如果有模拟器版本也可以加入
OUTPUT="output/macos"
FW_DIR="${OUTPUT}/${FW_NAME}.framework"
rm -rf "${FW_DIR}"
rm -rf "${OUTPUT}/${FW_NAME}.xcframework"
# 1. 准备 Framework 目录
mkdir -p "${FW_DIR}"
mkdir -p "${FW_DIR}/Versions/A/Resources"
mkdir -p "${FW_DIR}/Versions/A/Headers"
# 3. 移动文件并改名
cp "${MAC_OS_LIB}" "${FW_DIR}/Versions/A/${FW_NAME}"
# 4. 生成 Info.plist
cat <<EOF > "${FW_DIR}/Versions/A/Resources/Info.plist"
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleIdentifier</key>
<string>com.angcyo.${FW_NAME}</string>
<key>CFBundleExecutable</key>
<string>${FW_NAME}</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
</dict>
</plist>
EOF
# 4. 创建软链接 (关键步骤)
CURRENT_DIR=$(pwd)
# shellcheck disable=SC2164
cd "${FW_DIR}"
ln -sfh A Versions/Current
ln -sfh Versions/Current/${FW_NAME} ${FW_NAME}
ln -sfh Versions/Current/Resources Resources
ln -sfh Versions/Current/Headers Headers
# shellcheck disable=SC2164
cd "$CURRENT_DIR"
# 5. 封包成 XCFramework
xcodebuild -create-xcframework \
-framework "${OUTPUT}/${FW_NAME}.framework" \
-output "${OUTPUT}/${FW_NAME}.xcframework"加载framework的方式与iOS端一致.
与上述一致.
在 windoes 平台上直接使用 cargo build --release 编译即可生成.dll文件.
- 在
windows/CMakeLists.txt文件中加入dll所在的目录"${CMAKE_CURRENT_SOURCE_DIR}/libs"
这里注意必须要加载PARENT_SCOPE前面.
set(flutter_rust_ffi_bundled_libraries
# Defined in ../src/CMakeLists.txt.
# This can be changed to accommodate different builds.
$<TARGET_FILE:flutter_rust_ffi>
"${CMAKE_CURRENT_SOURCE_DIR}/libs" #...
PARENT_SCOPE
)
- 在
Flutter端使用DynamicLibrary.open('libs/rust_api_test.dll')加载动态库即可.
上述生成的产物还不支持ffi调用, 所以这里阐述一下.
传统的导出ffi的方式extern fn比较繁琐, 并且不易于生成.h头文件.
参考: https://doc.rust-lang.org/nomicon/ffi.html
这里使用safer_ffi库(https://getditto.github.io/safer_ffi/)导出`ffi`
首先在Cargo.toml文件中加入safer-ffi依赖:
[dependencies]
safer-ffi = "0.1.12"并且指定特性:
[features]
headers = ["safer-ffi/headers"]其次在需要导出的方法中使用#[ffi_export]宏:
#[ffi_export]
pub fn test_bool(value: bool) -> bool {
!value
}最后配置一下生成头文件名的方法generate_headers:
#[test]
#[cfg(feature = "headers")]
fn generate_headers() -> std::io::Result<()> {
safer_ffi::headers::builder()
.to_file("rust_api_test.h")?
.generate()
}之后就可以使用命令运行这个方法generate_headers生成对应的头文件了:
cargo test --lib generate_headers --features headers
使用
Flutter创建一个FFI Plugin工程, 既可以获得相应的模板代码.
有了.h头文件, Flutter就可以借助ffigen工具创建对应的dart绑定类
dart run ffigen --config ffigen.yaml
ffigen.yaml配置文件内容, 请参考: https://pub.dev/packages/ffigen 文档. 很简单, 只需要指定对应的.h文件位置即可.
这里需要注意的是, Flutter在加载.so文件或.a文件时, 有所差异.
- 加载
.so文件直接使用默认的DynamicLibrary.open('lib$_libName.so');方法即可. - 但是加载
.a文件则需要使用DynamicLibrary.executable();方法.
final DynamicLibrary _dylib = () {
if (Platform.isMacOS || Platform.isIOS) {
//return DynamicLibrary.open('$_libName.framework/$_libName');
return DynamicLibrary.executable();
}
if (Platform.isAndroid || Platform.isLinux) {
return DynamicLibrary.open('lib$_libName.so');
}
if (Platform.isWindows) {
return DynamicLibrary.open('$_libName.dll');
}
throw UnsupportedError('Unknown platform: ${Platform.operatingSystem}');
}();到这里为止, Flutter就已经可以在Android平台上愉快的调用ffi了, 而不需要额外的配置.
但是, 在iOS平台上, 会有问题, 如下:
IOS Failed to lookup symbol (dlsym(RTLD_DEFAULT, test_func): symbol not found)
为了解决这个问题, 您需要在Xcode中进行如下配置:
- 导航到
TARGETS->Runner->Build Settings->Linking - General将Dead Code Stripping配置改成No(这是iOS移除未使用的代码用的)
- 在同级的
Ohter Linker Flags中加入-all_load(这是加载所有符号表用的)
这里说明一点:
-all_load和上文中的-force_load xxx.a作用是一致的,自行取其一配置即可.
配置完成之后, rebuildFlutter在iOS平台上也可以愉快的调用ffi了.
文中源码有所省略, 文末有开源代码仓库地址, 欢迎食用.
至此文章就结束了!
群内有各(pian)种(ni)各(jin)样(qun)的大佬,等你来撩.