flutter_rust_bridge: 用于 Flutter 和 Rust 的高级内存安全绑定生成器

High-level memory-safe binding generator for Flutter/Dart <-> Rust

Rust Package Flutter Package Stars CI Example Codacy Badge

Logo

想把 Flutter(一个跨平台的热重载快速开发 UI 工具包)和 Rust(一种使每个人都能构建可靠和高效软件的语言)的优点结合起来?它来了!

🚀 优势

  • 内存安全: 完全不用考虑 malloc 和 free.
  • 特性丰富:支持类似 Rust 的枚举,根据平台自动优化的 Vec, 可以递归的 struct, 大数组零拷贝,流数据 (迭代器) 抽象,错误处理 (Result), 可取消的任务,并发控制,等。在 这里 查看所有特性。
  • 异步支持: Rust 代码永远不会阻塞 Flutter. 在 Flutter 的主隔离区 main isolate (或者说 thread) 中自然的调用 Rust 代码。
  • 轻量级: 这不是一个全面的框架,你可以自由的使用你喜欢的 Flutter 和 Rust 第三方库。例如,使用 Flutter 库(比如 MobX)进行状态管理更加优雅简单(相比于在 Rust 中实现); 使用 Rust 实现一个照片处理的算法更加快速和安全 (相比于在 Flutter 中实现).
  • 跨平台: 支持 Android, iOS, Windows, Linux, MacOS (Web coming soon)
  • 方便 & 易于代码审查: 这个库只是简单地模拟了人类编写的模板代码,一点也不神奇!如果你想让说服你自己(或你的团队)相信它是安全的, 没有多少代码可看。 (更多 safety concerns.)
  • 快速: 这个库只做了一个浅层次的包装(尽管功能丰富),没有 pritobuf 序列化的开销,因此高性能。(更多的 benchmarks 即将到来) (扔掉了像线程池这样的组件,使它更快)
  • 与纯 Dart 兼容: 尽管名字里包含了 Rust,这个库 100% 兼容 Pure Dart

💡 用户指南

请看 用户指南 for show-me-the-code, 教程, 特性 and much more.

📎 P.S. 方便的 Flutter 测试

如果您想在 Flutter 中方便地编写和调试测试,行为历史、时间回溯、屏幕截图、快速重新执行、视频录制、互动模式等功能,这里有我的另一个开源库:flutter_convenient_test.

✨ 贡献者

All Contributors

Thanks goes to these wonderful people (emoji key following all-contributors specification):


fzyzcjy

💻 📖 💡 🤔 🚧

Viet Dinh

💻 ⚠️ 📖

Joshua Wade

💻

Marcel

💻

rustui

📖

Michael Bryan

💻

bus710

📖

Sebastian Urban

💻

Daniel

💻

Kevin Li

💻 📖

Patrick Auernig

💻

Anton Lazarev

💻

Unoqwy

💻

Febrian Setianto

📖

syndim

💻

sagu

💻 📖

Ikko Ashimine

📖

alanlzhang

💻 📖

Sai Chaitanya

💻

Ares Andrew

📖

raphaelrobert

📖

thomas725

📖

Daniel Porteous (dport)

📖

Wouter Ensink

📖

老董

💻 📖

Lattice 0

💻 📖

orange soeur

📖

Rom's

💻 📖

Cupnfish

💻

More specifically, thanks for all these contributions:

  • Desdaemon: Support not only simple enums but also enums with fields which gets translated to native enum or freezed class in Dart. Support the Option type as nullable types in Dart. Support Vec of Strings type. Support comments in code. Add marker attributes for future usage. Add Linux and Windows support for with-flutter example, and make CI works for that. Avoid parameter collision. Overhaul the documentation and add several chapters to demonstrate configuring a Flutter+Rust project in all five platforms. Refactor command module. Precompiled binary CI workflow. Fix bugs.
  • SecondFlight: Allow structs and enums to be imported from other files within the crate by creating source graph. Auto-create relavent dir. Fix store_dart_post_cobject error with ffigen 6.0.
  • Unoqwy: Add struct mirrors, such that types in the external crates can be imported and used without redefining and copying.
  • antonok-edm: Avoid converting syn types to strings before parsing to improve code and be more robust.
  • lattice0: Support methods, such that Rust struct impls can be converted to Dart class methods. StreamSink at any argument.
  • sagudev: Make code generator a lib. Add error types. Depend on cbindgen. Fix LLVM paths. Update deps. Fix CI errors.
  • surban: Support unit return type. Skip unresolvable modules. Ignore prefer_const_constructors. Non-final Dart fields.
  • trobanga: Add support for [T;N] structs. Add usize support. Add a cmd argument. Separate dart tests.
  • Roms1383: Fix build_runner calling bug. Remove global ffigen dependency. Improve version check. Fix enum name-variant conflicts. Update CI. Code cleanup.
  • dbsxdbsx: Allow generating multiple Rust and Dart files.
  • AlienKevin: Add flutter example for macOS. Add doc for Android NDK bug.
  • alanlzhang: Add generation for Dart metadata.
  • efc-mw: Improve Windows encoding handling.
  • valeth: Rename callFfi's port.
  • Cupnfish: Allow multi mirror.
  • sccheruku: Prevent double-generating utility.
  • w-ensink: Improve doc. Fix CI. Refactor. Add tests.
  • Michael-F-Bryan: Detect broken bindings.
  • bus710: Add a case in troubleshooting.
  • Syndim: Add a bracket to box.
  • banool: Fix symbol-stripping doc.
  • TENX-S: Improve doc. Reproduce a bug.
  • raphaelrobert: Remove oudated doc.
  • thomas725: Improve doc.
  • juzi5201314: Improve doc.
  • feber: Fix doc link.
  • rustui: Fix a typo.
  • eltociear: Fix a typo.

快速开始

像平常一样编写你的 Rust 函数和类型定义。

// 一个普通的 Rust 函数 ...
pub fn draw_tree(root: TreeNode, mode: DrawMode) -> Result<Vec<u8>> { /* ... */ }

// ... 和一些丰富的类型
pub struct TreeNode { pub value: String, pub children: Vec<MyTreeNode> }
pub enum DrawMode { Colorful {palette: String}, Grayscale }

安装代码生成器 flutter_rust_bridge_codegen:

cargo install flutter_rust_bridge_codegen
# 或者使用 cargo-binstall
cargo binstall flutter_rust_bridge_codegen
# 或者使用 scoop (Windows)
scoop bucket add frb https://github.com/Desdaemon/scoop-repo
scoop install flutter_rust_bridge_codegen
# 或者使用 Homebrew
brew install desdaemon/repo/flutter_rust_bridge_codegen

(感谢 @Desdaemon 将脚本发布到 brew/scoop)

接着运行代码生成。

注意:安装需要一些步骤。你可以查看 教程, 从模板创建项目 或者 与现有项目集成 等章节获取更多信息.

flutter_rust_bridge_codegen --rust-input path/to/api.rs \
                            --dart-output path/to/bridge_generated.dart

绑定代码生成之后,你可以在 Flutter/Dart 中无缝使用:

api.drawTree(TreeNode(value: "root", ...), Colorful(palette: "viridis"));

译者注:如果你在代码生成时遇到了这个错误 fatal error: 'stdbool.h' file not found 可以尝试阅读:dart-lang/ffigen/issues/257

教程:一个 Flutter 和 Rust 构建的 app

在这个教程中,我们将绘制一个 曼德博集合(英语:Mandelbrot set,或译为曼德布洛特复数集合)(一个著名的分形)。这个图片会由 Rust 算法生成,并在 Flutter 中绘制,Rust 和 Flutter 会通过这个库通信。

(点击查看:Mandelbrot set 是什么:)

曼德博集合是由复数 c 组成的点的集合,对于这些点,均满足函数: f_c(z) = z^{2} + c,不同的参数可能使序列的绝对值逐渐发散到无限大,也可能收敛在有限的区域内。曼德博集合 M 就是使序列不延伸至无限大的所有复数 c 的集合。曼德勃罗集的图像显示了一个精心设计的、无限复杂的边界 随着放大倍数的增加,逐渐显示出越来越细的递归细节。放大倍数时,显示出越来越细的递归细节。

图片来源:维基百科

获取源代码

安装 Flutter (如果你想把程序运行在桌面上,可以选择添加 桌面支持), 安装 Rust, 接着克隆样例代码:

git clone https://github.com/fzyzcjy/flutter_rust_bridge && cd flutter_rust_bridge/frb_example/with_flutter

可选:运行代码生成器

这一步是可选的,因为我已经在 快速入门 时生成了绑定代码,再次运行也不会产生任何改变。

一旦你修改了 api.rs,就需要再次运行代码生成。代码生成器需要的依赖可以在 Installing dependencies 章节查看

运行 app

前言:命令细节

如果你需要了解命令的具体细节,可以查看 CI 工作流flutter_android_test, flutter_ios_test, flutter_windows_test, flutter_macos_testflutter_linux_test 演示了从一台新机器上运行该项目所需要的所有命令

译者注:如果您遇到运行失败,缺少环境等问题,请参考上述 CI 工作流。下面的 cargo ndk 等部分工具,和对应的交叉编译环境都需要单独安装,也可以参照 PART II 的项目设置部分。

Android app

  • ANDROID_NDK=(path to NDK) 添加到 android/gradle.properties
  • 接着运行 cargo ndk -o ../android/app/src/main/jniLibs build.
  • 最后运行 flutter run.

注意: 这个教程 能够帮你在构建 Flutter 程序时自动运行 cargo build.

iOS app

  • 打开 Cargo.toml 并把 cdylib 修改为 staticlib
  • 接着运行 cargo lipo && cp target/universal/debug/libflutter_rust_bridge_example.a ../ios/Runner 编译 Rust 代码,并复制静态连接库。
  • 最后像平时一样运行 flutter run.

注意: 这个教程 能够帮你在构建 Flutter 程序时自动运行 cargo build.

Windows app

假设 Flutter 桌面支持 已经配置完成,你可以直接运行 flutter run。更多细节可以在 #66.

Linux app

和 Windows 一样。如果你是通过 snap 安装的 Flutter, 请阅读一下 #53.

MacOS app

和 Windows 一样。(P.S. 本质上说,cargo-xcode 被用于自动化)

特性

这一章,我们将展示该库的各个特性。请使用左侧/左上的菜单栏切换页面。

序言

这个库是什么?

这个库只是一个代码生成器,帮助你的 Flutter / Dart 调用 Rust 函数。它只是生成了一些模板代码,代替了手工编写。此外,我们还为您提供了详细的教程,让您可以尝试实例,创建新的应用程序,并与现有的应用程序集成。

当然了,你仍然需要对 Flutter/Dart,Rust 以及 ffi. 有一些基本的了解。

完整实例

如果你想看一些例子 我要提前警告你,示例代码真的非常多pure_dart's api.rs 文件里包含了对该库的所有测试。

此外,当你对基本的例子已经熟悉后,可以看一下 pure_dart_multi 项目,这个例子包含多个 API 模块。对于复杂的项目来说是相当有用的。

语言间转换

这一部分我们将展示 Rust 和 Dart 语言间的特性是如何转换。

