赞
踩
文章的图片链接都是在github上,可能需要...你懂得;本文含有大量关键步骤配置图片,强烈建议在合适环境下阅读
Flutter直接调用C层还是蛮有魅力,想想你练习C++,然后直接能用flutter在上层展示出效果,是不是就有大量练手的机会了,逻辑反手就用C++,Rust去写,给后面的接盘侠留下一座壮丽的克苏鲁神山,供其瞻仰
上面只是开个玩笑,目前flutter ffi的交互,主要是为了和底层交互的统一,还能直接使用到大量宝藏一样的底层库
目前ffi的同步调用还是比较可以,异步交互有办法去解决,但是使用起来比较麻烦
异步消息通信
模块中贴的issueFlutter和Rust的交互
本文是循序渐进式,比较全面的介绍了flutter的ffi使用,ffigen使用,最后才是rust交互
介绍;如果对ffi和ffigen不太关心,也可直接阅读rust交互
内容
# mac
ndk.dir=/Users/***/Develop/SDK/android_sdk/ndk/21.3.6528147
# windows
ndk.dir=F:\\SDK\\AndroidSDK\\ndk\\21.3.6528147
android {
...
//配置CMakeList路径
externalNativeBuild {
cmake {
path "../lib/native/CMakeLists.txt"
}
}
}
CMakeLists.txt
,先来看下Android的配置
native_batch
批量添加文件lib
,set(PROJECT_NAME "native_fun")
生成的名称应该为libnative_fun.so
# cmake_minimum_required 表示支持的 cmake 最小版本
cmake_minimum_required(VERSION 3.4.1)
# 项目名称
set(PROJECT_NAME "native_fun")
# 批量添加c文件
# add_library 关键字表示构建链接库,参数1是链接包名称; 参数2'SHARED'表示构建动态链接库; 参数2是源文件列表
file(GLOB_RECURSE native_batch ../../ios/Classes/native/*)
add_library(${PROJECT_NAME} SHARED ${native_batch})
可以发现file(GLOB_RECURSE native_batch ../../ios/Classes/native/*)
这边路径设置在iOS的Classes文件下,这边是为了方便统一编译native文件夹下的所有c文件,macOS和iOS需要放在Classes下,可以直接编译
但是macOS和iOS没法指定编译超过父节点位置,必须放在Classes子文件夹下,超过这个节点就没法编译
所以这边iOS和macOS必须要维护俩份相同c文件(建个文件夹吧,方便直接拷贝过去);Android,Windows,Linux可以指定到这俩个中的其中之一(建议指定iOS的Classes,避免一些灵异Bug)
# cmake_minimum_required 表示支持的 cmake 最小版本 cmake_minimum_required(VERSION 3.4.1) # 项目名称 set(PROJECT_NAME "libnative_fun") # 批量添加cpp文件 # add_library 关键字表示构建链接库,参数1是链接包名称; 参数2'SHARED'表示构建动态链接库; 参数2是源文件列表 file(GLOB_RECURSE native_batch ../../ios/Classes/native/*) add_library(${PROJECT_NAME} SHARED ${native_batch}) # Windows 需要把dll拷贝到bin目录 # 动态库的输出目录 set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/$<$<CONFIG:DEBUG>:Debug>$<$<CONFIG:RELEASE>:Release>") # 安装动态库的目标目录 set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") # 安装动态库,到执行目录 install(FILES "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/${PROJECT_NAME}.dll" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime)
# cmake_minimum_required 表示支持的 cmake 最小版本 cmake_minimum_required(VERSION 3.4.1) # 项目名称 if (WIN32) set(PROJECT_NAME "libnative_fun") else() set(PROJECT_NAME "native_fun") endif() # 批量添加c文件 # add_library 关键字表示构建链接库,参数1是链接包名称; 参数2'SHARED'表示构建动态链接库; 参数2是源文件列表 file(GLOB_RECURSE native_batch ../../ios/Classes/native/*) add_library(${PROJECT_NAME} SHARED ${native_batch}) # Windows 需要把dll拷贝到bin目录 if (WIN32) # 动态库的输出目录 set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/$<$<CONFIG:DEBUG>:Debug>$<$<CONFIG:RELEASE>:Release>") # 安装动态库的目标目录 set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") # 安装动态库,到执行目录 install(FILES "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/${PROJECT_NAME}.dll" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) endif()
class NativeFFI { NativeFFI._(); static DynamicLibrary? _dyLib; static DynamicLibrary get dynamicLibrary { if (_dyLib != null) return _dyLib!; if (Platform.isMacOS || Platform.isIOS) { _dyLib = DynamicLibrary.process(); } else if (Platform.isAndroid) { _dyLib = DynamicLibrary.open('libnative_fun.so'); } else if (Platform.isWindows) { _dyLib = DynamicLibrary.open('libnative_fun.dll'); } else { throw Exception('DynamicLibrary初始化失败'); } return _dyLib!; } }
/// 俩数相加
int ffiAddSyncInvoke(int a, int b) {
final int Function(int x, int y) nativeAdd = NativeFFI.dynamicLibrary
.lookup<NativeFunction<Int32 Function(Int32, Int32)>>("twoNumAdd")
.asFunction();
return nativeAdd(a, b);
}
#include <stdint.h>
#ifdef WIN32
#define DART_API extern "C" __declspec(dllexport)
#else
#define DART_API extern "C" __attribute__((visibility("default"))) __attribute__((used))
#endif
DART_API int32_t twoNumAdd(int32_t x, int32_t y){
return x + y;
}
/// 传递的回调 typedef _NativeCallback = Int32 Function(Int32 num); /// Native方法 typedef _NativeSyncCallback = Void Function( Pointer<NativeFunction<_NativeCallback>> callback, ); /// Dart结束回调: Void和void不同,所以要区分开 typedef _DartSyncCallback = void Function( Pointer<NativeFunction<_NativeCallback>> callback, ); /// 必须使用顶层方法或者静态方法 /// macos端可以打印出native层日志, 移动端只能打印dart日志 int _syncCallback(int num) { print('--------'); return num; } /// 在native层打印回调传入的值 void ffiPrintSyncCallback() { final _DartSyncCallback dartSyncCallback = NativeFFI.dynamicLibrary .lookup<NativeFunction<_NativeSyncCallback>>("nativeSyncCallback") .asFunction(); // 包装传递的回调 var syncFun = Pointer.fromFunction<_NativeCallback>(_syncCallback, 0); dartSyncCallback(syncFun); }
#include <stdint.h> #include <iostream> #ifdef WIN32 #define DART_API extern "C" __declspec(dllexport) #else #define DART_API extern "C" __attribute__((visibility("default"))) __attribute__((used)) #endif using namespace std; // 定义传递的回调类型 typedef int32_t (*NativeCallback)(int32_t n); DART_API void nativeSyncCallback(NativeCallback callback) { // 打印 std::cout << "native log callback(666) = " << callback(666) << std::endl; }
说明
异步交互的写法有点复杂,可以查看下面的讨论
异步通信需要导入额外c文件用作通信支持,但是如果你的iOS项目是swift项目,无法编译这些额外c文件
目前来看
使用
import 'dart:async'; import 'dart:ffi'; import 'dart:isolate'; import 'package:flutter/material.dart'; import 'package:flutter_ffi_toolkit/src/native_ffi.dart'; ReceivePort? _receivePort; StreamSubscription? _subscription; void _ensureNativeInitialized() { if (_receivePort == null) { WidgetsFlutterBinding.ensureInitialized(); final initializeApi = NativeFFI.dynamicLibrary.lookupFunction< IntPtr Function(Pointer<Void>), int Function(Pointer<Void>)>("InitDartApiDL"); if (initializeApi(NativeApi.initializeApiDLData) != 0) { throw "Failed to initialize Dart API"; } _receivePort = ReceivePort(); _subscription = _receivePort!.listen(_handleNativeMessage); final registerSendPort = NativeFFI.dynamicLibrary.lookupFunction< Void Function(Int64 sendPort), void Function(int sendPort)>('RegisterSendPort'); registerSendPort(_receivePort!.sendPort.nativePort); } } void _handleNativeMessage(dynamic address) { print('---------native端通信,地址: $address'); Pointer<Int32> point = Pointer<Int32>.fromAddress(address); print('---------native端通信,指针: $point'); dynamic data = point.cast(); print('---------native端通信,cast: $data'); } void ffiAsyncMessage(int a) { _ensureNativeInitialized(); final void Function(int x) asyncMessage = NativeFFI.dynamicLibrary .lookup<NativeFunction<Void Function(Int32)>>("NativeAsyncMessage") .asFunction(); asyncMessage(a); } void dispose() { // TODO _unregisterReceivePort(_receivePort.sendPort.nativePort); _subscription?.cancel(); _receivePort?.close(); }
// C #include <stdio.h> // Unix #include <unistd.h> #include <pthread.h> #include "dart_api/dart_api.h" #include "dart_api/dart_native_api.h" #include "dart_api/dart_api_dl.h" // Initialize `dart_api_dl.h` DART_EXPORT intptr_t InitDartApiDL(void* data) { return Dart_InitializeApiDL(data); } Dart_Port send_port_; DART_EXPORT void RegisterSendPort(Dart_Port send_port) { send_port_ = send_port; } void *thread_func(void *args) { printf("thread_func Running on (%p)\n", pthread_self()); sleep(2 /* seconds */); // doing something Dart_CObject dart_object; dart_object.type = Dart_CObject_kInt64; dart_object.value.as_int64 = reinterpret_cast<intptr_t>(args); Dart_PostCObject_DL(send_port_, &dart_object); pthread_exit(args); } DART_EXPORT void NativeAsyncMessage(int32_t x) { printf("NativeAsyncCallback Running on (%p)\n", pthread_self()); pthread_t message_thread; pthread_create(&message_thread, NULL, thread_func, (void *)&x); }
手写这些ffi交互代码,也是件比较麻烦的事,而且每个方法都要写对应的类型转换和相应的硬编码方法名,如果c的某个方法改变参数和方法名,再回去改对应的dart代码,无疑是一件蛋痛的事
flutter提供了一个自动生成ffi交互的代码,通俗的说:自动将c代码生成为对应dart的代码
ubuntu/linux
sudo apt-get install libclang-dev
Windows
winget install -e --id LLVM.LLVM
MacOS
安装 Xcode
安装 LLVM: brew install llvm
引入ffigen
dependencies:
ffigen: ^7.2.0
ffigen:
# 输出生成的文件路径
output: 'lib/src/ffigen/two_num_add.dart'
# 输出的类名
name: NativeLibrary
headers:
# 配置需要生成的文件
entry-points:
- 'ios/Classes/native/ffigen/add.cpp'
# 保证只转换two_num_add.cpp文件,不转换其包含的库文件,建议加上
include-directives:
- 'ios/Classes/native/ffigen/add.cpp'
dart run ffigen
DART_API
,不然无法生成对应dart文件DART_API
,不然无法编译该方法#include <stdint.h>
#ifdef WIN32
#define DART_API extern "C" __declspec(dllexport)
#else
#define DART_API extern "C" __attribute__((visibility("default"))) __attribute__((used))
#endif
// DART_API int32_t twoNumAddGen(int32_t x, int32_t y){
// return x + y;
// }
int32_t twoNumAddGen(int32_t x, int32_t y){
return x + y;
}
// AUTO GENERATED FILE, DO NOT EDIT. // // Generated by `package:ffigen`. import 'dart:ffi' as ffi; class NativeLibrary { /// Holds the symbol lookup function. final ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName) _lookup; /// The symbols are looked up in [dynamicLibrary]. NativeLibrary(ffi.DynamicLibrary dynamicLibrary) : _lookup = dynamicLibrary.lookup; /// The symbols are looked up with [lookup]. NativeLibrary.fromLookup( ffi.Pointer<T> Function<T extends ffi.NativeType>(String symbolName) lookup) : _lookup = lookup; int twoNumAddGen( int x, int y, ) { return _twoNumAddGen( x, y, ); } late final _twoNumAddGenPtr = _lookup<ffi.NativeFunction<ffi.Int32 Function(ffi.Int32, ffi.Int32)>>( 'twoNumAddGen'); late final _twoNumAddGen = _twoNumAddGenPtr.asFunction<int Function(int, int)>(); }
class NativeFFI { NativeFFI._(); static DynamicLibrary? _dyLib; static DynamicLibrary get dynamicLibrary { if (_dyLib != null) return _dyLib!; if (Platform.isMacOS || Platform.isIOS) { _dyLib = DynamicLibrary.process(); } else if (Platform.isAndroid) { _dyLib = DynamicLibrary.open('libnative_fun.so'); } else if (Platform.isWindows) { _dyLib = DynamicLibrary.open('libnative_fun.dll'); } else { throw Exception('DynamicLibrary初始化失败'); } return _dyLib!; } }
NativeLibrary(NativeFFI.dynamicLibrary).twoNumAddGen(a, b);
使用flutter_rust_bridge:flutter_rust_bridge
下面全平台的配置,我成功编译运行后写的一份详细指南(踩了一堆坑),大家务必认真按照步骤配置~
大家也可以参考官方文档,不过我觉得写的更加人性化,hhhhhh…
name
参数,请保持一致,此处示例是name = "rust_ffi"
[package]
name = "rust_ffi"
version = "0.1.0"
edition = "2021"
[lib]
name = "rust_ffi"
crate-type = ["staticlib", "cdylib"]
[build-dependencies]
flutter_rust_bridge_codegen = "=1.51.0"
[dependencies]
flutter_rust_bridge = "=1.51.0"
flutter_rust_bridge_macros = "=1.51.0"
dependencies:
# https://pub.dev/packages/flutter_rust_bridge
flutter_rust_bridge: 1.51.0
ffi: ^2.0.1
dev_dependencies:
ffigen: ^7.0.0
# 必须
cargo install flutter_rust_bridge_codegen
# iOS和macOS 必须需要
cargo install cargo-xcode
安装LLVM
ubuntu/linux
sudo apt-get install libclang-dev
Windows
winget install -e --id LLVM.LLVM
MacOS
安装 Xcode
安装 LLVM: brew install llvm
生成命令
flutter_rust_bridge_codegen -r rust/src/api.rs -d lib/ffi/rust_ffi/rust_ffi.dart
flutter_rust_bridge_codegen -r rust/src/api.rs -d lib/ffi/rust_ffi/rust_ffi.dart -c ios/Runner/bridge_generated.h -c macos/Runner/bridge_generated.h
flutter_rust_bridge
版本flutter_rust_bridge_codegen
和flutter_rust_bridge
升级对应版本# 自动安装最新版本
cargo install flutter_rust_bridge_codegen
# 指定版本
cargo install flutter_rust_bridge_codegen --version 1.51.0 --force
cargo-ndk
:它能够将代码编译到适合的 JNI 而不需要额外的配置cargo install cargo-ndk
rustup target add aarch64-linux-android
rustup target add armv7-linux-androideabi
rustup target add x86_64-linux-android
rustup target add i686-linux-android
# mac
ANDROID_NDK=/Users/***/Develop/SDK/android_sdk/ndk/21.3.6528147
# windows
ANDROID_NDK=F:\\SDK\\AndroidSDK\\ndk\\21.3.6528147
android/app/build.gradle
的最后添加下面几行
[ new Tuple2('Debug', ''), new Tuple2('Profile', '--release'), new Tuple2('Release', '--release') ].each { def taskPostfix = it.first def profileMode = it.second tasks.whenTaskAdded { task -> if (task.name == "javaPreCompile$taskPostfix") { task.dependsOn "cargoBuild$taskPostfix" } } tasks.register("cargoBuild$taskPostfix", Exec) { // Until https://github.com/bbqsrc/cargo-ndk/pull/13 is merged, // this workaround is necessary. def ndk_command = """cargo ndk \ -t armeabi-v7a -t arm64-v8a -t x86_64 -t x86 \ -o ../android/app/src/main/jniLibs build $profileMode""" workingDir "../../rust" environment "ANDROID_NDK_HOME", "$ANDROID_NDK" if (org.gradle.nativeplatform.platform.internal.DefaultNativePlatform.currentOperatingSystem.isWindows()) { commandLine 'cmd', '/C', ndk_command } else { commandLine 'sh', '-c', ndk_command } } }
# 64 bit targets (真机 & 模拟器):
rustup target add aarch64-apple-ios x86_64-apple-ios
# New simulator target for Xcode 12 and later
rustup target add aarch64-apple-ios-sim
# 在rust项目下执行该命令
cargo xcode
添加一些绑定文件
ios/Runner.xcodeproj
, 接着把 $crate/$crate.xcodeproj
添加为子项目:File —> Add Files to “Runner”add
Runner
根项目,TARGETS —> Build Phases —> Target Dependencies :请添加 $crate-staticlib
lib$crate_static.a
绑定头文件
flutter_rust_bridge_codegen
会创建一个 C 头文件,里面列出了 Rust 库导出的所有符号,需要使用它,确保 Xcode 不会将符号去除。
在项目中需要添加 ios/Runner/bridge_generated.h
(或者 macos/Runner/bridge_generated.h
)
flutter_rust_bridge_codegen -r rust/src/api.rs -d lib/ffi/rust_ffi/rust_ffi.dart -c ios/Runner/bridge_generated.h -c macos/Runner/bridge_generated.h
在 ios/Runner/Runner-Bridging-Header.h
中添加
#import "GeneratedPluginRegistrant.h"
+#import "bridge_generated.h"
ios/Runner/AppDelegate.swift
中添加
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
+ let dummy = dummy_method_to_enforce_bundling()
+ print(dummy)
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
说明
macos上面指向有个很奇怪的情况,官方文档说明的是需要链接$crate-cdylib
,$crate.dylib
;但是链接这个库,用xcode编译可以执行,但是使用android studio直接编译执行的时候会报错
与iOS保持一致,链接 $crate-staticlib
,lib$crate_static.a
,可以顺利执行
下面配置,大家按需配置,我这边使用静态库能成功,链接动态库会失败
开始配置
# 在rust项目下执行该命令
cargo xcode
macos/Runner.xcodeproj
, 接着把 $crate/$crate.xcodeproj
添加为子项目:File —> Add Files to “Runner”Runner
根项目,TARGETS —> Build Phases —> Target Dependencies :请添加 $crate-staticlib
(或者 $crate-staticlib )lib$crate_static.a
(或者 $crate.dylib
)$crate.dylib
的时候
Link Binary With Libraries
的时候,macOS添加的**.dylib要选择Optional
Flutter 在 MacOS 上默认不使用符号,我们需要添加我们自己的
在 Build Settings 标签页中
把 Objective-C Bridging Header 设置为: Runner/bridge_generated.h
bridge_generated.h
文件加入macos项目macos/Runner/AppDelegate.swift
中添加
import Cocoa
import FlutterMacOS
@NSApplicationMain
class AppDelegate: FlutterAppDelegate {
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
+ dummy_method_to_enforce_bundling()
return true
}
}
说明
目前使用flutter_rust_bridge
等库保持在1.51.0
版本
将flutter_rust_bridge
更新到1.54.0
版本
flutter_rust_bridge_codegen
去生成代码failed to run custom build command for `dart-sys v2.0.1`
Microsoft.CppCommon.targets(247,5): error MSB8066
猜测是作者新版本dart-sys v2.0.1
这个库有问题,导致编译产物路径出了问题,报错了上面的错
目前本demo的版本号限制死在1.51.0
版本;后面作者可能会解决该问题,需要使用新版本可自行尝试
# 指定版本
cargo install flutter_rust_bridge_codegen --version 1.51.0 --force
重要说明
因为windows上编译需要下载Corrosion,需要开启全局xx上网,不然可能在编译的时候,会存在无法下载Corrosion的报错
推荐工具使用sstap 1.0.9.7版本,这个版本内置全局规则(可戴笠软件,不仅限浏览器),后面的版本该规则被删了
rust.make
git clone https://github.com/corrosion-rs/corrosion.git
# Optionally, specify -DCMAKE_INSTALL_PREFIX=<target-install-path>. You can install Corrosion anyway
cmake -Scorrosion -Bbuild -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release
# This next step may require sudo or admin privileges if you're installing to a system location,
# which is the default.
cmake --install build --config Release
windows和linux需要添加个rust.make文件:
# We include Corrosion inline here, but ideally in a project with # many dependencies we would need to install Corrosion on the system. # See instructions on https://github.com/AndrewGaspar/corrosion#cmake-install # Once done, uncomment this line: # find_package(Corrosion REQUIRED) include(FetchContent) FetchContent_Declare( Corrosion GIT_REPOSITORY https://github.com/AndrewGaspar/corrosion.git GIT_TAG origin/master # Optionally specify a version tag or branch here ) FetchContent_MakeAvailable(Corrosion) corrosion_import_crate(MANIFEST_PATH ../rust/Cargo.toml CRATES rust_ffi) # Flutter-specific set(CRATE_NAME "rust_ffi") target_link_libraries(${BINARY_NAME} PRIVATE ${CRATE_NAME}) list(APPEND PLUGIN_BUNDLED_LIBRARIES $<TARGET_FILE:${CRATE_NAME}-shared>)
调整
windows/CMakeLists.txt
添加rust.cmake文件 # Generated plugin build rules, which manage building the plugins and adding
# them to the application.
include(flutter/generated_plugins.cmake)
+include(./rust.cmake)
# === Installation ===
# Support files are copied into place next to the executable, so that it can
rust.cmake
依赖 Corrosion。需求修改 linux/CMakeLists.txt
的这一行-cmake_minimum_required(VERSION 3.10)
+cmake_minimum_required(VERSION 3.12)
...
# Generated plugin build rules, which manage building the plugins and adding
# them to the application.
include(flutter/generated_plugins.cmake)
+include(./rust.cmake)
# === Installation ===
# By default, "installing" just makes a relocatable bundle in the build
class NativeFFI { NativeFFI._(); static DynamicLibrary? _dyLib; static DynamicLibrary get dyLib { if (_dyLib != null) return _dyLib!; const base = 'rust_ffi'; if (Platform.isIOS) { _dyLib = DynamicLibrary.process(); } else if (Platform.isMacOS) { _dyLib = DynamicLibrary.executable(); } else if (Platform.isAndroid) { _dyLib = DynamicLibrary.open('lib$base.so'); } else if (Platform.isWindows) { _dyLib = DynamicLibrary.open('$base.dll'); } else { throw Exception('DynamicLibrary初始化失败'); } return _dyLib!; } } class NativeFun { static final _ffi = RustFfiImpl(NativeFFI.dyLib); static Future<int> add(int left, int right) async { int sum = await _ffi.add(left: left, right: right); return sum; } }
void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return const MaterialApp(title: 'Flutter Demo', home: MyHomePage()); } } class MyHomePage extends StatefulWidget { const MyHomePage({super.key}); @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { int _counter = 0; void _incrementCounter() async { _counter = await NativeFun.add(_counter, 2); setState(() {}); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Rust_Bridge Demo')), body: Center( child: Text( 'Count: $_counter', style: Theme.of(context).textTheme.headline4, ), ), floatingActionButton: FloatingActionButton( onPressed: _incrementCounter, tooltip: 'Increment', child: const Icon(Icons.add), ), ); } }
对于rust这块,这些配置确实有点麻烦,但是配置完,后面就不用管了
痛苦一次就行了.
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。