Flutter->`Flutter` 通过`ffi`调用`Rust`编译生成的产物.so文件(Android)和.a文件(iOS)接口方法

flutter_rust_ffi

Flutter 通过ffi调用Rust编译生成的产物.so文件(Android)和.a文件(iOS)接口方法;

拾用本文您将获取以下技能:

  • Rust编译.so文件的能力;
  • Rust编译.a文件的能力;
  • Flutter调用.so文件的能力;
  • Flutter调用.a文件的能力;

附加Buff:

  • Flutter环境安装指南;
  • Rust环境安装指南;
  • Android不同架构(v7a/v8a)的.so文件加载方式;
  • iOS不同设备(真机/模拟器)的.a文件加载方式;

本文环境

  • 开发电脑: 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

环境准备

使用Rust编译.so

工程准备

使用Rust IDE创建一个Library的工程.

Cargo.toml文件中配置库的类型.

[lib]的描述说明可以参考: https://doc.rust-lang.org/cargo/reference/cargo-targets.html#library

  • cdylib用于输出.so
  • staticlib用于输出.a

如果您还想了解更多类型可以参考: 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.so
  • armeabi-v7a平台的so文件输出在target/armv7-linux-androideabi/release/xxx.so

之后将产物分别复制到Flutter工程中的android/src/main/jniLibs/arm64-v8aandroid/src/main/jniLibs/armeabi-v7a这样在Android平台上,就会根据CPU的架构自动加载对应的so文件, 这一点在iOS平台上需要手动处理, 在介绍iOS时, 会提及.

到这为止, Android平台的产物so文件就已经输出了. 接下来编译iOS.

使用Rust编译.a

工程准备

与上述一致.

安装编译链

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 --release
cargo lipo --targets aarch64-apple-ios-sim --release

这里要分开2个命令编译不同的文件.

关于cargo lipo更多用法可以参考: https://github.com/TimNN/cargo-lipo

  • iPhone真机的a文件输出在target/aarch64-apple-ios/release/xxx.a
  • iPhone模拟器的a文件输出在target/aarch64-apple-ios-sim/release/xxx.a

之后将产物分别复制到Flutter工程中的ios/iphoneos(iPhone真机)和ios/iphonesimulator(iPhone模拟器).

之后在iOS工程中的xxx.podspec文件中加入:

...
s.user_target_xcconfig = {
'OTHER_LDFLAGS' => '-force_load ${PODS_ROOT}/../.symlinks/plugins/flutter_rust_ffi/ios/${PLATFORM_NAME}/librust_api_test.a'
}
...
  • ${PLATFORM_NAME}是用来自动加载iPhone真机iPhone模拟器的关键.

如果要区分i386arm64架构, 可以使用${ARCHS_STANDARD_INCLUDING_64_BIT}环境变量.

之后在Flutter工程中的example/ios文件夹中使用pod install命令.

到这为止, iOS平台的产物a文件就已经输出了.

Rust导出ffi

上述生成的产物还不支持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依赖:

toml 复制代码
[dependencies]
safer-ffi = "0.1.12"

并且指定特性:

toml 复制代码
[features]
headers = ["safer-ffi/headers"]

其次在需要导出的方法中使用#[ffi_export]宏:

rust 复制代码
#[ffi_export]
pub fn test_bool(value: bool) -> bool {
    !value
}

最后配置一下生成头文件名的方法generate_headers:

rust 复制代码
#[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

使用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();方法.
dart 复制代码
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 - GeneralDead Code Stripping配置改成No(这是iOS移除未使用的代码用的)
  • 在同级的Ohter Linker Flags中加入-all_load(这是加载所有符号表用的)

这里说明一点: -all_load和上文中的-force_load xxx.a作用是一致的,自行取其一配置即可.

配置完成之后, rebuildFlutter在iOS平台上也可以愉快的调用ffi了.

文中源码有所省略, 文末有开源代码仓库地址, 欢迎食用.

至此文章就结束了!


群内有各(pian)种(ni)各(jin)样(qun)的大佬,等你来撩.

联系作者

点此QQ对话 该死的空格 点此快速加群

开源地址

相关推荐
拭心9 分钟前
Google 提供的 Android 端上大模型组件:MediaPipe LLM 介绍
android
带电的小王2 小时前
WhisperKit: Android 端测试 Whisper -- Android手机(Qualcomm GPU)部署音频大模型
android·智能手机·whisper·qualcomm
coder_pig2 小时前
📝小记:Ubuntu 部署 Jenkins 打包 Flutter APK
flutter·ubuntu·jenkins
梦想平凡3 小时前
PHP 微信棋牌开发全解析:高级教程
android·数据库·oracle
元争栈道3 小时前
webview和H5来实现的android短视频(短剧)音视频播放依赖控件
android·音视频
阿甘知识库4 小时前
宝塔面板跨服务器数据同步教程:双机备份零停机
android·运维·服务器·备份·同步·宝塔面板·建站
元争栈道5 小时前
webview+H5来实现的android短视频(短剧)音视频播放依赖控件资源
android·音视频
捡芝麻丢西瓜5 小时前
flutter自学笔记5- dart 编码规范
flutter·dart
MuYe5 小时前
Android Hook - 动态加载so库
android
恋猫de小郭5 小时前
什么?Flutter 可能会被 SwiftUI/ArkUI 化?全新的 Flutter Roadmap
flutter·ios·swiftui