简单的对应关系

下面是一个简短的概览,显示了代码生成器可以生成的内容(并非完备)。有些行附带超链接,里面有更详细的解释。

RustDart
Vec<u8>, Vec<i8>..Uint8List, Int8List, ..
Vec<T>List<T>
[T; N]List<T>
struct { .. }, struct( .. )class
enum { A, B }enum
enum { A(..) }@freezed class
use ...act normally
Option<T>T?
Box<T>T
commentssame
Result::Err, panicthrow Exception
i8, u8, .., usizeint
f32, f64double
boolbool
StringString
()void

Vec and 数组

Vec<u8>, Vec<i8>, ...

在 Dart 中,当你想表达一个长字节数组,例如大图片或一些二进制 blob,通常会使用 Uint8List 而不是 List<int>,因为前者的性能更好。

flutter_rust_bridge 也为你考虑到了这一点。当你使用到 Vec<u8>(或 Vec<i8>Vec<i32>,等)时,它们会被翻译成 Uint8List 或其它类似的结构。

Vec<T>

当你使用 Vec<T>,并且 T 是 u8i8 等以外的类型时,它会被转换成正常的 List<T>

[T; N]

由于 Dart 没有对静态大小的数组进行特殊处理,所以它也会被转换为 List<T>

例子

pub fn draw_tree(tree: Vec<TreeNode>) -> Vec<u8> { ... }

转换为:

Future<Uint8List> drawTree({required List<TreeNode> tree});

注意:如果你对 Future 感兴趣,请看 这里.

结构体

一般的 Rust 结构体都是支持的,你甚至能够使用递归字段,比如:

pub struct TreeNode {
    pub value: String,
    pub children: Vec<MyTreeNode>,
    pub parent: Box<MyTreeNode>
}

如果一个结构体的字段是一个结构体或者枚举,请为它加上一层 Box, 否则会导致编译时错误。例如 struct A {b: B} 应该使用 struct A {b: Box<B>} 代替。

元组结构体

元组结构体 struct Foo(A, B) 会被翻译为 class Foo { A field0; B field1; }, 因为 Dart 没有匿名字段。

Non-final 字段

在结构体字段上添加 #[(non_final)], Dart 中对应的字段就会是 non-final 的。默认情况下,所有生成的字段都被设为 final,因为 Rust 默认是不可变的。

Dart 元数据注释

你可以使用 frb 宏中的 dart_metadata 参数为 dart 添加元数据注解。

  • 对于那些由 dart 提前引入的注解(例如 @deprecated),只需将注解作为一个 Rust 字面量。

    TODO! For annotations that are prelude by dart (e.g. @deprecated), just put annotation as a Rust literal.

  • 如果你需要使用 import, 请把 import 部分的代码追加到注解字面量之后。目前支持两种 import 格式:

    • import 'somepackage'
    • import 'somepackage' as somename, somename 部分会成为注解的前缀
  • 多个注解间使用 , 分割

具体的例子如下。

freezed Dart classes

如果你想让生成的 Dart 类是 freezed的(类似于 Kotlin 中的 data-classes),只需要在结构体前添加 #[frb(dart_metadata=("freezed"))],它会为你生成需要的东西。

示例

例 1 : 递归字段

pub struct MyTreeNode {
    pub value: Vec<u8>,
    pub children: Vec<MyTreeNode>,
}

转换为:

class MyTreeNode {
  final Uint8List value;
  final List<MyTreeNode> children;
  MyTreeNode({required this.value, required this.children});
}

注意:如果你想了解 Future , 请看这里 async_dart.

例 2 : Metadata

#[frb(dart_metadata=("freezed", "immutable" import "package:meta/meta.dart" as meta))]
pub struct UserId {
    pub value: u32,
}

转换为:

import 'package:meta/meta.dart' as meta;

@freezed
@meta.immutable
class UserId with _$UserId {
  const factory UserId({
    required int value,
  }) = _UserId;
}

枚举

众所周知,Rust 的 enum 功能强大,而且表达力强 - 它允许每一个枚举的变体关联不同的数据。Dart 没有内置这种枚举,但是不用担心 - 我们会使用 Dart 中的 freezed 库自动翻译为等价的结构。第一眼看上去,freezed 的语法可能看上去很奇怪,但是请阅读一下 它的文档, 看看它的强大之处。

示例

pub enum KitchenSink {
    Empty,
    Primitives {
        /// Dart field comment
        int32: i32,
        float64: f64,
        boolean: bool,
    },
    Nested(Box<KitchenSink>),
    Optional(
        /// Comment on anonymous field
        Option<i32>,
        Option<i32>,
    ),
    Buffer(ZeroCopyBuffer<Vec<u8>>),
    Enums(Weekdays),
}

转换为:

@freezed
class KitchenSink with _$KitchenSink {
  /// Comment on variant
  const factory KitchenSink.empty() = Empty;
  const factory KitchenSink.primitives({
    /// Dart field comment
    required int int32,
    required double float64,
    required bool boolean,
  }) = Primitives;
  const factory KitchenSink.nested(
    KitchenSink field0,
  ) = Nested;
  const factory KitchenSink.optional([
    /// Comment on anonymous field
    int? field0,
    int? field1,
  ]) = Optional;
  const factory KitchenSink.buffer(
    Uint8List field0,
  ) = Buffer;
  const factory KitchenSink.enums(
    Weekdays field0,
  ) = Enums;
}

它们由 freezed 中的 all functionalities 驱动。

外部类型

定义在相同 crate 中不同文件里的类型

use 语句可以正常使用。例如:添加 use crate::data::{MyEnum, MyStruct};后,你可以正常的使用 MyEnumMyStruct

示例

use crate::data::{MyEnum, MyStruct};

pub fn use_imported_things(my_struct: MyStruct, my_enum: MyEnum) { ... }

转换为:

// Well it just behaves normally as you expect
Future<void> useImportedThings({required MyStruct myStruct, required MyEnum myEnum});

注意:如果你对 Future 感兴趣,请看 这里.

其他 crate 中的类型

这个功能被称为 "镜像 (mirror)". 简单来说,对于你想使用的外部类型,你需要重新编写一次作为镜像。这个镜像只会在代码生成时负责告知 flutter_rust_bridge 所需的类型信息。下面的例子里有详细的语法。

不用担心它会打破 DRY (Don’t Repeat Yourself) 原则,也不用担心你可能写错一个字段。因为如果镜像和原始类型不完全一致就会导致编译错误。

更多信息: #352

当多个结构体有相同的字段时,你可以使用下面的语法只重写一遍。 #[frb(mirror(FirstStruct, SecondStruct, ThirdStruct))]. (#619)

示例

// Mirroring example:
// The goal of mirroring is to use external objects without needing to convert them with an intermediate type
// In this case, the struct ApplicationSettings is defined in another crate (called external-lib)

// To use an external type with mirroring, it MUST be imported publicly (aka. re-export)
pub use external_lib::{ApplicationEnv, ApplicationMode, ApplicationSettings};

// To mirror an external struct, you need to define a placeholder type with the same definition
#[frb(mirror(ApplicationSettings))]
pub struct _ApplicationSettings {
    pub name: String,
    pub version: String,
    pub mode: ApplicationMode,
    pub env: Box<ApplicationEnv>,
}

// It works with basic enums too
// Enums with struct variants are not yet supported
#[frb(mirror(ApplicationMode))]
pub enum _ApplicationMode {
    Standalone,
    Embedded,
}

#[frb(mirror(ApplicationEnv))]
pub struct _ApplicationEnv {
    pub vars: Vec<String>,
}

// This function can directly return an object of the external type ApplicationSettings because it has a mirror
pub fn get_app_settings() -> ApplicationSettings {
    external_lib::get_app_settings()
}

// Similarly, receiving an object from Dart works. Please note that the mirror definition must match entirely and the original struct must have all its fields public.
pub fn is_app_embedded(app_settings: ApplicationSettings) -> bool {
    // println!("env: {}", app_settings.env.vars[0]);
    match app_settings.mode {
        ApplicationMode::Standalone => false,
        ApplicationMode::Embedded => true,
    }
}

用一个结构体去镜像多个结构体:

// *不* 需要这样做
#[frb(mirror(MessageId))]
pub struct MId(pub [u8; 32]);
#[frb(mirror(BlobId))]
pub struct BId(pub [u8; 32]);
#[frb(mirror(FeedId))]
pub struct FId(pub [u8; 32]);

// 只要编写一次就足够了
#[frb(mirror(MessageId, BlobId, FeedId))]
pub struct Id(pub [u8; 32]);

Option

Dart 对于可能为空的字段有特殊的语法 ?,该库会自动把 Option 翻译为 ?。你可以查看 官方文档 了解更多。

此外,flutter_rust_bridge 也能够处理 Dart 中的 required 关键字:如果一个参数不能为空,它就会被标记为 required。如果它可以为空,那就不需要 required,Dart 默认为 null。

示例

pub struct Element {
    pub tag: Option<String>,
    pub text: Option<String>,
    pub attributes: Option<Vec<Attribute>>,
    pub children: Option<Vec<Element>>,
}

pub fn parse(mode: String, document: Option<String>) -> Option<Element> { ... }

转换为:

Future<Element?> handleOptionalStruct({required String mode, String? document});

class Element {
  final String? tag;
  final String? text;
  final List<Attribute>? attributes;
  final List<Element>? children;
  Element({this.tag, this.text, this.attributes, this.children});
}

注意:如果你对 Future 感兴趣,请看 这里.

方法

支持带有方法的结构体。包括静态方法和非静态方法。

示例

pub struct SumWith { pub x: u32 }

impl SumWith {
    pub fn sum(&self, y: u32) -> u32 { self.x + y }
    pub fn sum_static(x: u32, y: u32) -> u32 { x + y }
}

转换为

class SumWith {
  final FlutterRustBridgeExampleSingleBlockTest bridge;
  final int x;

  SumWith({
    required this.bridge,
    required this.x,
  });

  Future<int> sum({required int y, dynamic hint}) => ..
  static Future<int> sum({required int x, required int y, dynamic hint}) => ..
}

注意:如果你对 Future 感兴趣,请看 这里.

返回值类型

返回值类型可以是 anyhow::Result<YourType>, 或者直接是你的类型 YourType .

示例

pub fn f(a: i32, b: i32) -> i32 { a + b }

pub fn g(a: i32, b: i32) -> anyhow::Result<i32> { Ok(a + b) }

零拷贝

ZeroCopyBuffer<Vec<u8>> (以及与它类似的东西,比如 ZeroCopyBuffer<Vec<i8>>) 可以无需拷贝将数据从 Rust 发送到 Dart. 因此,可以节约拷贝数据的开销,如果你的数据很大(比如一张高分辨率图像),开销可能会很高。

示例

pub fn draw_tree(tree: Vec<TreeNode>) -> ZeroCopyBuffer<Vec<u8>> { ... }

转换为:

Future<Uint8List> drawTree({required List<TreeNode> tree});

生成的 Dart 代码看起来和没有用 ZeroCopyBuffer 的几乎一样。但是它的内部实现已经改变,完全不需要内存拷贝!

注意:如果你对 Future 感兴趣,请看 这里.

