针对常规的网络应用型APP,Flutter + Go 可以很好的兼顾性能与开发效率,而对于某些需要极致控制内存占用,需要与C库进行高效交互的程序,Flutter + Rust 则是另一个可选项。
Flutter + Rust 最直接的方式可能是使用 Flutter Favorite 的库 flutter_rust_bridge,基于代码模板以及提供多个工具简化整个对接流程。然而,对于一些只需要导出少量 rust 接口的功能,使用 flutter_rust_bridge 未免引入了过多复杂性,而直接使用 dart ffi 反而是一个相对精简的实现。
Rust <-> C
配置 Cargo.toml 输出 C 库
[package]
name = "xxx"
version = "0.1.0"
edition = "2024"
[lib]
name = "xxx"
crate-type = ["cdylib", "rlib"]
基于 cbindgen 导出 .h 头文件,创建 cbindgen.toml:
language = "C"
header = "/* Auto-generated by cbindgen */"
include_version = true
braces = "SameLine"
line_length = 100
tab_width = 4
documentation = true
documentation_style = "doxy"
[export]
include = ["xxx"]
exclude = []
[fn]
rename_args = "GeckoCase"
args = "auto"
sort_by = "Name"
[struct]
rename_fields = "ScreamingSnakeCase"
must_use = "/*must_use*/"
derive_const_casts = true
derive_mut_casts = true
[enum]
rename_variants = "ScreamingSnakeCase"
must_use = "/*must_use*/"
[const]
allow_static_const = true
allow_constexpr = false
[macro_expansion]
bitflags = true
然后更新 build.rs 编译脚本
use std::env;
use std::path::PathBuf;
fn main() {
let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
let output_dir = PathBuf::from(&crate_dir);
// Generate the header file
cbindgen::Builder::new()
.with_crate(crate_dir.clone())
.with_language(cbindgen::Language::C)
.generate()
.expect("Unable to generate bindings")
.write_to_file(output_dir.join("bindings.h"));
// Rerun if any of these change
println!("cargo:rerun-if-changed=src/dart");
println!("cargo:rerun-if-changed=cbindgen.toml");
}
需要导出的Rust函数添加 #[unsafe(no_mangle)]
与 extern "C"
,譬如,需要与 dart 对接必须要实现以下函数,接收 api_data 然后调用 Dart_InitializeApiDL 初始化,另外,Rust 到 Dart 的异步通讯可通过 dart_port 的方式实现:
#[unsafe(no_mangle)]
pub extern "C" fn initialize_dart_api(api_data: *mut ::core::ffi::c_void, dart_port: i64, is_debug: bool) {
println!("Initializing Dart API");
unsafe {
if Dart_InitializeApiDL(api_data) != 0 {
panic!("failed to initialize Dart DL C API: version mismatch. must update include/ to match Dart SDK version");
}
}
// Store the dart port globally
if let Ok(mut port) = DART_PORT.lock() {
*port = Some(dart_port);
}
// Try to initialize logger, but don't panic if it's already initialized
let log_level = if is_debug { "debug" } else { "info" };
let _ = env_logger::try_init_from_env(Env::default().default_filter_or(log_level));
info!("Dart API initialized with port: {}", dart_port);
}
这样打包Rust动态库的同时就会输出对应的头文件,譬如以上函数的定义:
void initialize_dart_api(void *api_data, int64_t dart_port, bool is_debug);
dart <-> C
dart 根据 C 头文件与动态库,通过 ffi 的形式交互
dart run ffigen --config ffigen.yaml
其中 ffigen.yaml:
# Run with `dart run ffigen --config ffigen.yaml`.
name: XXXBindings
description: |
Bindings for `bindings.h`.
Regenerate bindings with `dart run ffigen --config ffigen.yaml`.
output: 'lib/generated/xxx_bindings_generated.dart'
headers:
entry-points:
- '../core/bindings.h'
include-directives:
- '../core/bindings.h'
compiler-opts:
- '-I/usr/lib/clang/20/include/'
- '-I/usr/lib/clang/19/include/'
preamble: |
// ignore_for_file: always_specify_types
// ignore_for_file: camel_case_types
// ignore_for_file: non_constant_identifier_names
comments:
style: any
length: full
运行后输出 xxx_bindings_generated.dart 为 dart 下的绑定,如下:
void initialize_dart_api(
ffi.Pointer<ffi.Void> api_data,
int dart_port,
bool is_debug,
) {
return _initialize_dart_api(api_data, dart_port, is_debug);
}
late final _initialize_dart_apiPtr =
_lookup<
ffi.NativeFunction<
ffi.Void Function(ffi.Pointer<ffi.Void>, ffi.Int64, ffi.Bool)
>
>('initialize_dart_api');
late final _initialize_dart_api = _initialize_dart_apiPtr
.asFunction<void Function(ffi.Pointer<ffi.Void>, int, bool)>();
使用时需要增加动态库加载功能,并且最好是增加一个wapper类封装,类似 Rust 常用的 unsafe 机制。
动态库加载:
final dartffi.DynamicLibrary _dylib = () {
if (Platform.isMacOS || Platform.isIOS) {
return dartffi.DynamicLibrary.open('$_libName.framework/$_libName');
}
if (Platform.isAndroid || Platform.isLinux) {
return dartffi.DynamicLibrary.open('lib$_libName.so');
}
if (Platform.isWindows) {
return dartffi.DynamicLibrary.open('$_libName.dll');
}
throw UnsupportedError('Unknown platform: ${Platform.operatingSystem}');
}();
wapper类实例化时必须先调用 initialize_dart_api 函数,譬如一个简单的实现, 启动时传入 _receivePort.sendPort.nativePort,然后通过监听 _receivePort 将 Rust 的异步消息以Stream的方式广播出去。
final XXXBindings _xxxBindings = XXXBindings(_dylib);
StreamController<Message>? _messageController;
static XXXService get instance {
_instance ??= XXXService._internal();
return _instance!;
}
XXXService._internal() {
_receivePort = ReceivePort();
_messageController = StreamController<Message>.broadcast();
_engineStartedController!.add(false);
// Initialize the Dart API in the native library
_xxxBindings.initialize_dart_api(
dartffi
.NativeApi
.initializeApiDLData, // API data - pass null from Dart side
_receivePort.sendPort.nativePort, // Native port ID
kDebugMode, // Debug mode flag
);
// Start listening to the receive port
_startListening();
}
void _startListening() {
_receivePort.listen(
(rawMessage) {
if (_disposed || _messageController == null) return;
try {
if (rawMessage is Uint8List) {
final fbMessage = fb.Message(rawMessage);
final message = Message.fromFlatBuffer(fbMessage);
// logInfo('Received message');
_messageController!.add(message);
} else {
logWarning(
'Received unexpected message type: ${rawMessage.runtimeType}',
);
}
} catch (e, stackTrace) {
logWarning('Error processing message: $e');
logWarning('Stack trace: $stackTrace');
// Add error to stream instead of silently ignoring
_messageController!.addError(e, stackTrace);
}
},
onError: (error, stackTrace) {
logWarning('ReceivePort error: $error');
logWarning('Stack trace: $stackTrace');
_messageController?.addError(error, stackTrace);
},
onDone: () {
logInfo('ReceivePort closed');
_messageController?.close();
},
);
}
内存管理
需要注意的是 Rust 下面申请的内存只能 Rust 下释放,Dart 亦同理。
譬如,如果Rust以如下方式返回 CString:
CString::new(xxx_string)
.map(|c_string| c_string.into_raw())
.unwrap_or(std::ptr::null_mut())
则需要提供一个接口到 dart,使该字符串使用后能释放:
#[unsafe(no_mangle)]
pub extern "C" fn free_string(ptr: *mut c_char) {
if !ptr.is_null() {
unsafe {
let _ = CString::from_raw(ptr);
}
}
}
对于 dart 申请的内存,也可以使用类似的机制 :
final dataPtr = malloc.allocate<dartffi.Uint8>(...)
try {
// ...
} finally {
malloc.free(dataPtr);
}