2026年9月15日 · 约 9 分钟读完 · 更新于 2026年9月15日
Flutter 调 Rust,该选 FRB 还是 UniFFI?
面向刚接触 Rust 跨语言调用的开发者,用最小示例讲清 flutter_rust_bridge 和 UniFFI 的区别、用法与选型。
本文把 uniffi rs 统一称为 UniFFI。如果你第一次看到这两个名字,也不用先理解它们全部的实现细节。先记住一句话:它们都是让 Rust 能被别的语言调用的工具。
只记结论就够了: Flutter 为主,先选 FRB;Swift 或 Kotlin 为主,先看 UniFFI。后面的内容帮助你理解为什么。
Flutter 为什么不能直接调用 Rust?
假设你在 Flutter 里写了一个音乐播放器,但有一段逻辑想交给 Rust:例如加密、音频分析、文件扫描,或者一套希望同时给 iOS 和 Android 使用的规则。
Flutter 主要写 Dart,Rust 代码不能直接这样调用:
final result = rustFunction();
中间需要一座“翻译桥”。这座桥负责把 Dart、Swift 或 Kotlin 的参数变成 Rust 能理解的值,等 Rust 做完工作后,再把结果翻译回去。这个过程叫 FFI,也就是“不同编程语言之间互相调用”。
flutter_rust_bridge 和 UniFFI 都是在帮我们搭这座桥。它们会根据 Rust 代码自动生成很多重复的连接代码,所以我们不必手写 C 接口、内存释放和一大堆类型转换。
先给答案:你的 App 用什么写?
如果你的 App 用 Flutter 写,并且 Rust 主要给 Flutter 调用,先选 flutter_rust_bridge,通常最省心。
如果你想把 Rust 功能直接给 iOS 的 Swift、Android 的 Kotlin,或者 Python 等其他语言使用,先看 UniFFI。它更像一个把 Rust 打包成多语言小库的工具。
可以先用这张表判断:
| 你的情况 | 更适合的工具 |
|---|---|
| App 的界面由 Flutter 编写 | flutter_rust_bridge |
| 要在 SwiftUI/iOS 项目中调用 Rust | UniFFI |
| 要在 Android 原生 Kotlin 项目中调用 Rust | UniFFI |
| 只需要少量 Flutter 调 Rust 的功能 | flutter_rust_bridge |
| 同一套 Rust 功能要给 Flutter 和原生 App 使用 | Flutter 用 FRB,原生 App 用 UniFFI |
这里的“FRB”就是 flutter_rust_bridge 的常用简称。后面会一直用这个名字。
FRB:Flutter 项目最省事的 Rust 入口
FRB 的目标很明确:让 Flutter 和 Rust 配合工作。
从 Rust 到 Flutter,通常只走三步
在一个常见的 FRB 项目里,你会做三件事:
- 在 Rust 中写公开函数和数据类型。
- 运行 FRB 的生成命令。
- 在 Dart 中通过生成的 API 调用 Rust。
比如 Rust 中有一个向用户问好的函数:
pub struct Greeting {
pub message: String,
}
pub async fn greet(name: String) -> Result<Greeting, String> {
if name.trim().is_empty() {
return Err("Name cannot be empty".to_owned());
}
Ok(Greeting {
message: format!("Hello, {name}!"),
})
}
运行生成工具后,Dart 侧会得到可以调用的接口。使用时的感觉和普通异步 Dart 代码接近:
final greeting = await api.greet(name: 'Yulin');
print(greeting.message);
这就是 FRB 的优势:Flutter 开发者不需要先熟悉 Swift、Kotlin 或复杂的 C FFI。大多数时候,Rust 的异步函数会变成 Dart 的 Future,持续变化的数据可以用 Dart 的 Stream 接收。
例如 Rust 正在处理一批图片,Flutter 想显示进度,FRB 可以让 Flutter 用 stream 或回调接收“已完成 20%”“已完成 40%”这类更新。Flutter 的状态管理、FutureBuilder、StreamBuilder 仍然可以照常使用。
UniFFI:让 Rust 像原生 Swift/Kotlin 库
UniFFI 的想法是:先把 Rust API 定义清楚,再自动为不同语言生成各自习惯的调用方式。
它常见的使用场景是:核心逻辑写在 Rust,iOS 用 Swift 调用,Android 用 Kotlin 调用。这样两端不必各自实现一遍网络协议、加密算法或业务规则。
从 Rust 到原生端,多了一步“声明接口”
UniFFI 要求我们更明确地说明哪些 Rust 类型要给外部语言使用。下面仍然是问好的例子:
#[derive(uniffi::Record)]
pub struct Greeting {
pub message: String,
}
#[derive(Debug, thiserror::Error, uniffi::Error)]
pub enum GreetingError {
#[error("Name cannot be empty")]
EmptyName,
}
#[uniffi::export(async_runtime = "tokio")]
pub async fn greet(name: String) -> Result<Greeting, GreetingError> {
if name.trim().is_empty() {
return Err(GreetingError::EmptyName);
}
Ok(Greeting {
message: format!("Hello, {name}!"),
})
}
这里的标记可以先简单理解:
Record:一组有名字的普通数据,类似 Dart class 或 Swift struct。Error:一个可以让外部语言识别的错误类型。export:告诉 UniFFI,这个函数需要生成给其他语言调用。
UniFFI 生成 Swift 代码后,iOS 开发者大致会这样调用:
do {
let greeting = try await greet(name: "Yulin")
print(greeting.message)
} catch {
print("Something went wrong")
}
Swift 开发者看到的是熟悉的 async 和 try,不必直接理解 Rust 的 Result。这就是 UniFFI 的主要价值:让 Rust 在目标语言里看起来更像“本地写出来的库”。
真正的区别:不是性能,而是谁来调用 Rust
下面的表比只看“支持哪些语言”更有用:
| 对比点 | FRB | UniFFI |
|---|---|---|
| 主要面对谁 | Flutter/Dart 开发者 | Swift、Kotlin、Python 等开发者 |
| Dart/Flutter 体验 | 很自然,是它的主场 | 不是主要目标 |
| Swift/Kotlin 体验 | 不是主要目标 | 很自然,是它的主场 |
| Rust 代码的写法 | 从公开 Rust API 生成 Dart 调用代码 | 用标记说明哪些类型和函数可以导出 |
| 异步结果 | 通常对应 Dart Future | 对应目标语言的异步写法,例如 Swift async throws |
| 持续事件 | 可以用 Dart Stream 或回调 | 常用 callback interface,也会映射到目标语言的回调方式 |
| 最适合的项目 | Flutter 为主的 App | 原生多端共享 Rust 核心的项目 |
两者的性能差异通常不是选型的第一理由。很多实际项目里,网络、磁盘、图片解码和 UI 渲染花的时间远远多于这层桥接。真正应该先问的是:谁来调用 Rust?团队主要维护 Flutter,还是 Swift/Kotlin?
跨语言最容易出错的,其实是数据
Rust 里不是每种类型都适合拿出去给 Dart 或 Swift 用。可以把跨语言数据理解成 API 返回的 JSON 模型:字段越明确,跨语言越容易维护。
| 适合传给 Dart/Swift 的数据 | 应留在 Rust 内部的东西 |
|---|---|
| 字符串、数字、布尔值 | 数据库连接、网络 client |
| 可选值,例如“头像可能没有” | 文件句柄、多线程锁 |
| 列表,例如“搜索结果列表” | 正在运行的后台任务 |
| 字段固定的用户、歌曲、订单 | Flutter BuildContext、Swift UIViewController 等平台对象 |
| 加载中、成功、失败等有限状态 | 与具体运行环境强绑定的对象 |
这些对象和运行环境绑得很紧。把它们直接传到另一门语言,往往会遇到释放时机不对、线程不对或崩溃难查的问题。
别把 Rust 的内部状态摊给 UI
有时我们不只是调用一个函数,而是要长期保存状态。比如一个搜索服务、下载管理器、播放器队列或本地数据库。
这时不要把所有内部字段都暴露出去。更好的做法是把 Rust 对象当成一个“黑盒”:外部语言只能调用它提供的方法,内部的锁、缓存和后台任务仍由 Rust 管理。
UniFFI 中的写法大概是这样:
#[derive(uniffi::Object)]
pub struct Counter {
value: std::sync::Mutex<i64>,
}
#[uniffi::export]
impl Counter {
#[uniffi::constructor]
pub fn new(initial: i64) -> Self {
Self {
value: std::sync::Mutex::new(initial),
}
}
pub fn increment(&self) -> i64 {
let mut value = self.value.lock().expect("counter lock");
*value += 1;
*value
}
}
Swift 或 Kotlin 可以创建和使用 Counter,但不能直接改 Rust 内部的 value。FRB 也有类似的“不直接暴露内部”的对象方式。
真实项目里,比起写一堆 getName()、setName(),更建议提供完整动作,例如:
downloadManager.start(task)
downloadManager.cancel(taskId)
player.replaceQueue(tracks)
player.pause()
这样外部语言只表达“我想做什么”,不用知道 Rust 内部如何存数据、怎样加锁或怎样调度任务。
Rust 反过来需要系统能力时怎么办?
有些事情 Rust 不该自己做,例如:
- 从 Keychain 或安全存储读取密码;
- 请求相机、通知或文件权限;
- 使用手机系统播放器;
- 打开页面、弹出提示;
- 选择文件或照片。
这些属于 Flutter、iOS 或 Android 平台。Rust 只需要提出一个小问题,例如“请根据这个凭据编号取回密码”,由外部应用回答它。
UniFFI 可以定义这样的回调接口:
#[uniffi::export(callback_interface)]
pub trait CredentialProvider: Send + Sync {
fn password(&self, credential_ref: String) -> Option<String>;
}
可以把它理解成 Rust 规定了一个插座:iOS 用 Keychain 接上去,Flutter 用安全存储接上去。Rust 不需要知道每个平台具体怎么实现。
FRB 也可以处理 Rust 与 Dart 之间的回调和数据流。写法会随 FRB 版本和场景不同,但原则相同:回调只负责一件清楚的小事。不要让 Rust 通过回调直接控制页面跳转、Widget 或动画,这些仍应由 UI 自己管理。
别只返回一句“出错了”
最开始很容易写出 Result<T, String>,也就是出错时只传一句英文文字。但 App 需要区分“网络断了”“密码错了”“请求超时”时,只靠文字会很难处理。
更好的办法是同时提供错误类别和简单说明:
code: unauthorized
message: Please sign in again
retryable: false
或者:
code: network
message: Network is unavailable
retryable: true
这样 Flutter、Swift 或 Kotlin 可以根据 code 决定显示登录页、重试按钮还是普通提示,不必猜测错误文字。无论使用 FRB 还是 UniFFI,都不要把密码、token、带认证信息的 URL 或数据库路径放进错误信息。
代码生成以后,还有一半集成工作
工具自动生成桥接代码,不代表只点一次按钮就能完成集成。开始前通常还要准备:
- Rust 项目能单独编译和测试。
- 生成工具版本固定,避免团队成员生成出不同结果。
- iOS、Android 或桌面所需的 Rust 编译目标。
- 一个明确的生成命令,放进脚本或 CI。
- 不手改生成文件,改 Rust 接口后重新生成。
- 一个手写的 Dart/Swift/Kotlin service,把生成 API 和页面隔开。
最后一点很实用。页面直接使用生成代码当然可以,但项目变大后,生成工具升级、接口调整或错误类型变化,可能会影响很多页面。先用一个 RustService、repository 或 client 包住生成 API,未来会轻松很多。
一句话选型,再开始动手
FRB 和 UniFFI 都是在帮 Rust 和其他语言说话,只是它们主要服务的对象不同:FRB 更懂 Flutter,UniFFI 更懂 Swift、Kotlin 这类原生语言。
新手选型时,可以先按客户端决定,不必过早追求“一套工具支持全部平台”。Flutter 项目先把 FRB 用好;原生多端项目先把 UniFFI 用好。真正重要的是把跨语言接口设计得简单:传明确的数据、返回能处理的错误、让 Rust 管好自己的内部状态,让 UI 管好自己的页面和交互。
开始前可以用这份清单做最后确认:
- 调用 Rust 的主要客户端是 Flutter,还是 Swift/Kotlin?
- 跨语言接口只传递简单、明确的数据吗?
- 错误是否有类别,而不只是错误文字?
- Rust 的内部状态和 UI 的页面状态是否分开管理?
- 生成代码是否放在手写 service 或 client 后面?