返回列表

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 接口、内存释放和一大堆类型转换。

Flutter 通过 FRB、Swift 和 Kotlin 通过 UniFFI 调用 Rust Core 的两条路径
FRB 与 UniFFI 都是调用 Rust 的桥,差别主要在桥的另一端是谁。

先给答案:你的 App 用什么写?

如果你的 App 用 Flutter 写,并且 Rust 主要给 Flutter 调用,先选 flutter_rust_bridge,通常最省心。

如果你想把 Rust 功能直接给 iOS 的 Swift、Android 的 Kotlin,或者 Python 等其他语言使用,先看 UniFFI。它更像一个把 Rust 打包成多语言小库的工具。

可以先用这张表判断:

你的情况更适合的工具
App 的界面由 Flutter 编写flutter_rust_bridge
要在 SwiftUI/iOS 项目中调用 RustUniFFI
要在 Android 原生 Kotlin 项目中调用 RustUniFFI
只需要少量 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 项目里,你会做三件事:

  1. 在 Rust 中写公开函数和数据类型。
  2. 运行 FRB 的生成命令。
  3. 在 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

下面的表比只看“支持哪些语言”更有用:

对比点FRBUniFFI
主要面对谁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 不需要知道每个平台具体怎么实现。

Rust Core 通过回调接口向 Flutter、iOS 或 Android 请求系统能力并接收结果
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 或数据库路径放进错误信息。

代码生成以后,还有一半集成工作

工具自动生成桥接代码,不代表只点一次按钮就能完成集成。开始前通常还要准备:

  1. Rust 项目能单独编译和测试。
  2. 生成工具版本固定,避免团队成员生成出不同结果。
  3. iOS、Android 或桌面所需的 Rust 编译目标。
  4. 一个明确的生成命令,放进脚本或 CI。
  5. 不手改生成文件,改 Rust 接口后重新生成。
  6. 一个手写的 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 后面?