字节笔记本
2026年8月29日
Rust + SwiftUI + Flutter 混合架构怎么落地
BeyondTranslate(以前叫比译)是一款跑在 macOS、Windows、Linux 上的划词/截图翻译应用,能接十来家引擎。仓库最近在做一件挺具体的事:核心服务迁到 Rust,翻译窗还是 Flutter,macOS 设置页改成原生 SwiftUI,两边都通过 FFI 调同一套 Rust API。
下面按仓库现状把这条拆法捋一遍,方便你自己的桌面应用照着落。
为什么要把 UI 和核心拆开
翻译这类应用,真正费劲的不是界面,是引擎调度、词典、OCR、权限、设置持久化。这些在三个桌面平台上几乎一样,但窗口形态差很多:翻译窗要轻、要能从任意屏幕取词;设置页在 macOS 上用户更认系统那套列表和分组。
如果把引擎和设置写在 Flutter 里,SwiftUI 设置页只能再抄一份,或者通过 MethodChannel 把 Dart 当中转。反过来,设置写在 Swift 里,Windows / Linux 又没戏。拆开之后,Rust 只暴露「运行时能干什么」,Flutter 和 SwiftUI 各自画自己的窗。仓库 README 里写得很直:核心换 Rust 是为了性能和跨平台复用,UI 层继续用 Flutter,macOS 设置页用 SwiftUI。
翻译窗这边还用了 nativeapi 一类的原生能力,让 Flutter 桌面窗看起来不那么「跨平台壳」。这只影响观感,不改变「业务在 Rust」这件事。
Rust 怎么当共享 API
仓库里其实有两条绑定,职责不太一样。
第一条是给 Flutter 翻译窗用的 flutter_rust_bridge。桌面端 Rust 暴露一组 Runtime API,生成出来的 Dart 侧大致是 Runtime、RuntimeSettings、RuntimeTranslation、RuntimeDictionary(还有 OCR、权限、取词等周边)。设置、引擎、翻译这些 domain 写在 apps/desktop/rust 里,flutter_rust_bridge 生成 frb_generated.dart 和对应的 Rust glue。Flutter 里的 DesktopSettingsService、运行时调试页,走的都是这条路。
第二条是 packages/runtime:一个 Flutter FFI 插件,用 UniFFI 和 uniffi-dart 把同一份小 Rust crate 同时暴露给 Dart 和 Swift。这一条是给「Flutter 进程里的原生 Swift 代码」准备的,设置页、以后的 Share Extension 都能直接调,不必再绕 Dart。
packages/runtime 的构建钩子在 hook/build.dart:按当前 Flutter 目标跑 cargo build --release,再注册一个 CodeAsset(package:beyondtranslate_runtime / uniffi:beyondtranslate_runtime)。Dart 侧的 @Native(assetId: ...) 才能在运行时找到那份 cdylib。
Swift 这边,钩子本身不管。插件另带一个 Swift Package,路径是 macos/beyondtranslate_runtime/,Flutter 的 macOS SPM 会按 pluginClass: BeyondtranslateRuntimePlugin 自动发现。Package.swift 里两个 target:beyondtranslate_runtimeFFI 是 UniFFI 的 C 伞头文件和 modulemap(由 scripts/generate/runtime_bindings.py 从 swift/Generated/ 镜像过来);beyondtranslate_runtime 再导出生成的 Swift 绑定,外加一个很薄的 FlutterPlugin stub。链接用了 -undefined dynamic_lookup,符号等到运行时再解析。
插件 register 的时候会 dlopen Frameworks/beyondtranslate_runtime.framework/beyondtranslate_runtime。宿主 Swift(AppDelegate、设置页、扩展)要等 RegisterGeneratedPlugins(...) 跑完再调绑定,否则框架还没加载进去。
两条绑定面对的是同一份运行时概念:设置变更、翻译请求、词典查询都落在 Rust。差别只是谁生成胶水、谁在进程里先把动态库装上。
SwiftUI 设置页和 Flutter 翻译窗怎么接到同一套核心
对照仓库里的目录,可以想成三层:
- Rust 运行时。
Runtime是入口,设置走RuntimeSettings,翻译走RuntimeTranslation,词典走RuntimeDictionary。引擎列表、快捷键、自动翻译这些状态存在这儿,不存在某个 UI 框架的 Store 里。 - Flutter 翻译窗。
apps/desktop用 FRB 生成的 Dart API 调上面这些对象。主窗、划词、截图翻译、多引擎结果,都还是 Flutter。需要更像原生窗口时,再叠 nativeapi,不把业务搬回 Dart。 - macOS SwiftUI 设置页。
apps/desktop/macos/Runner/Features/Settings/下面是模型、View、ViewModel、SettingsRepository。仓库里还有NativeSettingsPlugin,把原生设置页嵌回 Flutter 的调试路由,方便两边对照。Repository 通过 UniFFI 生成的 Swift 绑定调 Rust,不经过 Dart。
同一条设置,Flutter 改完 Rust 里是一份,SwiftUI 再读也是这份;反过来也成立。调试页(NativeSettingsDebug、runtime_debug)就是用来看这条回路有没有断。
Windows 和 Linux 没有 SwiftUI 设置页,它们继续用 Flutter 设置,但底层还是同一套 Runtime。拆的是「谁来画设置」,不是「设置存在哪」。
怎么 clone 和跑起来
仓库:https://github.com/beyondtranslate/beyondtranslate
文档:https://beyondtranslate.com/docs/
只想先用成品,去 Releases 下安装包即可。
从源码跑桌面端,README 给的是:
git clone https://github.com/beyondtranslate/beyondtranslate.git
cd beyondtranslate/apps/desktop
flutter run -d macos把 macos 换成 linux 或 windows 即可。本机需要能用的 Flutter 桌面工具链,以及能编 cdylib 的 Rust。第一次跑会走 hook/build.dart 编 runtime,再走 FRB 那份已提交的生成代码。绑定有变动时,UniFFI 一侧看 scripts/generate/runtime_bindings.py,FRB 一侧看 apps/desktop 里的 flutter_rust_bridge.yaml。
容易踩的坑
两套 UI 工具包会一直在。Flutter 和 SwiftUI 的窗口生命周期、快捷键、深色模式、字体渲染都不共用。设置页「看起来像系统设置」的代价,是你要维护两套导航和两套控件。不要指望用一套主题文件抹平。
绑定生成是第二处摩擦。FRB 和 UniFFI 各生成一份胶水,改 Rust API 漏跑一边,常见症状是 Dart 能编译、Swift 还在用旧符号,或者反过来。仓库把生成物提交进 git(frb_generated.*、swift/Generated/、Dart @Native 绑定),review 时把这些文件和手写的 domain 一起看,比只看 *.rs 稳。
第三条跟加载顺序有关。hook/build.dart 只给 Dart 的 native-assets 注册 cdylib;Swift 靠插件 dlopen 那份 framework。宿主代码如果在 RegisterGeneratedPlugins 之前就调 UniFFI,会遇到符号找不到。Share Extension 这类不走 Flutter 插件注册的进程,要自己链 swift/Generated/ 里的 .swift / .h,并带上同一份 libbeyondtranslate_runtime.dylib。
dynamic_lookup 让链接能过,但不保证运行时符号还在。Flutter 进程里正常、纯 Swift 宿主里崩,多半是 framework 没被装进 bundle,或 asset 名和 @Native(assetId:) 对不上。
最后,架构还在升级中。README 自己标了 in progress。对照文档和仓库时,以 packages/runtime 和 apps/desktop/rust 的当前代码为准。
文档和仓库
- 项目主页:https://beyondtranslate.com/
- 使用文档:https://beyondtranslate.com/docs/
- 源码:https://github.com/beyondtranslate/beyondtranslate
- 运行时插件说明:https://github.com/beyondtranslate/beyondtranslate/tree/main/packages/runtime
- UniFFI:https://mozilla.github.io/uniffi-rs/
- flutter_rust_bridge:https://github.com/fzyzcjy/flutter_rust_bridge
只想在 Flutter 桌面应用里复用一份 Rust 核心,可以先抄 packages/runtime 这条 UniFFI + native-assets 的装库方式,再按自己的 domain 加 FRB。macOS 上要再挂一层 SwiftUI,关键是让原生代码在插件 dlopen 之后,调同一份 crate,而不是再写一个设置后端。