Dart 遇见 Rust:天作之合 ✨
一小块 Dart
Dart是一种针对任何平台上的快速应用程序进行客户端优化的语言,它可以轻松构建应用程序的 UI,并且是一种非常好用的语言,它是Flutter框架使用的语言,Flutter 是 Google 的 UI 工具包,用于从单个代码库为移动、Web 和桌面构建漂亮的、本机编译的应用程序。
进入 Rust
Rust速度极快,内存效率高,无需运行时或垃圾收集器,它可以为性能关键型服务提供支持,在嵌入式设备上运行,并轻松与其他语言集成。
我们在Sunshine中同时使用 Rust 和 Dart(在 Flutter 中),以使开源资助计划能够在链上生态系统中轻松运作。
我们几乎所有的代码都是用 Rust 编写的,这就是为什么我们需要考虑在客户端应用程序中使用相同的代码和相同的逻辑,但是如何做呢?
好吧,让我们看看这里有哪些选择
使用 Flutter 平台通道
Flutter 平台通道是一个灵活的系统,允许您调用特定于平台的 API,无论是在 Android 上使用 Kotlin 或 Java 代码,还是在 iOS 上使用 Swift 或 Objective-C 代码。
这样,我们必须首先将 Rust 代码绑定到 Java(适用于 Android)、Swift(适用于 iOS)和 WASM(适用于 Web),但这会过于复杂,并且可能会导致将来的性能问题。这里有一个简单的图表,可以让您大致了解它的样子:
但是正如您所看到的,这里涉及很多开销,并且数据序列化/反序列化在运行时成本非常高,所以我们还能做些什么呢?
FFI,打破界限
正如维基百科所说:外部函数接口(FFI)是一种机制,通过这种机制,用一种编程语言编写的程序可以调用例程或使用用另一种编程语言编写的服务。
嗯,有意思,看看能做什么。Dart 支持 FFI 吗?
是的!实际上 FFI 是在去年年底Dart 2.5中引入的,所以它目前仍在积极开发中,但相当稳定。
在使用Dart 的 FFI 示例之后,我开始研究flutterust一个简单的模板来展示如何通过 FFI 将 Flutter/Dart 与 Rust 结合使用。
这里的简单想法是,我们为所有支持的目标构建我们的 rust 代码,然后构建使用这些目标的 Flutter 包。
以下是使用 FFI 方法的好处
- 没有 Swift/Kotlin 包装器
- 没有消息传递
- Dart 上没有 async/await
- 一次编写,随处使用
- 没有垃圾收集
- 无需导出
aar
捆绑包或.framework's
所以,它会是这样的:
这太酷了,这是一个简单的例子
学习如何数数!
我们将使用相同的 Flutter hello world 示例,但不是在 Dart 端执行逻辑(增加计数器),而是在 Rust 端执行。
我们的项目结构:
.
├── android
├── ios
├── lib <- The Flutter App Code
├── native <- Containes all the Rust Code
│ ├── adder
│ └── adder-ffi
├── packages <- Containes all the Dart Packages that bind to the Rust Code
│ └── adder_ffi
├── target <- The compiled rust code for every arch
│ ├── aarch64-apple-ios
│ ├── aarch64-linux-android
│ ├── armv7-linux-androideabi
│ ├── debug
│ ├── i686-linux-android
│ ├── universal
│ ├── x86_64-apple-ios
│ └── x86_64-linux-android
└── test
锈蚀面
首先创建一个 Cargo Workspace,所以我们Cargo.toml
在 Flutter 应用的根目录下添加一个简单的
[workspace]
members = ["native/*"]
[profile.release]
lto = true
codegen-units = 1
debug = true # turn it off if you want.
创建我们的简单adder
包
$ cargo new --lib native/adder
让我们写一些代码
pub fn add(a: i64, b: i64) -> i64 {
a.wrapping_add(b)
}
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(super::add(2, 2), 4);
}
}
很无聊不是吗?🥱
让我们向世界展示我们的新add
功能:)
$ cargo new --lib native/adder-ffi
别忘了在native/adder-ffi/Cargo.toml
[lib]
name = "adder_ffi"
crate-type = ["cdylib", "staticlib"]
[dependencies]
adder = { path = "../adder" }
// lib.rs
#[no_mangle]
pub extern "C" fn add(a: i64, b: i64) -> i64 {
adder::add(a, b)
}
不错,但是如何编译移动端的代码呢?
嗯,这有点复杂。我们可以直接使用 Cargo,当然也可以,但我们需要配置很多其他东西,所以我们将借助其他工具来帮我们完成这些工作,比如cargo-lipo
和cargo-ndk
。
将我们的 Rust 代码编译到所有这些平台后:
aarch64-apple-ios
aarch64-linux-android
armv7-linux-androideabi
i686-linux-android
x86_64-apple-ios
x86_64-linux-android
我们已准备好进行下一步,我们将编译后的代码复制到特定位置
首先生成一个以我们的 rust crate 命名的 Flutter 插件:
$ flutter create --template=plugin packages/adder
target/universal/debug/libadder_ffi.a -> packages/adder/ios/libadder_ffi.a
target/aarch64-linux-android/debug/libadder_ffi.so -> packages/adder/android/src/main/jniLibs/arm64-v8a/libadder_ffi.so
...
...other android libs
我们准备好了吗?嗯,技术上来说,是的,但是 Xcode 还有另一件事要做,比如为我们的 iOS FFI 编写一个C 头文件。如果你在 macOS 上开发,你应该执行这里的这些步骤,除此之外,你还可以进行下一步,将 Flutter 包写入我们的 rust 库。
达特那边
回到 Dart,在我们生成的 Flutter 插件中,我们将定义 Rust 函数在 Dart 代码中的样子(类型定义)
import 'dart:ffi';
// For C/Rust
typedef add_func = Int64 Function(Int64 a, Int64 b);
// For Dart
typedef Add = int Function(int a, int b);
我们需要一个根据平台(例如 iOS/Android 或 Linux/macOS 或其他平台)加载 rust 库的函数。
import 'dart:io' show Platform;
DynamicLibrary load({String basePath = ''}) {
if (Platform.isAndroid || Platform.isLinux) {
return DynamicLibrary.open('${basePath}libadder_ffi.so');
} else if (Platform.isIOS) {
// iOS is statically linked, so it is the same as the current process
return DynamicLibrary.process();
} else if (Platform.isMacOS) {
return DynamicLibrary.open('${basePath}libadder_ffi.dylib');
} else if (Platform.isWindows) {
return DynamicLibrary.open('${basePath}libadder_ffi.dll');
} else {
throw NotSupportedPlatform('${Platform.operatingSystem} is not supported!');
}
}
class NotSupportedPlatform implements Exception {
NotSupportedPlatform(String s);
}
最后创建一个包含 ffi 函数的简单类
class Adder {
static DynamicLibrary _lib;
Adder() {
if (_lib != null) return;
// for debugging and tests
if (Platform.isWindows || Platform.isLinux || Platform.isMacOS) {
_lib = load(basePath: '../../../target/debug/');
} else {
_lib = load();
}
}
}
add
方法如下
int add(int a, int b) {
// get a function pointer to the symbol called `add`
final addPointer = _lib.lookup<NativeFunction<add_func>>('add');
// and use it as a function
final sum = addPointer.asFunction<Add>();
return sum(a, b);
}
到目前为止一切顺利,让我们在 Flutter 应用中使用它
在pubspec.yaml
应用程序的下面添加我们的adder
包dependencies
adder:
path: packages/adder_ffi
并lib/main.dart
改变方法的逻辑_incrementCounter
以使用我们的 Rust 逻辑
import 'package:adder/adder.dart';
// in the `MyHomePage` add
final adder = Adder();
// and latter in `_MyHomePageState` replace
...
void _incrementCounter() {
setState(() {
_counter = widget.adder.add(_counter, 1);
});
}
...
并在 Android 模拟器或 iOS 模拟器上启动 Flutter 应用程序并进行测试🔥。
呼..
但我们发现这样做太无聊了,尤其是在使用其他构建系统(例如 Xcode 和 Android NDK 工具链)并将所有内容链接在一起时🤦♂️。因此,我们尝试将所有内容自动化,但我们需要一种易于使用、跨平台且对持续集成友好的工具。
Cargo-make 来救援🚀
cargo-make是一个用 Rust 构建的跨平台任务运行器和构建工具,它真的是一个非常棒的工具,它可以帮助您在一组简单的任务中编写工作流程,并且它还有很多其他很酷的功能,比如很容易在其中添加内联脚本等等。你可以在sunshine-flutter
上看到我们如何使用它。
就是这样,我希望它有助于理解 Dart FFI 和 Rust 如何协同工作。
接下来,如何处理异步 Rust 和 Dart FFI,
我将很快在下一篇博文中讨论这个问题 :)
现在,您可以看到我开始破解所scrap
创建的包,以演示如何将异步 Rust 与 Dart 集成。
其他有趣的 Rust + 移动 FFI 开发
- https://dart.dev/guides/libraries/c-interop
- https://flutter.dev/docs/development/platform-integration/c-interop
- https://github.com/dart-lang/samples/blob/master/ffi/structs/structs.dart
- https://mozilla.github.io/firefox-browser-architecture/experiments/2017-09-06-rust-on-ios.html
- https://mozilla.github.io/firefox-browser-architecture/experiments/2017-09-21-rust-on-android.html