返回博客

cargo-swift 打包 Rust → Swift 的两个隐形炸弹

cargo-swift 把一个 Rust crate 封装成 Swift Package(.xcframework + 自动生成的 .swift 绑定),是目前在 macOS/iOS 上复用 Rust 逻辑最流畅的方案之一。然而整个流程有两处陷阱,共同特征是:编译和链接都成功,等你跑起来才炸

我在一个把 Rust 核心逻辑嵌入 SwiftUI 应用的项目里踩了这两个坑,记录下来省得你再走弯路。

现象

坑 A(运行时秒崩):Xcode 编译通过、链接无报错。App 启动后,第一次调用任何 Rust 导出函数,进程立即死亡,Xcode 控制台打印:

Fatal error: UniFFI contract version mismatch:
try cleaning and rebuilding your project

进程生命周期约 1 秒。更诡异的是,按照错误提示去 Clean Build Folder 再重跑,毫无作用——因为问题根本不在"脏缓存"。

坑 B(构建期 panic):在 agent、CI 或任何非交互式 shell 里执行 cargo swift package,命令直接 panic:

thread 'main' panicked ... Result::unwrap() on an Err value:
IO(Custom { kind: NotConnected, error: "not a terminal" })

如果 Rust 是通过 Homebrew 安装的(没有 rustup),还会额外报:

The following toolchains are not installed: x86_64-apple-darwin

根因

坑 A 的根因:bindgen 版本双轨

cargo-swift 内部用自己捆绑的 uniffi-bindgen 生成 Swift 绑定代码,但打包的是你 crate 里声明的 uniffi 版本编译出的 scaffolding。两者版本不一致时,FFI 合约版本号对不上,运行时直接 fatal。

错误信息建议"clean and rebuild"是误导——clean 不能解决版本不匹配的问题。

坑 B 的根因:交互式假设 + universal 构建依赖

cargo swift package 默认走交互式提示(TTY 读取),非 TTY 环境下 unwrap() 直接炸。同时它默认尝试构建 universal binary(arm64 + x86_64),Homebrew Rust 没装 x86_64 cross target,于是再炸一次。

坑:UniFFI 版本不对齐,运行时无声炸锅

cargo-swift 内置的 uniffi-bindgen 版本和 crate 里 [dependencies] uniffi 的版本必须小版本一致(如同为 0.31.x)。版本差一个 minor,编译链接全绿,进程活不过 1 秒。错误提示叫你 clean,但 clean 无效——这是版本问题,不是缓存问题。

正解

修坑 A:对齐 UniFFI 版本

先确认 cargo-swift 捆绑的 bindgen 版本。安装时 build log 会输出,例如:

Compiling uniffi_bindgen v0.31.1

也可查 cargo-swift 的 release notes(cargo-swift 0.11.1uniffi 0.31)。

然后在 crate 的 Cargo.toml 里固定相同 minor

[dependencies]
uniffi = { version = "0.31", features = ["cli"] }
 
[build-dependencies]
uniffi = { version = "0.31", features = ["build"] }

重新 cargo swift package + Xcode 重 build,进程不再秒崩。

升级 cargo-swift 时记得同步升 crate 侧的 uniffi 版本,保持锁步。

修坑 B:非交互式 + arm64-only 调用

cargo swift package -y --skip-toolchains-check \
  --target aarch64-apple-darwin \
  --platforms macos --name MyPackage --release
  • -y--accept-all):自动接受所有交互提示,无需 TTY。
  • --skip-toolchains-check:跳过 x86_64 target 检测,避免 Homebrew Rust 场景报错。
  • --target aarch64-apple-darwin:只构建 arm64,Apple Silicon 开发机上足够验证。Universal binary 留到正式发版时用 rustup 装全双架构再跑。
正解:两行 flag 解决非 TTY 环境 panic

-y --skip-toolchains-check --target aarch64-apple-darwin 三个 flag 组合,让 cargo swift package 在 agent、CI、Homebrew Rust 环境下稳定运行,不再 panic 也不再因缺 x86_64 toolchain 失败。

架构建议:预构建 Swift Package,别在 Xcode Build Phase 里跑 cargo

把 Rust 打包为本地 Swift Package(SPM 本地路径引用),而不是在 Xcode Build Phase 里内嵌 cargo build。后者在 CI 上极其脆弱,环境变量、$PATH、Rust toolchain 位置一旦不对,整个 Xcode 构建就挂。预构建产物 + SPM 本地引用更稳定,也更容易缓存。

一句话外卖

cargo-swift 里的 uniffi-bindgen 版本和你 crate 的 uniffi 版本必须 minor 对齐,版本差异只在运行时爆炸,编译期完全静默。