Stream 流 / 迭代器

Stream / Iterator

Stream 是什么?简单来说:调用一次,返回多次,类似于 Iterator。

译者注:异步 Iterator。

Flutter 的 Stream 是一个强大的抽象。

假如,你的 Rust 代码在运行一个十分复杂的算法,并且每隔几百毫秒,就能找到一种新的解决方案。每找到一个解,它就可以直接将这部分内容回复给 Flutter,并立即渲染到 UI。用户无需等到算法完全结束就能看到一部分结果。

至于细节方面,一个带有下面函数签名的 Rust 函数 fn f(sink: StreamSink<T>, ..) -> Result<()> 会被翻译为:Stream<T> f(..)

注意:你可以永远持有这个 StreamSink,并且自由使用,甚至在 Rust 函数返回后。下面的 logger 例子证明了这点(create_log_stream 函数几乎是立即返回,但是你可以在一小时之后使用 StreamSink)。

Notice that, you can hold that StreamSink forever, and use it freely even after the Rust function itself returns. The logger example below also demonstrates this (the create_log_stream returns almost immediately, while you can use the StreamSink after, say, an hour).

StreamSink 可以被放在函数的任何地方。例如 fn f(a: i32, b: StreamSink<String>)fn f(a: StreamSink<String>, b: i32) 都是合法的。

示例

下面的例子只是为了加深你对 Stream 特性的理解。

示例:适用于生产环境的 Logger

在我的 app(已经应用在生产环境)里,我使用了下面的策略处理 Rust 日志:使用常规的 Rust 方法记录日志,比如 info!debug! 宏。接着日志会在两个地方得到处理:一方面是通过不同平台特定的方法打印(就像安卓平台的 Logcat 和 IOS 的 NSLog)另一个是通过 Stream 发送到 Dart 端并进一步处理 (例如保存到文件,或者是发送给服务端等)。

完整的代码可以在这里查看#486

示例:简单的 logger

让我们实现了一个简单的日志系统(改编自我在生产环境中使用 flutter_rust_bridge 构建的日志系统),Rust 代码可以向 Dart 端发送日志。

Rust 端 api.rs:

pub struct LogEntry {
    pub time_millis: i64,
    pub level: i32,
    pub tag: String,
    pub msg: String,
}

// 用于展示的简化代码
// 为了成功编译,你需要一个 OnceCell,或者 Mutex,或者 RwLock
// 更多资料:https://github.com/fzyzcjy/flutter_rust_bridge/issues/398
lazy_static! { static ref log_stream_sink: StreamSink<LogEntry>; }

pub fn create_log_stream(s: StreamSink<LogEntry>) {
    stream_sink = s;
}

现在 Rust 编译器应该会向你抱怨 LogEntry 没有实现 IntoDart。这是符合预期的,因为 flutter_rust_bridge 会为你生成相关实现。修复它也很简单,再次运行 flutter_rust_bridge_codegen 即可

生成的 Dart 代码:

Stream<LogEntry> createLogStream();

在 Dart 中使用:

Future<void> setup() async {
    createLogStream().listen((event) {
      print('log from rust: ${event.level} ${event.tag} ${event.msg} ${event.timeMillis}');
    });
}

现在我们能够愉快的在 Rust 中用日志记录任何东西:

log_stream_sink.add(LogEntry { msg: "hello I am a log from Rust", ... })

当然了,你也可以跟着 Rust 中的 log 库去实现一个自己的 logger,只需要在外面包装一层 stream sink,就可以使用标准的 Rust 日志机制,例如 info!。我在我在我的项目中就是这么做的。

示例:简单定时器

Credits: this and #347.

use anyhow::Result;
use std::{thread::sleep, time::Duration};

use flutter_rust_bridge::StreamSink;

const ONE_SECOND: Duration = Duration::from_secs(1);

// can't omit the return type yet, this is a bug
pub fn tick(sink: StreamSink<i32>) -> Result<()> {
    let mut ticks = 0;
    loop {
        sink.add(ticks);
        sleep(ONE_SECOND);
        if ticks == i32::MAX {
            break;
        }
        ticks += 1;
    }
    Ok(())
}

然后在 Dart 中使用:

import 'package:flutter/material.dart';
import 'ffi.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);
  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  late Stream<int> ticks;

  @override
  void initState() {
    super.initState();
    ticks = api.tick();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text("Time since starting Rust stream"),
            StreamBuilder<int>(
              stream: ticks,
              builder: (context, snap) {
                final style = Theme.of(context).textTheme.headline4;
                final error = snap.error;
                if (error != null)
                  return Tooltip(
                      message: error.toString(),
                      child: Text('Error', style: style));

                final data = snap.data;
                if (data != null) return Text('$data second(s)', style: style);

                return const CircularProgressIndicator();
              },
            )
          ],
        ),
      ),
    );
  }
}

Dart 中的异步

默认情况下,生成的代码都是异步的。所以 fn f(..) -> String 将被转换为 Future<String> f(..),多了一个 Future

为什么?Flutter UI 是单线程的。如果你使用同步方法(一些老的 binding 就是这样),在 Rust 代码执行时,UI 渲染将被阻塞。如果你的 Rust 代码进行一次复杂运算需要耗时 100ms,那么你的 UI 就会完全冻结 100ms,用户就不乐意了。

另一方面,有了生成的异步 Dart 绑定,你就可以在 Dart/Flutter 的主隔离区直接调用,Rust 代码不会阻塞 Flutter UI。

async 和 Future 在 Flutter/Dart 中几乎无处不在,并且有着非常好的内部支持,完全不用担心 :D。

注意:一个常见的误解是在 Dart 的其他隔离区调用 Rust 代码(例如 "线程"),而不是主隔离区。这是完全没必要的,只会徒增你的心智负担。如上所述,即使你的 Rust 只用 100ms 就能完成,async 也只需要花费 0.1 ms,并且不会阻塞你的 UI。

Dart 中的同步

如果你真的需要生成同步的 Dart 函数,你可以使用 SyncReturn<Vec<u8>> 作为返回值类型。

我们建议只在非常快的 Rust 函数上这样做,否则 UI 将被阻塞。

因为同步 Dart 函数使用得很少,所以现在仅支持 Vec<u8> 一种类型,其他类型可以通过序列化实现,例如 JSON 和 Protobuf。注意,这种方法极少用到,99% 的 flutter_rust_bridge 都不会用到。如果你需要其他类型支持,请提交一个 issue.

并发

多个 Rust 函数能够同时运行,并且是并发的运行。因为我们默认会使用一个线程池去执行 Rust 代码。但是,你可以完全自定义这里的行为(甚至是完全舍弃线程池)。

示例

看一下下面的例子:

pub fn compute() {
  thread::sleep(Duration::from_millis(1000));
}

下面的 Dart 代码使用了它:

var a = compute();
var b = compute();
var c = compute();
await Future.wait([a, b, c]); // 你可能需要提前学习 Dart 中的 `Future` and `async` 读懂这行代码

总体运行时间是 1s 而不是 3s,因为多个 compute 函数是并发执行的。

Handler

默认情况下,frb 使用 DefaultHandler,你可以实现你自己的 Handler 做你想做的事情。首先,你需要在 Rust 文件中创建一个名为 FLUTTER_RUST_BRIDGE_HANDLER 的变量(可能会用到 lazy_static)。接着,你不一定需要创建一个新的实现了 Handler 的结构体,只需要利用现有的 SimpleHandler,并自定义它的泛型参数,例如Executor

示例

例 1: 除了 Dart 之外,同时向你的后端报告错误

pub struct MyErrorHandler(ReportDartErrorHandler);

impl ErrorHandler for MyErrorHandler {
    fn handle_error(&self, port: i64, error: handler::Error) {
        send_error_to_your_backend(&error);
        self.0.handle_error(port, error)
    }

    ...
}

例 2: 记录函数执行开始和结束的时间

pub struct MyExecutor(ThreadPoolExecutor<MyErrorHandler>);

impl Executor for MyExecutor {
    fn execute<TaskFn, TaskRet>(&self, wrap_info: WrapInfo, task: TaskFn) {
        let debug_name_string = wrap_info.debug_name.to_string();
        self.thread_pool_executor
            .execute(wrap_info, move |task_callback| {
                Self::log_around(&debug_name_string, move || task(task_callback))
            })
    }
}

impl MyExecutor {
    fn log_around<F, R>(debug_name: &str, f: F) -> R where F: FnOnce() -> R {
        let start = Instant::now();
        debug!("(Rust) execute [{}] start", debug_name);
        let ret = f();
        debug!("(Rust) execute [{}] end delta_time={}ms", debug_name, start.elapsed().as_millis());
        ret
    }
}

初始化

如果你需要这个特性,去看一下 Flutter 侧的 FlutterRustBridgeSetupMixin (文档尚未完成,如果你有问题,记得创建一个 issue)。

Rust 中的异步

如果你想在 Rust 中使用 async / await 或者是返回一个 Future,请看 这个文档.

多文件

在大型项目中,把所有的文件都放在一个 api.rs 是不够的,我们通常想把它分离到 api_of_one_module.rs, api_of_another_module.rs 等多个文件里。

你只需要指定所有的 Rust 输入文件以及输出位置即可,这里有一个例子:

flutter_rust_bridge_codegen \
  --rust-input "$REPO_DIR/native/src/api_1.rs" "$REPO_DIR/native/src/api_2.rs" \
  --dart-output "$REPO_DIR/lib/bridge_generated_api_1.dart" "$REPO_DIR/lib/bridge_generated_api_2.dart" \
  --class-name ApiClass1 ApiClass2 \
  --rust-output generated_api_1 generated_api_2

更多信息在 这篇文章

build.rs 中运行

执行代码生成器有两种方法。第一种也是最明显的方法是直接在命令行中执行 flutter_rust_bridge

另一种方法时集成到 build.rs 里。通过这种方法,代码生成器会在编译 Rust 项目时自动触发。更多信息请看 build.rs 文件。

可取消的任务

当 Rust 代码的计算量很大时,你可能会在中途想要取消 (例如用户中途点击了取消),这样就能节约宝贵的计算资源。

