难点:集成 Rust代码
下面是一份按 「从零 → 集成 Rust → 打包 iOS → UniFFI 重构」 时间线整理的难点总结,可直接用于面试或项目复盘。
项目背景(一句话)
在 React Native + Expo 钱包 App 中,把自研 MPC GG20 签名 / DKG 能力从 Rust 侧接入移动端:Rust → xcframework → UniFFI Swift 绑定 → Expo Native Module → TypeScript。
涉及模块:
| 模块 | Rust crate | 产物 |
|---|---|---|
expo-mpc(早期) | mpc_client | libmpc_client.xcframework |
expo-mpc-stack(当前) | mpc_ffi | libmpc_ffi.xcframework |
expo-age(加密) | expo_age_uniffi | libexpo_age_uniffi.xcframework |
阶段一:从零开始 — 认知与架构
难点
- 技术栈跨度大:同时涉及 Rust、MPC 密码学、FFI、iOS 交叉编译、CocoaPods、Expo autolinking,没有现成「RN + Rust MPC」模板可抄。
- 调用链很长,任何一环出错都表现为「Native module is null」:
TypeScript → Expo Module (Swift) → UniFFI 生成层 → Rust 静态库 (xcframework)
- Rust 代码在外部仓库(
mpc-stack),App 仓库只消费编译产物,需要建立「改 Rust → 重建 → 拷贝 → pod install」的协作流程。
收获
先画清整条链路,再逐段排查,而不是在 JS/Swift 业务层盲目改代码。
阶段二:集成 Rust — 把密码学能力接进 App
难点
- Expo Module 桥接层:Swift 要把 JS 的
[String: Any]转成 Rust 侧的结构体(MpcConfig、KeyShare等),类型映射和错误传递都要手写。 - 异步与阻塞:Rust 侧
initialize()/signData()可能长时间阻塞;Swift 侧加了 shutdown 超时、Task 隔离,TS 侧也要做 timeout 保护。 - 业务 API 演进:
- 早期
expo-mpc:createKeyShare(nParties, threshold, chain, accountId)— 按 chain 区分 - 重构后
expo-mpc-stack:createKeyShares(curves, ...)— 按 curve(ecdsa/ed25519)区分,一条 curve 可服务多链
- 早期
- MPC 运行时依赖:客户端 DKG/签名需要 gateway + SSE 服务可达,调试时要区分「FFI 层问题」和「网络/协议层问题」。
关键代码位置
- Swift 桥接:
modules/expo-mpc-stack/ios/ExpoMpcStackModule.swift - TS 客户端:
modules/expo-mpc-stack/src/
阶段三:Rust 打包成 iOS 可用 — 交叉编译与产物集成
难点(这一阶段通常最耗时间)
1. iOS 交叉编译 toolchain
- 需要
rustup target add aarch64-apple-ios(真机 arm64) - 模拟器还要
aarch64-apple-ios-sim/x86_64-apple-ios - 外部仓库提供
build_ios.sh,输出.a静态库并打包成xcframework
2. 原生依赖 GMP
MPC 大数运算依赖 GMP,不能直接用 macOS 的 brew install gmp,必须先为 iOS 单独编译:
# 在 mpc-stack 仓库
./scripts/build_gmp_ios.sh # → vendor/gmp-install-ios
# 在 horizonkey 项目
./scripts/rebuild-mpc-stack-ffi.sh # → 拷贝到 modules/expo-mpc-stack/ios/Frameworks/
npx pod-install
3. xcframework 集成到 CocoaPods
podspec 里要正确声明 vendored framework 和头文件搜索路径:
# ExpoMpcStack.podspec
s.vendored_frameworks = 'Frameworks/libmpc_ffi.xcframework'
s.preserve_paths = 'Frameworks/**/*', 'Frameworks/mpc_ffiFFI.modulemap'
s.pod_target_xcconfig = {
'SWIFT_INCLUDE_PATHS' => '$(PODS_TARGET_SRCROOT)/Frameworks $(PODS_TARGET_SRCROOT)',
}
4. iOS 最低版本多处不一致(典型坑)
Rust 库按 iOS 16 构建,但 Expo/RN 默认 15.1,导致 pod install 后 ExpoMpcStack 根本没进 Podfile.lock,JS 报 Native module cannot be null。
需要对齐的位置:
| 配置位置 | 作用 |
|---|---|
*.podspec → s.platforms[:ios] | Pod 声明最低版本 |
ios/Podfile → platform :ios | CocoaPods 全局 platform |
ios/Podfile.properties.json | Expo prebuild 写入值 |
app.json → deploymentTarget | Expo 配置源 |
xcframework 构建时的 min OS | 二进制自带约束 |
Podfile 有 || '15.1' 兜底 — 若 Podfile.properties.json 缺失,全局 platform 会低于 podspec 要求,Pod 被 resolver 跳过。
最终方案:全部统一到 16.0,再 prebuild + pod install。
5. 预编译 vs 按需重建
仓库里预置的 libmpc_ffi.xcframework 可能不含客户端 DKG;要启用 createKeyShare,必须从 mpc-stack 源码重建并拷贝产物。
阶段四:UniFFI 重构 — 从手工 FFI 到自动生成绑定
为什么要重构
早期手工维护 C FFI(cbindgen + 手写 Swift 封装)的问题:
- 接口一改,Swift/ObjC 绑定要手工同步,容易 crash 或内存泄漏
- 错误类型、字符串、字节数组的跨语言传递容易出错
- 多模块(MPC、Age 加密)各自维护一套 FFI,成本高
UniFFI 方案
Rust 侧用 UniFFI 定义接口(.udl 或 proc-macro),自动生成:
| 生成物 | 作用 |
|---|---|
mpc_ffi.swift | Swift 高层 API(MpcSigner、MpcConfig 等) |
mpc_ffiFFI.h | C 层 FFI 头文件 |
mpc_ffiFFI.modulemap | Clang module 定义 |
libmpc_ffi.a → xcframework | Rust 静态库 |
App 侧 只消费生成物,不手改(文件头明确写着 "Trust me, you don't want to mess with it!")。
重构带来的变化
| 维度 | 早期 expo-mpc | 重构后 expo-mpc-stack |
|---|---|---|
| Rust crate | mpc_client | mpc_ffi |
| 外部仓库 | mpc-wallet-gg20 | mpc-stack |
| DKG API | 单 curve + chain 参数 | 多 curve 数组(ecdsa/ed25519) |
| 错误类型 | 4 种 | 新增 KeyError 等 |
| 目录结构 | Frameworks/ | 同结构,命名空间换为 mpc_ffi |
expo-age 也走同一套 UniFFI 模式(GeneratedUniffi/ + FrameworksUniffi/),形成可复用的集成模板。
UniFFI 集成的 podspec 要点
# expo-age 示例 — GeneratedUniffi 与 FrameworksUniffi 分离
s.source_files = "*.{h,m,mm,swift}", "GeneratedUniffi/*.{h,m,mm,swift}"
s.vendored_frameworks = 'FrameworksUniffi/libexpo_age_uniffi.xcframework'
s.public_header_files = "GeneratedUniffi/*.h"
重构后的维护流程
改 Rust (mpc-stack/mpc-ffi)
→ build_ios.sh 重建 xcframework + 生成 Swift/头文件
→ 拷贝到 modules/expo-mpc-stack/ios/Frameworks/
→ npx pod-install
→ 真机验证 DKG / 签名
整体架构图
STAR 口述模板
| 维度 | 内容 |
|---|---|
| S | Expo 钱包需要接入 MPC 门限签名,核心算法在 Rust,要在 iOS 上跑通 |
| T | 从零完成 Rust → iOS 集成,并重构为 UniFFI 自动生成绑定 |
| A | ① 理清 TS→Swift→UniFFI→Rust 链路;② 交叉编译 + GMP + xcframework;③ 配置 CocoaPods podspec;④ 对齐 iOS deployment target;⑤ 迁移到 mpc-stack + UniFFI,统一 rebuild 脚本 |
| R | MPC 签名/DKG 在 iOS 16+ 真机跑通;形成可复用的 Rust-iOS-Expo 集成模板(MPC + Age 两个模块) |
面试口述稿
30 秒版
钱包 App 的 MPC 签名核心在 Rust。我从零搭了 Rust 交叉编译 → xcframework → UniFFI 自动生成 Swift 绑定 → Expo Native Module 的整条链路。最难的是 iOS 交叉编译(GMP 依赖)和 CocoaPods 版本不一致导致 Pod 装不上。后来重构到 UniFFI,接口变更只需重建 Rust 产物,不再手写 FFI。
2 分钟版
项目要做 MPC 门限签名,算法在 Rust 的 mpc-stack 仓库。我一开始对 Rust 移动端集成完全没经验,调用链是 TypeScript → Expo Swift Module → UniFFI 生成的 Swift → Rust 静态库。
第一阶段是搞懂怎么把 Rust 编成 iOS 能用的 xcframework:需要 iOS 交叉编译 toolchain、先编 GMP 大数库、再用 build_ios.sh 产出 framework 和 Swift 绑定,通过 CocoaPods vendored_frameworks 链进 App。
第二阶段踩了 deployment target 的坑:Rust 库按 iOS 16 编译,Expo 默认 15.1,podspec 要求 16,Podfile 有 15.1 fallback,导致 ExpoMpcStack 根本没进 Podfile.lock。对齐 app.json、Podfile.properties.json 和所有 podspec 到 16.0 后解决。
第三阶段是把 FFI 层重构到 UniFFI:早期手工维护 C 绑定,接口一改就要同步改 Swift,还容易内存问题。UniFFI 从 Rust 接口定义自动生成 Swift + C 头文件 + modulemap,App 只消费产物。同时从 expo-mpc 迁移到 expo-mpc-stack,DKG API 从按 chain 改为按 curve 数组,支持 ecdsa/ed25519 多曲线。
收获是形成了一套可复用的 Rust-iOS-Expo 集成模式,Age 加密模块也复用了同一套 UniFFI + xcframework 方案。
简历一行(英文)
Built end-to-end Rust-to-iOS integration for MPC wallet signing: cross-compiled Rust with GMP into xcframework, wired UniFFI-generated Swift bindings through Expo Native Modules, resolved CocoaPods deployment-target mismatches, and migrated from manual FFI to UniFFI-based
expo-mpc-stack.
常见追问
| 追问 | 简答 |
|---|---|
| 为什么用 Rust 不用 Swift/Kotlin 写 MPC? | MPC 协议复杂、已有 Rust 实现(multi-party-ecdsa),跨平台复用,性能和内存安全更好 |
| UniFFI vs 手写 cbindgen? | UniFFI 自动生成 Swift/Kotlin 绑定 + 类型安全 + 错误映射,接口变更成本低 |
| xcframework 和 .a 的区别? | xcframework 是 Apple 的多架构/多平台打包格式,CocoaPods 用 vendored_frameworks 直接链 |
| 为什么 iOS 16? | Passkeys、Security API、Rust 构建目标、Skia 等依赖都要求 16 |
| 改 Rust 后怎么更新 App? | rebuild-mpc-stack-ffi.sh → 拷贝 Frameworks → pod install → 重编 Dev Client |
| 模拟器能跑吗? | 当前 xcframework 只有 ios-arm64(真机),模拟器需额外编译 sim slice |
如果需要,我可以再单独压一版 「和 Android NDK/JNI 类比」 或 「UniFFI 绑定文件清单 checklist」 方便背诵。