安装:目前,该功能已经完成,并且我已经在我自己的 app 中使用了很长时间。(但是我并没有将这个 PR 合并到主分支,因为我需要时间决定如何把这部分代码放到 api.rs 中),因此如果你需要的话,请直接将 这里的代码 (#333) 复制到你的项目里,然后像平常一样使用即可。

对象池

当你的 Rust 侧有一些大型的对象时,你可能不想让它在 Rust 和 Dart 之间反复复制。这是,对象池就派上用场了:你只需要在 Rust 和 Dart 之间传递一个 "对象句柄"(实际上只是几个整数),Rust 会把这个句柄转换为真实的对象。

安装:和 cancelable tasks 一样,请查看文档。

杂项

把生成代码的定义和实现分开

生成的 bridge_generated.dart 文件默认情况下会包含 API 的定义和实现。在生成时,加上 --dart-decl-output 参数,它们就会被分开,并且定义中不会包含任何类似于 dart:ffi 的东西。

更多信息:#298.

从模板创建新项目

在这一章里,我们将从模板创建你自己的项目。看起来有点长,这只是因为我们想讲清楚你可能遇到的每个细节。

注意: 虽然过程很复杂,但是绝大多数原因都不是来源于该库本身,使用原始的 Rust/Flutter FFI 和它一样复杂。换句话说,真正耗时的是搭建 Dart/Flutter + Rust 工具链。

创建一个新项目

首先使用 flutter_rust_bridge_template 的模板创建一个仓库。这个模板在大多数 Flutter 支持的平台应该可以通过 flutter run 直接运行。

安卓设置

对于安卓平台,你需要安装一部分组件:

Rust 编译目标

交叉编译到安卓需要一些额外的组件,你可以通过下面的命令安装:

rustup target add \
    aarch64-linux-android \
    armv7-linux-androideabi \
    x86_64-linux-android \
    i686-linux-android

JDK 8

Android Studio 依赖于 javax 库存在于 Java 运行时,验证安装成功的唯一可靠的方法是安装一个老版本的 Java。在类 Unix 系统上,你可以使用 asdf 或者类似的工具去管理你的 Java 版本。模板在 .tool-versions 文件中定义了一个已知的可以正常运行的 Java 版本。

译者注:asdf 是一个版本管理工具,.tool-versions 是 asdf 的配置文件

Android NDK

安装:

Android Studio > SDK Manager > SDK Tools > uncheck Hide Obsolete Packages > NDK (version 22)

译者注:您也可以不下载 Android Studio,直接通过 sdkmanager 在命令行安装。

Android NDK, 或者说 Native Development Kit, 确保了使用其他语言编写的代码可以通过 JNI 或者说 Java Native Interface 运行在 JVM 上。我们会把 Cargo 创建的动态连接库和项目的打包结果打包在一起。

跟着上面的步骤,你应该会把 NDK 安装到了 $ANDROID_SDK_HOME/ndk 文件夹,ANDROID_SDK_HOME 通常是:

  • Windows: %APPDATA%\Local\Android\sdk
  • MacOS: ~/Library/Android/sdk
  • Linux: 通过设置环境变量 ANDROID_SDK_HOME, 或者 ~/Android/sdk

An issue 在构建 Rust core 库时,只有 NDK 22 和更早版本可以使用。

ANDROID_NDK Gradle 配置

echo "ANDROID_NDK=(path to NDK)" >> ~/.gradle/gradle.properties

下一步,你需要设置 NDK 对 Gradle 可见。根据你的系统有不同的设置方法,但是一般都可以在 ~/.gradle/gradle.properties 里配置以下内容:

ANDROID_NDK=(path to NDK)

或者是修改当前项目文件夹,android 目录里的同名文件。

cargo-ndk

cargo install cargo-ndk --version 2.6.0

cargo-ndk 是一个 cargo 插件,它能够将代码编译到适合的 JNI 而不需要额外的配置。运行上述命令进行安装。cargo-ndk 2.7.0 版本引入了一些变化,破坏了对 NDK 22 版本的支持,所以 目前必须使用 2.6.0。

可选的 NDK 设置

你也可以选择最新版本的 NDK,它比 22 版本要好。但是你需要 Hack 部分代码去解决一个报错:unable to find library -lgcc error.

Android NDK

安装最新版本的 NDK:

Android Studio > SDK Manager > SDK Tools > NDK (Side by side)

cargo-ndk

如果你使用的是版本高于 22 的 NDK,那么你需要 cargo-ndk 2.7.0 或者更新版本。

cargo install cargo-ndk --version ^2.7.0

A workaround may be under development in the cargo-ndk project. Until it is finished, you need to manually create four text files to redirect calls from libgcc to libunwind (reference):

  1. Find out all the 4 folders containing file libunwind.a.

    • On Windows, it is similar to:

      C:\Users\Administrator\AppData\Local\Android\Sdk\ndk\24.0.8215888\toolchains\llvm\prebuilt\windows-x86_64\lib64\clang\14.0.1\lib\linux\x86_64\
      
    • On macOS Monterey, it is similar to:

      ~/Library/Android/sdk/ndk/24.0.8215888/toolchains/llvm/prebuilt/darwin-x86_64/lib64/clang/14.0.1/lib/linux/x86_64/
      

    The three other folders end with aarch64, arm, i386 instead of x86_64.

  2. Create 4 text files named libgcc.a in the four folders mentioned above with this contents

    INPUT(-lunwind)
    

iOS 设置

iOS 需要一些额外的交叉编译目标:

# 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
# 32 bit targets (你应该不需要这个):
rustup target add armv7-apple-ios i386-apple-ios

Web 设置

目前为止,Web 支持尚未完成 fzyzcjy/flutter_rust_bridge#315

考虑一下评论或者建议让我们了解您对该功能的需求!

Windows 和 Linux

Windows 和 Linux 共享同一套编译系统(CMake),从零开始设置这两个平台也非常简单。模板该模板使用到了 Corrosion 库来加快这一过程,安装 Corrosion 需要先克隆代码,并初始化。跟着 本指南 学习如何将 Corrosion 安装到你的系统上。安装完成后,请继续修改 rust.cmake

-# find_package(Corrosion REQUIRED)
+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)

Troubleshooting: CMake on Linux

Corrosion 对 CMake 的最低版本需求是 3.12,这不是 CMakeLists.txt 的默认版本。所以你需要手动修改 linux/CMakeLists.txt:

-cmake_minimum_required(VERSION 3.10)
+cmake_minimum_required(VERSION 3.12)

但是它带来了另一个问题,它不允许你通过 Flutter SDK 通过 Snap 构建,因为他的构建过程和 CMake 3.10 绑定。可能的话,建议使用命令行手动安装 Flutter。canonical/flutter-snap#61 地。

一个变通的方法是忽略 rust.cmake 并手动配置 CMake 来构建和捆绑 Rust 库。例如 本评论 是在 ARM Linux 上使用 Flutter。

其他平台

对于所有剩下的平台,基本没有需要设置的步骤。除了在下面列表中标注的。Desktop support for Flutter。如果你要查看当前进展,运行 flutter -v 它会显示你的工具链的状态和任何可操作的步骤。

模板之旅

success-screen

祝贺!🎉 现在你应该有一个可以正常运行的 Flutter 应用了,并且配备了 一个 Rust 运行时组件。本节旨在温和地介绍 Rust 与现有 Flutter 工具链集成的细节。不用担心,你可以直接跳到生成代码一节来学习如何编写新的代码。或者直接查看 集成到现有项目 一节,将 Rust 添加到现有的 Flutter 项目

native/src/api.rs

这是库默认入口文件,只有定义在这里的函数才能进行代码生成。函数可以引用定义在其他文件中的类型,作为参数或者返回值类型。但是这些类型必须通过 pub use 导入,以便它们在 native/src/bridge_generated.rs 中可见。

只有定义在当前 crate 的类型有资格进行代码生成。

除此之外,结构和枚举的字段也需要符合上述条件。

要查看目前符合条件的函数和类型,请看 示例文件.

android/app/build.gradle

该文件是 Flutter 构建安卓 app 所需默认文件的一部分。

模板中注入了额外的钩子,以便在调用 flutter run 时运行 cargo-ndk。这个方法在以下文章中有更详细的解释 Hooking onto tasks

native/native.xcodeproj

这是由 cargo-xcode 生成的 Xcode 项目文件夹,用于生成 Rust 代码库。

iOS 和 MacOS 的根项目会把这个文件夹作为 子项目 导入,并在 build 时依赖它。

为你的目标设备配置合适的 cate-type 非常重要。确保你的 Cargo.toml 中有这几行:

[lib]
crate-type = ["lib", "cdylib", "staticlib"]

一些说明:

  • lib 对于非库项目时必须的,例如 tests 和 benchmarks
  • staticlib 对 iOS 是必须的
  • cdylib 是用于其他平台

justfile

这个文件定义了 just 命令的 "配方",它和 make 与 MakeFile 类似。just 使用 Rust 构建,并在传统的 Makefile 语法基础上做了改进,更好地支持条件语句、参数、跨平台兼容性等。

在某些设置中,通过 brew install llvm 安装不会使 LLVM 库对其他可执行文件可见,这会给 ffigen 带来了问题。 flutter_rust_bridge_codegen 使用它作为 C-to-Dart 的代码生成器。

运行 just 默认会运行 genlint 任务。

just gen

生成 Rust 绑定并把它们放到正确的文件夹。Generating new code 这部分会详细介绍如何修改任务脚本以执行附加任务。

just lint

运行 Dart 和 Rust 默认的 linter。

just clean

运行 Flutter 和 Rust 默认的 clean 命令。通常在调试和 build 相关的问题时有用。

rust.cmake

windowslinux 中有两个相同的文件,名为rust.cmake。 这些文件包含在现有的CMakeLists.txt中,Flutter 使用它来编译对应平台的应用程序。

代码生成

这一部分需要您已经跟随 创建一个新项目 章节,并成功运行 flutter run 到您的设备上。

到目前为止,程序运行的所有必要代码都已经提供,没有额外的东西需要安装。现在我们将注意力集中在如何编写 Rust 代码,生成必要的胶水代码并在 Dart 中使用。

安装代码生成器

更多信息在 安装依赖 部分。

添加新代码

我们想要跨平台,并不关心代码到底是在 Inter 还是 Apple Silicon 上运行。但是我们需要保留平台信息,以便底层代码能够作出对应的响应。我们可以把 MacAppleMacIntel 归为一个 MacOs(String),里面包含了当前 CPU 架构。现在更新 native/src/api.rs:

 pub enum Platform {
     ..
-    MacIntel,
-    MacApple,
+    MacOs(String),
     ..
 }

接着运行 just,看看生成的绑定代码会如何变化。

Troubleshooting: "Please supply one or more path/to/llvm..."

对 LLVM 安装的检测在不同平台上并不可靠。特别是对于 MacOS 和 x86-64 和 arm64 的二进制文件,你可能需要修改 justfile 以明确指向它的位置:

llvm_path := if os() == "macos" {
    "--llvm-path /opt/homebrew/opt/llvm"
} else {
    ""
}

使用 build_runner

检查一下你的 lib/bridge_generated.dart,你会发现 Platform 的定义变为了:

@freezed
class Platform with _$Platform {
    const factory Platform.unknown() = Unknown;
    const factory Platform.android() = Android;
    const factory Platforn.ios() = Ios;
    const factory Platform.windows() = Windows;
    const factory Platform.unix() = Unix;
    const factory Platform.macOs(
        String field0,
    ) = MacOs;
    const factory Platform.wasm() = Wasm;
}

它不再是一个普通的枚举,而是带着一个具有变体的枚举类!现在代码不能通过编译,因为我们还缺少 freezed 库。freezed 库也是一个代码生成库,和我们目前为止遇到的有些相似,但是它生成的更多是 Dart 代码。所有的这些库都是在调用 build_runner 时进行代码生成的,即执行 flutter pub run build_runner build 时。

不管怎么说,为了使这段代码通过编译,我们需要做一些修改:

  • 执行下面的代码,添加最新的 freezed 依赖:
flutter pub add -d build_runner
flutter pub add -d freezed
flutter pub add freezed_annotation
  • 更新 justfile 文件,在 Rust 代码生成后运行 build_runner:
 gen:
     ..
     # Uncomment this line to invoke build_runner as well
-    # flutter pub run build_runner build
+    flutter pub run build_runner build

现在调用 just 会同时生成 Rust 绑定和 Dart 代码。

收尾

有了对 "平台" 的新定义,我们可以重写以前的代码去使用它!下面是一个例子,展示了 freezed 枚举的使用技巧。

lib/main.dart 里:

- final text = const {
-   Platform.Android: 'Android',
-   Platform.Ios: 'iOS',
-   Platform.MacApple: 'MacOS with Apple Silicon',
-   Platform.MacIntel: 'MacOS',
-   Platform.Windows: 'Windows',
-   Platform.Unix: 'Unix',
-   Platform.Wasm: 'the Web',
- }[platform] ??
- 'Unknown OS';
+ final text = platform.when(
+   android: () => 'Android',
+   ios: () => 'iOS',
+   macOs: (arch) => 'MacOS on $arch',
+   windows: () => 'Windows',
+   unix: () => 'Unix',
+   wasm: () => 'the Web',
+ );

native/src/api.rs 里:

     } else if cfg!(target_os = "ios") {
         Platform::Ios
     } else if cfg!(all(target_os = "macos", target_arch = "aarch64")) {
-        Platform::MacApple
+        Platform::MacOs("Apple Silicon".into())
     } else if cfg!(target_os = "macos") {
-        Platform::MacIntel
+        Platform::MacOs("Intel".into())
     } else if cfg!(target_family = "wasm") {
         Platform::Wasm
     } else if cfg!(unix) {

当你运行 flutter run 后,你应该能看到: macos-intel

集成到现有项目

这一部分是中级教程,介绍了如何将 Rust 与现有的 Flutter 项目集成。如果您是 Rust 初学者,或者只是在配置开发环境,我建议你先去看 the template tour 章节,学习 flutter run 背后的部分。

在进入教程之前,为了使集成更简单,请更新你的 Flutter SDK,如果可以的话,也请您重新构建项目。

注意: 虽然过程很复杂,但是绝大多数原因都不是来源于该库本身,使用原始的 Rust/Flutter FFI 和它一样复杂。换句话说,真正耗时的是搭建 Dart / Flutter + Rust 工具链。

创建一个新的 crate

首先,你需要在项目文件夹里创建一个新的 Rust crate,运行 cargo new --lib。建议将 crate 的根目录设为和其他项目同等级别,这样有助于简化配置过程。

├── android
├── ios
├── lib
├── linux
├── macos
├── $crate
│   ├── Cargo.toml
│   └── src
├── test
├── web
└── windows

这部分中我们会把你的 crate 称为 $crate。除非有其他说明,crate 文件夹和 crate 名称意义相同。

接着,在你的 Cargo.toml 加上这两行:

+[lib]
+crate-type = ["staticlib", "cdylib"]

这两个行配置会让你的 crate 在 MacOS 和 iOS 上构建为一个静态库。在其他平台上则是动态库。根据你的需要进行配置。如果你 需要编写单元测试或基准测试,也可以把 "rlib" 加进去。

安装依赖

下一步,我们需要安装一些构建时和运行时依赖。

构建依赖

这些依赖只在 build 时需要:

安装这些依赖的简单方法:

  • dart 项目

    cargo install flutter_rust_bridge_codegen
    dart pub add --dev ffigen && dart pub add ffi
    # if building for iOS or MacOS
    cargo install cargo-xcode
    
  • flutter 项目

    cargo install flutter_rust_bridge_codegen
    flutter pub add --dev ffigen && flutter pub add ffi
    # if building for iOS or MacOS
    cargo install cargo-xcode
    

另外,这些依赖也可能已经提供了预构建的二进制版本。请自行到包管理器中查找。

Dart 依赖

在 Dart 这里,flutter_rust_bridgeflutter_rust_bridge_codegen 必需的运行时组件。如果你打算在 Dart 中使用 Rust 的 enum struct,你需要这些依赖:

  • build_runner (dev)
  • freezed (dev)
  • freezed_annotation

它们的是使用方法在 Using build_runner

flutter pub add flutter_rust_bridge
# if using Dart codegen
flutter pub add -d build_runner
flutter pub add -d freezed
flutter pub add freezed_annotation

Rust 依赖

和 Dart 类似,Rust 需要 flutter_rust_bridge 最为运行时依赖。

Cargo.toml 里添加:

+[dependencies]
+flutter_rust_bridge = "1"

集成到安卓

设置过程与 Android setup 相同。 所以请继续按照那里的步骤进行。当你完成后,我们将继续修改现有的工具链以适应 Rust。

设置 Cargo 与 Gradle 一起运行的方法有很多,本指南将介绍两种主要方式:任务钩子(hooking onto tasks),以及与 CMake 集成。

Hooking onto tasks

这部分与模板的使用方法相同,也是比较简单的方法。如果你还没有安装 cargo-ndk,请继续安装。

cargo install cargo-ndk

接着,在 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 "../../$crate"
        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
        }
    }
}

注意 ANDROID_NDK 变量,这是一个 Gradle 属性,它指向你安装的 Android NDK 目录。你可以硬编码这个值,但最可靠的方法是写入到 ~/.gradle/gradle.properties

ANDROID_NDK=(path to NDK)

CMake 和 Gradle

如果你之前看过 windowslinux 文件夹,你会看到 一个名为 CMakeLists.txt 的文件。这个文件是 CMake 工具链的定义文件,Flutter 使用它来构建 Windows 和 Linux 应用程序。你也可以在 Gradle 上使用 这个策略,但这种设置超出了本指南的范围,是留给高级人员的。

请参考官方 Android 文档中的 Add C and C++ code to your project,围绕 C 语言的特定部分进行修改,并使用 Corrosion 这样的工具来集成到 Cargo。这种设置的好处是,你可以重复使用 C 语言工具,并且受益于各种现有成熟技术,如构建缓存。

与 iOS/MacOS 集成

Credit to brotskydotcom/rust-on-ios for the inspiration of this method.

为 iOS 和 MacOS 设置 flutter run 比其他平台稍微复杂一些。由于其对 Xcode 用户界面的依赖。本指南假设您正在运行 一个相对较新的 Xcode 版本,在写这篇文章的时候是 Xcode 13。其他版本可能会有一些小的差异,但整体过程应该是相同的。

创建 Rust 项目

首先,请按照 Usagecargo-xcode 部分的说明进行操作。下面的内容就是从那里引用的,但请注意,它可能已经过时了。


确保你的 $crate/Cargo.toml 里有下面几行代码:

[lib]
crate-type = ["lib", "staticlib", "cdylib"]

一些说明

  • lib 对于非库项目时必须的,例如 tests 和 benchmarks
  • staticlib 对 iOS 是必须的
  • cdylib 是用于其他平台

请按照您的需求进行配置。接着在 $crate 下运行:

cargo xcode

这行命令会生成一个 $crate/$crate.xcodeproj,可以导入到其他 Xcode 项目。你只需要为每个 crate 做一次这样的工作。先不要打开这个项目 我们需要先通过父项目对其进行配置。

链接该项目

在 Xcode 中打开 ios/Runner.xcodeproj, 接着把 $crate/$crate.xcodeproj 添加为子项目。结果大致是这样:

proj-tree

点击 Runner 根项目,接着找到 Build Phases. 首先,展开 Dependencies , 接着对于 IOS 请添加 $crate-staticlib ,对于 MacOS 请添加 $crate-cdylib

dep-phase

接着,展开 Link Binary With Libraries, 为 IOS 添加 lib$crate_static.a,或者对于 MacOS 添加 $crate.dylib.

link-phase

生成绑定

现在我们已经完成了大部分的工作,让我们来编译我们的 Rust 程序。如果你刚才创建了你的 crate,请继续 在 $crate/src/api.rs 处添加一个新文件,并将其内容替换为下面的代码片段或其他内容。

pub fn greet() -> String {
    "Hello from Rust! 🦀".into()
}

接着在 $crate/src/lib.rs 添加:

+mod api;

运行代码生成

在我们编译之前,我们需要先生成绑定。从项目根目录运行这些命令:

flutter_rust_bridge_codegen \
    -r $crate/src/api.rs \
    -d lib/bridge_generated.dart \
    -c ios/Runner/bridge_generated.h \
    -c macos/Runner/bridge_generated.h   # if building for MacOS

注意: 每次修改 Rust 代码后都会使用到这些命令。

运行这个命令可以得到由 Rust 库导出的函数和类型的 C 头文件。我们需要确保它来保持符号不被剥离。

Using dummy headers

flutter_rust_bridge_codegen 会创建一个 C 头文件,里面列出了 Rust 库导出的所有符号,我们需要使用它确保 Xcode 不会将符号去除。

在项目中添加 ios/Runner/bridge_generated.h (或者 macos/Runner/bridge_generated.h),你可以把文件直接拖到项目文件夹中或者点击菜单上的 Add Files to "Runner"... .

如果它还没有出现,请切换到 Build Phases 标签页,把 bridge_generated.h 文件拖到 Copy Bundle Resources

iOS

接下来,在 ios/Runner/Runner-Bridging-Header.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)
     ..
 }

这里调用 dummy_method_to_enforce_bundling() 并打印返回值的步骤非常重要(类似于上面的例子),否则最终的符号还是可能被去除。

MacOS

Flutter 在 MacOS 上默认不使用符号,我们需要添加我们自己的。在 Build Settings 标签页中,把 Objective-C Bridging Header 设置为 Runner/bridge_generated.h

最后,在 macos/Runner/AppDelegate.swift 文件的某个地方使用一下 dummy_method_to_enforce_bundling , 只要 Xcode 不把它当作 dead code 就行。

与 Windows 和 Linux 集成

本指南将 Windows 和 Linux 桌面应用程序的说明放在一起,因为它们使用相同的构建系统。

和其他平台一样:我们将使用脚本整合到现有的项目。现在我们将借用下模板,把 rust.cmake 下载到你的 "windows" 和 "linux" 文件夹里。请注意,CMake 会拒绝使用位于其工作目录之外的文件,所以在这两个文件夹之间会有一些重复的文件。

接下来,在你的CMakeLists.txt文件中添加这一行:

 # 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

Linux

在 Linux 上,你需要将 CMake 的最低版本升到 3.12,这是 Corrosion 的要求,rust.cmake 依赖 Corrosion。请修改 linux/CMakeLists.txt 的这一行:

-cmake_minimum_required(VERSION 3.10)
+cmake_minimum_required(VERSION 3.12)

可选:你可以将 Corrosion 安装到系统上。请查看 Linux troubleshooting notes.

与 Web 集成

截至发稿时,Web 支持正在进行中,并在fzyzcjy/flutter_rust_bridge#315持续跟踪。 请留下评论和建议,让我们了解您对这一功能的需求!

使用动态连接库

如果一切顺利,运行 flutter run 就会自动构建你的 Rust 库,Flutter 二进制文件,并将二者连接起来。现在唯一要做的事情就是使用它!

这个文件 下载到 lib/ffi.dart,接着修改下面几行:

 // Re-export the bridge so it is only necessary to import this file.
 export 'bridge_generated.dart';
 import 'dart:io' as io;

-const _base = 'native';
+const _base = '$crate';

 // On MacOS, the dynamic library is not bundled with the binary,
 // but rather directly **linked** against the binary.
 final _dylib = io.Platform.isWindows ? '$_base.dll' : 'lib$_base.so';

收尾工作

恭喜!您已经成功地将 Rust 组件通过 flutter_rust_bridge 添加到您的 Flutter 程序中,并配置了 flutter run 来构建您的 Rust 库并将其链接到程序中。

作为提醒,每次 Rust 代码改变时,以及在你运行flutter run之前,你都需要运行这些命令。

flutter_rust_bridge_codegen \
    -r $crate/src/api.rs \
    -d lib/bridge_generated.dart \
    -c ios/Runner/bridge_generated.h \
    -c macos/Runner/bridge_generated.h   # if building for MacOS
# if using Dart codegen
flutter pub run build_runner build

重命名 Rust bridge 模块

如果你想用 flutter_rust_bridge_codegen--rust-output 参数,不要忘记更新 $crate/src/lib.rs 里引用的模块名

flutter_rust_bridge_codegen \
    ..
    --rust-output $crate/src/my_bridge.rs

then you need to modify this in lib.rs:

-mod bridge_generated;
+mod my_bridge;

总览

Prelude

首先,欢迎并感谢您做出的贡献!

如果你想成为一个贡献者,请提交一个 Pull Request。如果你需要一些改进的点子,去看一下仓库的 Issues section

需要一个 PR 模板?请看一下 PR template.

总体设计

如果你想从高层了解这个库的实现原理,这里是整体设计:link

设计总览

这篇文档正在编写中。Tracking issue: https://github.com/fzyzcjy/flutter_rust_bridge/issues/593

文件结构

  • frb_codegen: 代码生成器。它接收 api.rs 作为输入,并输出 Rust and Dart 代码文件。
  • frb_example: 例子。
    • pure_dart: 不只是一个例子,更重要的是作为端到端的测试。
    • with_flutter: 集成到 Flutter 的例子。
    • pure_dart_multi: 展示多文件的使用。
  • frb_dart: 对 Dart 库的支持 - 需要由用户引入。
  • frb_rust: 对 Rust 库的支持 - 需要由用户引入。
  • frb_macros: frb_rust 独立的一部分。 由于 proc macro 的限制,所以它是一个独立的 crate。
  • book: 文档。
  • .github: GitHub 相关。
    • workflows/ci.yaml: CI 工作流的定义。

代码生成结构

流程如下:

----------    src/parser    ----------    src/generator     ---------------
| api.rs | ---------------> | src/ir | -------------------> | Rust & Dart |
----------                  ----------                      ---------------
  • 输入 (即图中的 api.rs), 是由用户提供的手工编写的 Rust 代码。
  • 解析器 (src/parser) 将输入的代码 (其实是 syn 树) 转换为 IR.
  • IR (src/ir), 或者说 internal representation, 是一种结构,用来表示我们感兴趣的代码的信息。
  • 生成器 (src/generator) 将 IR 转换为最终的输出。更具体一点就是 src/generator/dart 生成 Dart 代码, src/generator/rust 生成 Rust 代码,src/generator/c 生成 (部分) C 代码。
  • 最终的输出 (图中的 Rust & Dart) 被写入到对应的文件。

数据流

建议读者配合着 IDE 的代码跳转功能一同查看

让我们看一下当调用一个函数时发生了什么。

假设用户调用了一个(生成的)名为 func 的 Dart 函数 func({required String str})。下面是详细的调用过程:

  1. 生成的 Dart 函数,func({required String str}), 首先会将参数类型进行转换,将 "Dart api data" (即用户提供的数据) 转换为 "Dart wire data" (即真正在 Dart 和 Rust 间传递的数据)。再具体一点,它会调用 _api2wire_String(str) 并得到一个指针 ffi.Pointer<wire_uint_8_list> (因为 String 类型在底层使用 pub struct wire_uint_8_list { ptr: *mut u8, len: i32 }) 表示。
  2. 接着可以用拿到的底层数据结构 wire_uint_8_list 调用 Dart 版本的 wire_func。在此之前,我们已经使用代码生成器生成了 Rust 的 wire_func 函数,并使用 cbindgen 生成对应的 C 函数,使用 ffigen 得到对应的 Dart 函数。在这里,我们调用 Dart 版本的 wire_func。注意,因为我们使用的是和 C 语言兼容的函数,所以我们只能传递类似于指针的低级数据类型,而不是高级的安全的数据类型。
  3. 当 Rust 版的 wire_func 被调用时,也会对参数类型进行转换。即使用 .wire2api() 将 "Rust wire data" (wire_uint_8_list,在 Dart 和 Rust 间传递的数据) 转换为 "Rust api data" (在这里就是 String, 用户真正使用的数据).
  4. 携带着转换后的 "Rust api data" 调用 FLUTTER_RUST_BRIDGE_HANDLER。handler 是用户自定义的,所以用户可以提供他们自己的实现,而不是使用默认的线程池等。默认情况下,我们的 Handler 使用一个线程池,并在里面调用 api.rs 中定义的由用户编写的 Rust 函数
  5. 调用用户编写的 fn func(str: String) -> String { ... },并得到返回值。
  6. 返回值类型是一个 String,它会被传递到 Dart 侧。这是通过 Dart 提供的 API 实现的。Dart_PostCObject,这个项目允许我们提供 C 的结构体,并自动转换到 Dart 的数据。我们使用了一个 Rust 安全的 wrapper allo-isolate 去通信,因为它允许 Dart 代码可以是异步的而不是同步。
  7. 现在让我们回到 Dart 一侧,你应该会接收到一些 Dart 对象(其实就是 "Dart wire data")。接着我们会使用一些类似于 _wire2api_SomeType 的函数将它们转换为最终的 "Dart api data"。注意,这里提到的 "wire2api" 只定义在 Dart 一侧,它的作用就是将 "Dart wire data" 转换为 "Dart api data",和之前定义在 Rust 中的不一样。举个例子,由于 Dart_PostCObject 并没有提供构建任意的结构体(类)的方法,我们必须将 Rust 结构体中的所有字段作为一个列表传递,并使用 wire2api 转换为对应的 Dart 类。
  8. 最终的结果会以 Dart 函数的返回值出现,即用户刚开始调用的 func 函数。到此为止,函数调用的整个过程就结束了!

内存安全

如何保障内存安全?这个具体需要具体问题具体分析。例如,假设我们想看一个 String 是如何从 Dart 传递给 Rust 的。那么我们需要关注的是 Dart 的 _api2wire_String 和 Rust 的 .wire2api()

实际上 String 是通过委派给 Vec<u8> 生成的,所以我们需要检查和 String 和 Vec<u8> 相关 的代码。经过一系列跳转,你会看到下面的代码:

ffi.Pointer<wire_uint_8_list> _api2wire_String(String raw) {
  return _api2wire_uint_8_list(utf8.encoder.convert(raw));
}

ffi.Pointer<wire_uint_8_list> _api2wire_uint_8_list(Uint8List raw) {
  final ans = inner.new_uint_8_list_0(raw.length);
  ans.ref.ptr.asTypedList(raw.length).setAll(0, raw);
  return ans;
}

以及

impl Wire2Api<Vec<u8>> for *mut wire_uint_8_list {
    fn wire2api(self) -> Vec<u8> {
        unsafe {
            let wrap = support::box_from_leak_ptr(self);
            support::vec_from_leak_ptr(wrap.ptr, wrap.len)
        }
    }
}

impl Wire2Api<String> for *mut wire_uint_8_list {
    fn wire2api(self) -> String {
        let vec: Vec<u8> = self.wire2api();
        String::from_utf8_lossy(&vec).into_owned()
    }
}

pub struct wire_uint_8_list {
    ptr: *mut u8,
    len: i32,
}

换句话说,String(或者 Vec<u8>)被转换为了一个原始结构体,它带有指针和长度字段。对内存的操作非常小心,因此不会造成泄漏或重复释放。

我们同时还使用了 Valgrind 进行检查,我本人已经在生产环境中使用它,并没有发现任何问题,所以不用担心内存问题。

想了解更多?请告诉我

你还想了解哪些方面?请在 Github 上创建一个 Issue,我会告诉你更多 :)

附录

注意:一些文档可能过时了。请在 ci.yaml, 主文档,justfile 等等查看最新版本,etc to see an up-to-date version. 本附录将进行大修。

发行新版本

通常是由库的拥有者做的 (@fzyzcjy),所以你基本不需要做这一步。如果你需要发布一个新版本,至少需要进行以下步骤。Bump 一些版本,在 changelog 中改变版本号,并使用 cargo check来自动更新实例的依赖版本。

just release

运行代码生成器的简单代码

只需要从 CI codegen.yml 复制即可。

(cd frb_codegen && cargo run --package flutter_rust_bridge_codegen --bin flutter_rust_bridge_codegen -- --rust-input ../frb_example/pure_dart/rust/src/api.rs --dart-output ../frb_example/pure_dart/dart/lib/bridge_generated.dart --dart-format-line-length 120 && cargo run --package flutter_rust_bridge_codegen --bin flutter_rust_bridge_codegen -- --rust-input ../frb_example/with_flutter/rust/src/api.rs --dart-output ../frb_example/with_flutter/lib/bridge_generated.dart --c-output ../frb_example/with_flutter/ios/Runner/bridge_generated.h --dart-format-line-length 120)

格式化 和 lint

(cd frb_codegen && cargo fmt --all); (cd frb_rust && cargo fmt --all); (cd frb_macros && cargo fmt --all); (cd frb_example/pure_dart/rust && cargo fmt --all); (cd frb_example/with_flutter/rust && cargo fmt --all);
(cd frb_codegen && cargo clippy); (cd frb_rust && cargo clippy); (cd frb_macros && cargo clippy); (cd frb_example/pure_dart/rust && cargo clippy); (cd frb_example/with_flutter/rust && cargo clippy);
(cd frb_dart && dart format . --line-length 80); (cd frb_example/pure_dart/dart && dart format . --line-length 120); (cd frb_example/with_flutter && dart format . --line-length 120);
(cd frb_dart && dart analyze --fatal-infos); (cd frb_example/pure_dart/dart && dart analyze --fatal-infos); (cd frb_example/with_flutter && dart analyze --fatal-infos);

升级依赖的版本

flutter pub upgrade flutter_rust_bridge
cargo update -p flutter_rust_bridge

教程:Pure Dart

注意: 如果你想了解每一条具体命令, CI workflow 中的 valgrind_test 部分也是有用的。

和之前的教程不一样,这个教程里 Rust 只和 Dart 本身集成,没有 Flutter.

拿到示例代码

下载 Dart, 下载 Rust,并熟悉一下。接着运行 git clone https://github.com/fzyzcjy/flutter_rust_bridge, 例子在 frb_example/pure_dart.

(可选) 手动运行代码生成

注意:代码会在运行完 cargo build 后,通过 build.rs 里的构建脚本自动生成。所以这一步是可选的。即使你再次运行,代码也不会发生什么改变。

安装代码生成器:cargo install flutter_rust_bridge_codegen.

运行: flutter_rust_bridge_codegen --rust-input frb_example/pure_dart/rust/src/api.rs --dart-output frb_example/pure_dart/dart/lib/bridge_generated.dart (你可以将 CI workflow 最为查阅手册.) (对 Windows 用户,你可能需要在路径里用 \\ 代替 /.)

Run "Dart+Rust" app

你可以将 frb_example/pure_dart/dart/lib/main.dart 作为一个普通的 Dart 程序运行,唯一不同的是,你需要提供 Rust 的动态连接库 (简单起见,这里我只演示动态链接库的方法,但你当然可以使用其他方法)详细步骤如下。

frb_example/pure_dart/rust 目录下运行 cargo build 将 Rust 代码编译为 .so 文件。接着执行 dart frb_example/pure_dart/dart/lib/main.dart frb_example/pure_dart/rust/target/debug/libflutter_rust_bridge_example.so 去运行 Dart 程序。

(如果你的运行出现问题,请看 "Troubleshooting" 部分) (如果你的平台是 MacOS, Rust 应该会生成.dylib, 请把命令里的 ...dylib 替换为 ...so)

P.S. 这个例子里并没有 UI 或其他功能,你只能看到一些测试通过的信息。

安全顾虑

该库使用 CI 工作流,并会在设置过程中自动运行 Valgrind ,当 Dart 程序使用该库调用 Rust 程序时,Valgrind 能及时发现内存安全问题 ( 注意:即时你只运行一个简单的 hello-world Dart 程序,Valgrind 也会检测到几百个错误。请查看 this Dart lang issue 了解更多。因此,我检查了 Valgrind 报告的所有 "definitely lost", 并且手动在库里搜索 - 如果报告的所有错误都和该库无关,那么它就是安全的)

除此之外,与 Flutter 的集成也是通过 CI 完成。确保了使用该库的 Flutter 应用不会产生问题。

大多数代码都是 safe Rust。 unsafe 代码主要来自 support::box_from_leak_ptrsupport::vec_from_leak_ptr. 他们被用于处理指针和数组,我会遵循高票数的答案和官方文档编写相关代码。

我在我的个人 Flutter 项目 (yplusplus, or why++) 里非常频繁的使用到了该库。那些 app 已经用于生产环境,并且运行非常稳定,如果我自己观察到了任何问题,我会修复相关 bug。

CI 同时会运行 run_codegen 工作流,确保生成的代码可以通过编译。最后,CI 还会运行代码格式化和 linter(fmt, clippy, dart analyze, dart format), linter 也能捕获到一些常见错误。

Troubleshooting

The generated store_dart_post_cobject() has the wrong signature / 'stdarg.h' file not found in Linux / stdbool.h / ...

Try to run code generator with working directory at /, or set the environment variable:

export CPATH="$(clang -v 2>&1 | grep "Selected GCC installation" | rev | cut -d' ' -f1 | rev)/include"

as described in ffigen #257, or add include path as is described in #108. This is a problem with Rust's builtin Command. See also: #472 & #494.

Issue with store_dart_post_cobject

If calling rust function gives the error below, please consider running cargo build again. This can happen when the generated rs file is not included when building is being done.

[ERROR:flutter/lib/ui/ui_dart_state.cc(209)] Unhandled Exception: Invalid argument(s): Failed to lookup symbol 'store_dart_post_cobject': target/debug/libadder.so: undefined symbol: store_dart_post_cobject

Error running cargo ndk: ld: error: unable to find library -lgcc

Downgrade Android NDK to version 22. This is an ongoing issue with cargo-ndk, a library unrelated to flutter_rust_bridge but solely used to build the examples, when using Android NDK version 23. (See #149)

Fail to run flutter_rust_bridge_codegen on MacOS, "Please supply one or more path/to/llvm..."

If you are running macOS, you will need to specify a path to your llvm:

flutter_rust_bridge_codegen --rust-input path/to/your/api.rs --dart-output path/to/file/being/bridge_generated.dart --llvm-path /usr/local/homebrew/opt/llvm/

You can install llvm using brew install llvm and it will be installed at /usr/local/homebrew/opt/llvm/ by default.

Freezed file is sometimes not generated when it should be

If your .freezed.dart or .g.dart seems outdated, ensure you have run the build_runner.

Related: https://github.com/fzyzcjy/flutter_rust_bridge/issues/330

Can't create typedef from non-function type.

Ensure min sdk version of Flutter pubspec.yaml is at least 2.13.0 to let ffigen happy.

https://github.com/fzyzcjy/flutter_rust_bridge/issues/334

Generated code is so long

Indeed all generated code are necessary (if you find something that can be simplified, file an issue). Moreover, other code generation tools also generate long code - for example, when using Google protobuf, a very popular serialization library, I see >10k lines of Java code generated for a quite simple source proto file.

为什么需要 Dart 2.14.0

这个库并不需要 Dart SDK >=2.14.0, 但是最新的 ffigen 工具需要。所以才会在 pubspec.yamlenvironment 中限制 sdk: ">=2.14.0 <3.0.0". 如果你需要摆脱这个限制,请考虑使用更老版本的 ffigen 工具。

其它问题?

不要犹豫 open an issue! 我通常会在几分钟或者是几小时内回复 (当然除了我睡觉时 :D).

命令行参数

加上 --help 查看完整文档。下面是一个快照 (可能已经过期):

flutter_rust_bridge_codegen 1.20.1

USAGE:
    flutter_rust_bridge_codegen [FLAGS] [OPTIONS] --dart-output <dart-output> --rust-input <rust-input>

FLAGS:
        --skip-add-mod-to-lib    Skip automatically adding `mod bridge_generated;` to `lib.rs`
        --no-build-runner        Skip running build_runner even when codegen-capable code is detected
    -v, --verbose                Show debug messages
    -h, --help                   Prints help information
    -V, --version                Prints version information

OPTIONS:
    -r, --rust-input <rust-input>                              Path of input Rust code
    -d, --dart-output <dart-output>                            Path of output generated Dart code
        --dart-decl-output <dart-decl-output>
            If provided, generated Dart declaration code to this separate file

    -c, --c-output <c-output>...                               Path of output generated C header
        --rust-crate-dir <rust-crate-dir>                      Crate directory for your Rust project
        --rust-output <rust-output>                            Path of output generated Rust code
        --class-name <class-name>                              Generated class name
        --dart-format-line-length <dart-format-line-length>    Line length for dart formatting
        --llvm-path <llvm-path>...                             Path to the installed LLVM
        --llvm-compiler-opts <llvm-compiler-opts>              LLVM compiler opts
        --dart-root <dart-root>
            Path to root of Dart project, otherwise inferred from --dart-output

从 0 设置 Flutter/Dart+Rust 环境

This documentation is archived, though technically still correct. Have a look at integrating with existing projects chapters for a more detailed demonstration.

I suggest that you can start with the Flutter example first, and modify it to satisfy your needs. It can serve as a template for new projects. It is run against CI so we are sure it works.

Indeed, this library is nothing but a code generator that helps your Flutter/Dart functions call Rust functions. Therefore, "how to create a Flutter app that can run Rust code" is actually out of the scope of this library, and there are already several tutorials on the Internet.

However, I can sketch the outline of what to do if you want to set up a new Flutter+Rust project as follows.

Step 1

Create a new Flutter project (or use an existing one). The Dart SDK should be >=2.14.0 if you want to use the latest ffigen tool.

Step 2

Create a new Rust project, say, at directory rust under the Flutter project.

Step 3

Edit Cargo.toml and add:

[lib]
name = "flutter_rust_bridge_example" # whatever you like
# notice this type. `cdylib` for android, and `staticlib` for iOS. I write down a script to change it before build.
+ crate-type = ["cdylib"]

Step 4

Follow the standard steps of "how iOS uses static libraries".

  1. In XCode, edit Strip Style in Build Settings to Debugging Symbols.
  2. Add your lib{crate}.a to Link Binary With Libraries in Build Phases.
  3. Add binding.h to Copy Bundle Resources.
  4. Add #import "binding.h" to Runner-Bridging-Header.
  5. Last but not least, add a never-to-be-executed dummy function in Swift that calls any of the generated C bindings. This lib has already generated a dummy method for you, so you simply need to add print("dummy_value=\(dummy_method_to_enforce_bundling())"); to swift file's override func application(...) {}, and this will prevent symbol stripping - especially in the release build for iOS (i.e. when building ipa file or releasing to App Store). Notice that, we have to use that dummy_method_to_enforce_bundling(), otherwise the symbols will not maintain in the release build, and Flutter will complain it cannot find the symbols.

Step 5

Lastly, in order to build the Rust library automatically when you are building Flutter, follow this tutorial.

文章

这一章包含了一些和 flutter_rust_bridge 相关的文章。

Rust 异步

作者:@AlienKevin

This library does not yet support returning a Future type from Rust and this has to do with the difficulty of uniting the various approaches to async in Rust. The Rust Book summarized the current state of async support succinctly:

The most fundamental traits, types and functions, such as the Future trait are provided by the standard library. The async/await syntax is supported directly by the Rust compiler.

Many utility types, macros and functions are provided by the futures crate. They can be used in any async Rust application.

Execution of async code, IO and task spawning are provided by "async runtimes", such as Tokio and async-std. Most async applications, and some async crates, depend on a specific runtime.

While the futures crate provides an executor called futures::executor::block_on, libraries that use Tokio runtime cannot use this executor. According to Rust-lang community wiki, crates like Tokio that provide both a runtime and IO abstractions often have their IO depend on the runtime. This can make it difficult to write runtime-agnostic code. First, we demonstrate a common use case of async programming in Rust by attempting to fetch the content of a file from the internet using the popular HTTP Client Reqwest:

use anyhow;

async fn get() -> anyhow::Result<String> {
    let url = "https://link/to/file/download";
    let data = reqwest::get(url).await?.text().await?;
    Ok(data)
}

When you try to generate bindings for the get function, the generated code will contain errors because this library does not support returning Future from Rust.

Mismatched runtime

The next logic thing to try would be to convert the asynchronous code to synchronous by directly blocking the current thread and execute the code. For our first attempt, we wrap futures::executor::block_on around an async block containing reqwest calls.

use anyhow;
use futures::executor::block_on;

fn get() -> anyhow::Result<String> {
    block_on(async {
        let url = "https://link/to/file/download";
        let data = reqwest::get(url).await?.text().await?;
        Ok(data)
    })
}

Since Reqwest uses the Tokio runtime instead of the futures runtime, our code panicked with the error "there is no reactor running, must be called from the context of a Tokio 1.x runtime". To fix this error, we have two ways to execute async codes using the Tokio runtime. Approach 1 is the simplest and uses the convenient tokio::main macro to turn an async function to a synchronous one. Approach 2 requires you to explicitly create a new Tokio runtime and use its block_on function to run the future to completion.

Approach 1 (macro)

use anyhow;

#[tokio::main(flavor = "current_thread")]
async fn get() -> anyhow::Result<String> {
    let url = "https://link/to/file/download";
    let data = reqwest::get(url).await?.text().await?;
    Ok(data)
}

It has the following dependencies:

[dependencies]
futures = "0.3"
reqwest = "0.11.6"
tokio = { version = "1.14.0", features = ["rt", "macros"] }
anyhow = { version = "1.0.49" }

Approach 2 (runtime)

use anyhow;
use tokio::runtime::Runtime;

fn get() -> anyhow::Result<String> {
    let rt = Runtime::new().unwrap();
    rt.block_on(async {
        let url = "https://link/to/file/download";
        let data = reqwest::get(url).await?.text().await?;
        Ok(data)
    })
}

It has the following dependencies:

[dependencies]
futures = "0.3"
reqwest = "0.11.6"
tokio = { version = "1.14.0", features = ["rt-multi-thread"] }
anyhow = { version = "1.0.49" }

Plain futures

If you are using the plain futures crate without runtimes like Tokio, you should be safe to wrap the asynchronous code in an async block and use the futures::executor::block_on to run the future to completion:

use futures::executor::block_on;

async fn hello_world() -> String {
    "hello, world!".to_string()
}

fn get() -> String {
    block_on(async {
        hello_world().await
    })
}

fn main() {
    println!("{}", get()); // prints "hello, world!"
}

Avoid async

Lastly, you can avoid async code all together by using synchronously/blocking version of the functions if they are available. In Reqwest, there's a module called reqwest::blocking designed specifically for this purpose. So you can achieve the same thing above without using async.

use anyhow;
use reqwest;

fn get() -> anyhow::Result<String> {
    let url = "https://link/to/file/download";
    let data = reqwest::blocking::get(url)?.text()?;
    Ok(data)
}

It has the following dependencies:

[dependencies]
futures = "0.3"
reqwest = { version = "0.11.6", features = ["blocking"] }
anyhow = { version = "1.0.49" }

Generating multiple files

Author: @dbsxdbsx

This article describes some thoughts and implementations about the feature of generating multiple files.

Before, like the pure_dart's api.rs, all APIs are exposed together in a single file(block). This is not bad when the whole project is simple. But it would become quite hard to maintain or develop, when the project becomes more and more complex, especially when it is a team project. Therefore, it is time to reconstruct code --- classify the exposed Api into proper blocks(files).

(Before going on reading, make sure that you are quite familiar with how to use template to generate code with flutter_rust_bridge. If not, take a look at the former chapters or the basic example again, please.)

Try to classify Api into different blocks(files)

Suppose, you only have two Api in api.rs originally, like this:

#![allow(unused_variables)]

pub fn simple_add(a: i32, b: i32) -> i32 {
    a + b
}

pub fn simple_minus(a: i32, b: i32) -> i32 {
    a - b
}

Now you want to classify these 2 Api into 2 blocks for some reason-- say, you put the simple_add Api into file api_1.rs and the other into api_2.rs. And then make a little modification in lib.rs:

mod api_1;
mod api_2;

Ok, now the question is how to deal with them with flutter_rust_bridge? From the template justfile, we know code from a single API file called api_rs can be generated with a command like this:

gen:
    export REPO_DIR="$PWD"; cd /; flutter_rust_bridge_codegen {{llvm_path}} \
        --rust-input "$REPO_DIR/native/src/api.rs" \
        --dart-output "$REPO_DIR/lib/bridge_generated.dart" \
...

(For simplicity, only two necessary flags rust-input and dart-output here.)

Then, to generate code within 2 blocks(files), you may come out with an approach like this:

gen:
    export REPO_DIR="$PWD"; cd /; flutter_rust_bridge_codegen {{llvm_path}} \
        --rust-input "$REPO_DIR/native/src/api_1.rs" \
        --dart-output "$REPO_DIR/lib/bridge_generated_api_1.dart" \

    export REPO_DIR="$PWD"; cd /; flutter_rust_bridge_codegen {{llvm_path}} \
        --rust-input "$REPO_DIR/native/src/api_2.rs" \
        --dart-output "$REPO_DIR/lib/bridge_generated_api_2.dart" \
...

But here comes a problem, how to use them in dart? Like await API.simpleAdd(1,2) or await API.simpleMinus(1,2) as before? The point here is, to thoroughly decouple Api from different blocks (which is the main reason for using multiple blocks of API), flag class-name is needed. So the command should be modified like this:

gen:
    export REPO_DIR="$PWD"; cd /; flutter_rust_bridge_codegen {{llvm_path}} \
        --rust-input "$REPO_DIR/native/src/api_1.rs" \
        --dart-output "$REPO_DIR/lib/bridge_generated_api_1.dart" \
        --class-name ApiClass1

    export REPO_DIR="$PWD"; cd /; flutter_rust_bridge_codegen {{llvm_path}} \
        --rust-input "$REPO_DIR/native/src/api_2.rs" \
        --dart-output "$REPO_DIR/lib/bridge_generated_api_2.dart" \
        --class-name ApiClass2
...

(The class name ApiClass1 and ApiClass2 are chosen arbitrarily here.)

So now it seems to be perfect to generate code and using Api in Dart like ApiClass1.simpleAdd(1,2) or ApiClass2.simpleMinus(1,2).

But actually, the above command is still not enough to generate code correctly. Because multiple blocks need to be translated respectively through FFI. So on the rust side, instead of generating code to a single file bridge_generated.rs, now there are 2 files needed. But, what are the names of these 2 auto-generated rust files? Here, for less misunderstanding, flutter_rust_bridge decides to ask for another compulsory flag rust-output. So the command should be modified like this:

gen:
    export REPO_DIR="$PWD"; cd /; flutter_rust_bridge_codegen {{llvm_path}} \
        --rust-input "$REPO_DIR/native/src/api_1.rs" \
        --dart-output "$REPO_DIR/lib/bridge_generated_api_1.dart" \
        --class-name ApiClass1 \
        --rust-output generated_api_1

    export REPO_DIR="$PWD"; cd /; flutter_rust_bridge_codegen {{llvm_path}} \
        --rust-input "$REPO_DIR/native/src/api_2.rs" \
        --dart-output "$REPO_DIR/lib/bridge_generated_api_2.dart" \
        --class-name ApiClass2 \
        --rust-output generated_api_2
...

(Still, the rust output name generated_api_1 and generated_api_2 are chosen arbitrarily here.)

That is, flutter_rust_bridge asks you to manually define the generated rust file names, feel free to choose any name you like.

Some issues with separate commands

Based on the last commands we come up with, everything seems to be fine --- the code generated, you can use them in Dart, and the whole project is compilable. And you would also notice some changes in lib.rs:

mod api_1;
mod api_2;
mod generated_api_1; /* AUTO INJECTED BY flutter_rust_bridge. This line may not be accurate, and you can change it according to your needs. */
mod generated_api_2; /* AUTO INJECTED BY flutter_rust_bridge. This line may not be accurate, and you can change it according to your needs. */

But actually, it is not good enough.

issue from explicit Api conflict

Let's say one day, you decide to add another API, say simpleDivide. But when you compile the whole project, the Dart compiler just complains "The symbol simpleDivide has already been defined ...". Then you check whether this simpleDivide is defined duplicated. Finally, you find that it's already defined in another block. This situation occurs quite a lot, when the other block is in the charge of someone else, especially in a big project. It is easy to see that the whole routine is a little inefficient since you don't realize the Api conflict until doing compiling when you've probably coded a lot with this "new defined" Api --- and the more time compiling takes, the more inefficient.

issue from implicit Api conflict

And what makes the Api conflict issue more catastrophic? Say you define another Api with parameter String in api_1.rs:

pub fn test_string_1(s1: String) {
    println!("test implicit parameter conflicts {}", s1);
}

And then you put another Api with parameter String in api_2.rs:

pub fn test_string_2(s2: String) {
    println!("test implicit parameter conflicts {}", s2);
}

These 2 Apis don't violate the uniqueness required by FFI. They should be compilable with no error. But the truth is no! Why? Because for the String parameter, flutter_rust_bridge would automatically generate API like this:

#[no_mangle]
pub extern "C" fn new_uint_8_list(len: i32) -> *mut wire_uint_8_list

which is used to let rust code easily cooperate with Dart through FFI. So if there are 2 APIs both taking String as parameters over blocks, you should notice a similar panic like "the symbol new_uint_8_list is already defined ..." during compiling(issue #511).

(Actually, since version 1.37, even with the separated commands with no Api defined, the whole project is still not compilable with error "symbol free_WireSyncReturnStruct is already defined... ", the symbol free_WireSyncReturnStruct is another implicitly Api generated by flutter_rust_bridge.)

So these kinds of explicit/implicit Api conflicts are annoying and frustrating. How to resolve it?

Theoretically, the conflict can be detected earlier during generating code, when flutter_rust_bridge knows every detail about API. But the key is that flutter_rust_bridge has to know all Api over all blocks before generating code. That is, with the separated command stated above, flutter_rust_bridge can't do the check for you in practice. Therefore, it is necessary to unite the separated commands into ONE command.

correct command for generating code with multiple blocks

Now comes the joined command to resolve the above issue:

gen:
    export REPO_DIR="$PWD"; cd /; flutter_rust_bridge_codegen {{llvm_path}} \
        --rust-input "$REPO_DIR/native/src/api_1.rs" "$REPO_DIR/native/src/api_2.rs" \
        --dart-output "$REPO_DIR/lib/bridge_generated_api_1.dart" "$REPO_DIR/lib/bridge_generated_api_2.dart" \
        --class-name ApiClass1 ApiClass2 \
        --rust-output generated_api_1 generated_api_2
...

Here, with just 1 command, flutter_rust_bridge would smartly check if there are conflicts over all Api over all blocks, be it defined explicitly or implicitly.

That is, for the explicitly defined APIs like simple_add and simple_minus, if there are duplicated ones, flutter_rust_bridge would throw a panic like "thread 'main' panicked at 'symbol [simple_add] has already been defined'...", and you are responsible to fix it. And for the implicitly defined API like new_uint_8_list, since it is essential, flutter_rust_bridge would try to work around it by adding suffix starting from 0, like new_uint_8_list_0 and new_uint_8_list_1.

To sum up, there are 4 compulsory flags when you deal with multiple blocks. They are rust-input, dart-output, class-name and rust-output. Also, the number of fields following each flag should be consistent. You can try to cargo build with fewer flags or inconsistent fields to see what kind of panic would be popped up with the pure_dart_multi example when doing generation.

bizarre, weird but compilable command with the disorder

Flutter_rust_bridge doesn't do semantic correction over all flags. So, it is syntactically correct with the following generation command:

gen:
    export REPO_DIR="$PWD"; cd /; flutter_rust_bridge_codegen {{llvm_path}} \
        --rust-input "$REPO_DIR/native/src/api_orange.rs" "$REPO_DIR/native/src/api_apple.rs" \
        --dart-output "$REPO_DIR/lib/gen_api_apple.dart" "$REPO_DIR/lib/gen_api_orange.dart" \
        --class-name ApiClassOrange ApiClassApple \
        --rust-output generated_api_apple generated_api_orange

NOTE: the suffix apple and orange are quite disordered for each flag here on purpose. It is compilable and usable. But as you should know, it is not a good practice, semantically. It is all up to you to decide the field names for each flag, so be beware of it!