一个 `#[uniffi::export]`,把 Rust 接进 React Native

一篇偏教程向的 uniffi-bindgen-react-native 入门。适合已经有 Rust core、正在做 React Native / Expo 移动端,并且不想手写 Swift / Kotlin / C++ 胶水的人。

这是 SwarmNote 跨端架构系列的第三篇。上一篇讲了 tauri-specta 如何让 Tauri 的 invoke 有类型;这一篇接着讲移动端:React Native 怎么调用同一份 Rust core。

先说故事

桌面端的问题解决了:Tauri 的 invoke("xxx") 不再靠手写类型硬撑,tauri-specta 帮我们生成了 commands.xxx()events.xxx.listen()

然后移动端来了。

SwarmNote Mobile 选择了 React Native / Expo。原因在第一篇讲过:Tauri mobile 能跑,但移动端文件系统、权限、手势、键盘、安全区这些东西,RN/Expo 生态更顺手。

可是换成 RN 以后,新的问题出现了:

Rust core 已经写好了,React Native 怎么调用它?

你当然可以手写 Swift、Kotlin、C ABI、C++ Turbo Module,再手写 TypeScript 类型。能做,但很快就会变成另一种"跨端重复劳动"。

我们真正想要的是:

rust 复制代码
#[uniffi::export]
pub async fn open_workspace(path: String) -> Result<WorkspaceInfo, FfiError> {
    // Rust 做真正的事
}

RN 侧:

ts 复制代码
const workspace = await appCore.openWorkspace(path);

这就是 uniffi-bindgen-react-native 要解决的问题。

它是什么

一句话:

uniffi-bindgen-react-native 基于 Mozilla UniFFI,把 Rust API 自动生成成 React Native 可以调用的 TypeScript + C++ JSI / Turbo Module 绑定。

运行时链路是:

flowchart LR TS["TypeScript / React Native"] Hermes["Hermes JSI"] CPP["generated C++ binding"] Rust["Rust crate"] TS --> Hermes --> CPP --> Rust style Rust fill:#fff4cc,stroke:#d97706,stroke-width:2px

和桌面端的对应关系:

桌面 Tauri 移动 React Native
#[tauri::command] #[uniffi::export]
tauri-specta 生成 bindings.ts uniffi-bindgen-react-native 生成 TS + C++
WebView IPC Hermes JSI / Turbo Module
tauri_specta::Event callback interface / foreign trait
Tauri 注入 AppHandle / State Rust Object 自己持有状态

这篇不讲所有底层原理,只讲怎么搭起来、怎么写 API、怎么在 RN 里用。

你需要准备什么

基础工具:

bash 复制代码
# Rust 工具链
rustup --version
cargo --version

# C++ 构建工具
cmake --version
ninja --version

Android:

bash 复制代码
rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android
cargo install cargo-ndk

iOS:

bash 复制代码
xcode-select --install
rustup target add aarch64-apple-ios aarch64-apple-ios-sim x86_64-apple-ios

如果你用 Expo,要记住一句话:

有原生 Rust Turbo Module 后,不能再用 Expo Go,需要 development build。

从零搭一个绑定包

通常做法是先创建一个 React Native library,再让 uniffi-bindgen-react-native 接管其中的 C++/TS 绑定。

1. 用 builder-bob 生成脚手架

bash 复制代码
npx create-react-native-library@latest my-rust-lib

交互选项可以这样选:

text 复制代码
Library type: Turbo module
Languages: C++ for Android & iOS
Example app: Vanilla

进入项目:

bash 复制代码
cd my-rust-lib
yarn

为什么要选 C++?因为 uniffi-bindgen-react-native 会生成 C++ JSI 绑定,Turbo Module 壳需要这层。

2. 安装 uniffi-bindgen-react-native

bash 复制代码
yarn add uniffi-bindgen-react-native

或者在 pnpm 项目里:

bash 复制代码
pnpm add uniffi-bindgen-react-native

然后在 package.json 里加脚本:

json 复制代码
{
  "scripts": {
    "ubrn:ios": "ubrn build ios --and-generate && (cd example/ios && pod install)",
    "ubrn:android": "ubrn build android --and-generate",
    "ubrn:web": "ubrn build web",
    "ubrn:checkout": "ubrn checkout",
    "ubrn:clean": "rm -rfv cpp/ android/CMakeLists.txt android/src/main/java android/*.cpp ios/ src/Native* src/index.*ts* src/generated/"
  }
}

ubrnuniffi-bindgen-react-native 的 CLI。

3. 清掉 builder-bob 原模板

builder-bob 会生成一套默认 C++ 示例。接入 Rust 以后,这些通常不需要:

bash 复制代码
yarn ubrn:clean

Windows / pnpm 项目可以按自己习惯改成 del-cli 或 node 脚本。SwarmNote Mobile 里就额外有一个 ubrn:fix 脚本,用来修正生成后的工程细节。

配置 ubrn.config.yaml

项目根目录创建:

yaml 复制代码
---
rust:
  directory: ./rust
  manifestPath: Cargo.toml

bindings:
  cpp: cpp/generated
  ts: src/generated

noOverwrite:
  - "*.podspec"
  - package.json
  - android/build.gradle

如果 Rust 代码来自远程仓库,可以写:

yaml 复制代码
rust:
  repo: https://github.com/user/my-rust-crate.git
  branch: main
  manifestPath: crates/api/Cargo.toml

SwarmNote Mobile 是 monorepo / 本地目录模式:

yaml 复制代码
rust:
  directory: ./rust/mobile-core
  manifestPath: Cargo.toml

bindings:
  cpp: cpp/generated
  ts: src/generated

noOverwrite:
  - android/build.gradle
  - package.json
  - SwarmnoteCore.podspec

manifestPath 指向的 Rust crate 要能编译成 native library。通常在 Cargo.toml 里要有类似:

toml 复制代码
[lib]
crate-type = ["staticlib", "cdylib", "rlib"]

具体保留哪些 crate type,取决于你的 iOS / Android / workspace 配置。经验上,移动端绑定 crate 最好独立出来,不要直接把 uniffi 宏撒进共享 core。

构建和生成

常用命令:

bash 复制代码
# iOS:编译 Rust + 生成绑定 + pod install
yarn ubrn:ios

# Android:编译 Rust + 生成绑定
yarn ubrn:android

# 只拉取远程 Rust 代码
yarn ubrn:checkout

开发时可以只编某个架构,加快速度:

bash 复制代码
ubrn build ios --sim-only --and-generate
ubrn build android --targets aarch64-linux-android --and-generate

生成后你会看到:

text 复制代码
my-rust-lib/
├── cpp/generated/              # C++ JSI 绑定
├── src/generated/              # TypeScript 绑定
├── android/src/main/jniLibs/   # Android .so
└── ios/build/*.xcframework     # iOS framework

SwarmNote Mobile 的绑定包叫 react-native-swarmnote-core,核心脚本是:

json 复制代码
{
  "scripts": {
    "ubrn:android": "ubrn build android --and-generate && pnpm ubrn:fix",
    "ubrn:android:release": "ubrn build android --and-generate --release -t arm64-v8a && pnpm ubrn:fix",
    "ubrn:ios": "ubrn build ios --and-generate && pnpm ubrn:fix",
    "ubrn:ios:release": "ubrn build ios --and-generate --release && pnpm ubrn:fix"
  }
}

写 Rust API

Rust crate 入口先放:

rust 复制代码
uniffi::setup_scaffolding!();

SwarmNote 的 mobile-core/src/lib.rs 大概是:

rust 复制代码
uniffi::setup_scaffolding!();

mod app;
mod error;
mod events;
mod keychain;
mod types;
mod workspace;

下面看常用 API 形态。

1. 导出函数

最简单:

rust 复制代码
#[uniffi::export]
pub fn greet(name: String) -> String {
    format!("Hello, {name}")
}

TS 侧:

ts 复制代码
const message = greet("SwarmNote");

2. Record:传值类型

没有方法、只是数据,用 Record

rust 复制代码
#[derive(Debug, Clone, uniffi::Record)]
pub struct DeviceInfo {
    pub peer_id: String,
    pub device_name: String,
    pub hostname: String,
}

生成到 TS 后字段会转成 camelCase:

ts 复制代码
type DeviceInfo = {
  peerId: string;
  deviceName: string;
  hostname: string;
};

SwarmNote 里会专门做 FFI 侧 DTO,比如:

rust 复制代码
#[derive(Debug, Clone, uniffi::Record)]
pub struct UniffiDeviceInfo {
    pub peer_id: String,
    pub device_name: String,
    pub hostname: String,
    pub os: String,
    pub platform: String,
    pub arch: String,
    pub created_at: String,
}

不要把数据库表、内部状态机字段一股脑暴露给 RN。FFI DTO 应该是 host 需要的 API,而不是 core 内部结构的截图。

3. Object:让 RN 持有 Rust 句柄

真实 app 里更常见的是对象:

rust 复制代码
use std::sync::Arc;

#[derive(uniffi::Object)]
pub struct AppCoreHandle {
    inner: Arc<AppCore>,
}

#[uniffi::export(async_runtime = "tokio")]
impl AppCoreHandle {
    #[uniffi::constructor]
    pub async fn new(app_data_dir: String) -> Result<Arc<Self>, FfiError> {
        let inner = AppCore::new(app_data_dir).await?;
        Ok(Arc::new(Self { inner }))
    }

    pub async fn start_network(&self) -> Result<(), FfiError> {
        self.inner.start_network().await?;
        Ok(())
    }
}

SwarmNote 真实代码里的设备级对象叫 UniffiAppCore

rust 复制代码
#[derive(uniffi::Object)]
pub struct UniffiAppCore {
    pub(crate) inner: Arc<AppCore>,
    pub(crate) event_bus: Arc<UniffiEventBusAdapter>,
}

它负责持有设备身份、配置、P2P 节点、devices DB 等长期状态。RN 启动时创建一次,放进 context/store 里。

生成后的异步 constructor 在 TS 侧通常是 create

ts 复制代码
const appCore = await UniffiAppCore.create(keychain, eventBus, appDataDir);
await appCore.startNetwork();

4. async:Rust Future 变 Promise

Rust:

rust 复制代码
#[uniffi::export(async_runtime = "tokio")]
impl WorkspaceHandle {
    pub async fn read_text(&self, rel_path: String) -> Result<String, FfiError> {
        self.inner.read_text(&rel_path).await.map_err(Into::into)
    }
}

TS:

ts 复制代码
const text = await workspace.readText("daily.md");

如果你的 core 用 Tokio,#[uniffi::export(async_runtime = "tokio")] 很重要。SwarmNote 的 SeaORM、文件 I/O、P2P 任务都在 tokio runtime 里,所以导出 impl 基本都这样标。

uniffi-bindgen-react-native 生成的 async 方法还支持 AbortSignal

ts 复制代码
const controller = new AbortController();
setTimeout(() => controller.abort(), 10_000);

await workspace.readText("daily.md", { signal: controller.signal });

5. Enum:状态和事件

简单状态:

rust 复制代码
#[derive(Debug, Clone, uniffi::Enum)]
pub enum NodeStatus {
    Stopped,
    Running,
    Error { message: String },
}

带数据的 enum 会生成 tagged union 风格。SwarmNote 事件就是这种:

rust 复制代码
#[derive(Debug, Clone, uniffi::Enum)]
pub enum UniffiAppEvent {
    DocFlushed { doc_id: String },
    ExternalUpdate { doc_id: String, update: Vec<u8> },
    FileTreeChanged { workspace_id: String },
    NodeStarted,
    NodeStopped,
    SyncProgress {
        workspace_id: String,
        peer_id: String,
        completed: u32,
        total: u32,
    },
}

一个实战坑:不要随意在 enum 中间插入新 variant。旧 TS 绑定可能按旧 tag 解码,导致事件字段错位。新增事件尽量放末尾。

RN 侧怎么用

生成文件通常在:

text 复制代码
src/generated/

你可以在包的 src/index.ts 里统一 re-export:

ts 复制代码
export * from "./generated/mobile_core";

然后 app 里:

ts 复制代码
import {
  UniffiAppCore,
  type ForeignEventBus,
  type UniffiAppEvent,
} from "react-native-swarmnote-core";

class EventBus implements ForeignEventBus {
  emit(event: UniffiAppEvent): void {
    // 分发到 Zustand / Redux / event emitter
  }
}

const appCore = await UniffiAppCore.create(keychain, new EventBus(), appDataDir);
const info = appCore.deviceInfo();
const workspace = await appCore.openWorkspace(path);

注意:命名一般会从 Rust snake_case 变成 TS camelCase,例如:

rust 复制代码
pub async fn open_workspace(&self, path: String) -> Result<Arc<UniffiWorkspaceCore>, FfiError>

TS:

ts 复制代码
await appCore.openWorkspace(path);

事件:Rust 怎么通知 JS

Tauri 桌面端可以 app.emit()。RN 里没有 Tauri AppHandle,所以用 callback interface / foreign trait。

完整链路是这样的:

sequenceDiagram participant Core as swarmnote-core participant Adapter as UniffiEventBusAdapter participant Trait as ForeignEventBus participant RN as RN EventBus class participant Store as Zustand / Editor bridge Core->>Adapter: EventBus::emit(AppEvent) Adapter->>Adapter: map_event(AppEvent) -> UniffiAppEvent Adapter->>Trait: foreign.emit(UniffiAppEvent) Trait->>RN: JS callback emit(event) RN->>Store: switch event.tag, update stores

1. Rust 先定义一个 JS 要实现的 trait

SwarmNote 的事件桥在 mobile-core/src/events.rs

rust 复制代码
#[uniffi::export(with_foreign)]
pub trait ForeignEventBus: Send + Sync {
    fn emit(&self, event: UniffiAppEvent);
}

with_foreign 的意思是:这个 trait 由外部语言实现。生成到 TypeScript 后,会变成一个接口:

ts 复制代码
export interface ForeignEventBus {
  emit(event: UniffiAppEvent): void;
}

事件 payload 是一个 UniFFI enum:

rust 复制代码
#[derive(Debug, Clone, uniffi::Enum)]
pub enum UniffiAppEvent {
    DevicesChanged { devices: Vec<UniffiDevice> },
    NetworkStatusChanged { nat_status: String, public_addr: Option<String> },
    PairingRequestReceived {
        pending_id: u64,
        peer_id: String,
        os_info: UniffiOsInfo,
        method: UniffiPairingMethod,
        expires_at: SystemTime,
    },
    ExternalUpdate { doc_id: String, update: Vec<u8> },
    NodeStarted,
    NodeStopped,
}

2. constructor 接收这个 callback

UniffiAppCore::new 把 JS 传进来的 event_bus 接住:

rust 复制代码
#[uniffi::export(async_runtime = "tokio")]
impl UniffiAppCore {
    #[uniffi::constructor]
    pub async fn new(
        keychain: Arc<dyn ForeignKeychainProvider>,
        event_bus: Arc<dyn ForeignEventBus>,
        app_data_dir: String,
    ) -> Result<Arc<Self>, FfiError> {
        let event_bus = Arc::new(UniffiEventBusAdapter::new(event_bus));

        let inner = AppCoreBuilder::new(
            Arc::new(UniffiKeychainAdapter::new(keychain)),
            event_bus.clone(),
            app_data_dir,
        )
        .build()
        .await?;

        Ok(Arc::new(Self { inner, event_bus }))
    }
}

这里的关键是:RN 侧传进来的对象不是临时调用一次就结束,而是被 Rust adapter 包起来,交给 AppCoreBuilder。之后 core 里任何地方触发 EventBus::emit(...),都能一路回到 JS。

3. Rust adapter 把 core event 映射成 FFI event

共享 core 不认识 UniFFI,它只认识自己的 EventBus trait。移动端 wrapper 负责做投影:

rust 复制代码
pub(crate) struct UniffiEventBusAdapter {
    foreign: Arc<dyn ForeignEventBus>,
}

impl EventBus for UniffiEventBusAdapter {
    fn emit(&self, event: AppEvent) {
        self.foreign.emit(map_event(event));
    }
}

真实代码里还有一个 map_event(event),把 core 的 AppEvent 转成 UniffiAppEvent。比如 UUID 转成 string,chrono::DateTime 转成 SystemTime,内部设备类型转成 FFI DTO。

4. RN 侧实现 callback class

生成绑定后,RN 侧实现 ForeignEventBus

ts 复制代码
import {
  type ForeignEventBus,
  type UniffiAppEvent,
  UniffiAppEvent_Tags,
} from "react-native-swarmnote-core";

export class EventBus implements ForeignEventBus {
  emit(event: UniffiAppEvent): void {
    switch (event.tag) {
      case UniffiAppEvent_Tags.DevicesChanged:
        useSwarmStore.getState().setDevices(event.inner.devices);
        break;

      case UniffiAppEvent_Tags.NetworkStatusChanged:
        useSwarmStore.getState().setNetworkStatus({
          natStatus: event.inner.natStatus,
          publicAddr: event.inner.publicAddr ?? null,
        });
        break;

      case UniffiAppEvent_Tags.PairingRequestReceived: {
        const { pendingId, peerId, osInfo, method, expiresAt } = event.inner;
        useNotificationStore.getState().push({
          id: `pairing-${pendingId.toString()}-${Date.now()}`,
          type: "pairing-request",
          payload: {
            pendingId,
            peerId,
            deviceName: osInfo.name ?? osInfo.hostname,
            os: osInfo.os,
            platform: osInfo.platform,
            method: method.tag,
            expiresAt,
          },
          timestamp: Date.now(),
        });
        break;
      }

      case UniffiAppEvent_Tags.ExternalUpdate: {
        const active = getActiveEditorBridge();
        if (active === null) break;
        const { docId, update } = event.inner;
        if (active.docUuid !== docId) break;
        active.applyRemoteUpdate(new Uint8Array(update));
        break;
      }
    }
  }
}

这段代码的作用就是把 Rust 事件翻译成 RN 世界里的状态更新:设备列表进 store,配对请求进通知队列,远端 Yjs update 转给 WebView editor bridge。

5. 创建 AppCore 时把 callback 传进去

最后,在 RN 初始化核心对象时,把 callback 实例作为参数传给 Rust constructor:

ts 复制代码
import { Paths } from "expo-file-system";
import { UniffiAppCore, type UniffiAppCoreLike } from "react-native-swarmnote-core";
import { EventBus } from "./event-bus";
import { Keychain } from "./keychain";

let corePromise: Promise<UniffiAppCoreLike> | null = null;
let core: UniffiAppCoreLike | null = null;

export function initAppCore(): Promise<UniffiAppCoreLike> {
  if (corePromise !== null) return corePromise;

  corePromise = (async () => {
    const instance = await UniffiAppCore.create(
      new Keychain(),
      new EventBus(),
      Paths.document.uri,
    );
    core = instance;
    return instance;
  })();

  return corePromise;
}

这就是前端"传 callback"的地方。new EventBus() 实现了生成出来的 ForeignEventBus 接口,UniFFI 会把它降到 native handle,Rust 侧收到的是 Arc<dyn ForeignEventBus>

6. 回调链路小结

把上面的代码串起来就是:

text 复制代码
RN: new EventBus()
  ↓ 作为 constructor 参数
UniffiAppCore::new(event_bus: Arc<dyn ForeignEventBus>)
  ↓ 包成 adapter
AppCoreBuilder::new(..., Arc<dyn EventBus>, ...)
  ↓ core 触发 AppEvent
UniffiEventBusAdapter::emit(AppEvent)
  ↓ map_event
ForeignEventBus.emit(UniffiAppEvent)
  ↓ 回到 JS
EventBus.emit(event)
  ↓
Zustand store / WebView editor bridge

经验规则:

  • callback 里不要做重活,快速转发到 store 后返回。
  • 不要持有 Rust 锁时调用 JS callback,容易死锁。
  • 事件 payload 尽量用 FFI-friendly DTO,不要暴露内部复杂类型。
  • JS callback 实例要和 core 一样长期存活;通常把 UniffiAppCore 做成 singleton,callback 跟着它一起活。
  • 如果事件里有 u64,TS 侧通常是 bigint,UI store 需要 Number(...) 时要确认范围安全。

错误处理

Rust 侧:

rust 复制代码
#[derive(Debug, thiserror::Error, uniffi::Error)]
pub enum FfiError {
    #[error("invalid path: {0}")]
    InvalidPath(String),

    #[error("P2P node is not running")]
    NetworkNotRunning,

    #[error("invalid input ({field}): {reason}")]
    InvalidInput { field: String, reason: String },
}

导出函数返回:

rust 复制代码
pub async fn open_workspace(&self, path: String) -> Result<Arc<UniffiWorkspaceCore>, FfiError>

TS 侧是 rejected Promise。判断错误时,不要用普通 instanceof,用生成类型的 instanceOf()

ts 复制代码
try {
  await appCore.startNetwork();
} catch (e) {
  if (FfiError.NetworkAlreadyRunning.instanceOf(e)) {
    return;
  }
  throw e;
}

SwarmNote 没有直接给共享的 AppError derive uniffi::Error,而是在 mobile wrapper 里单独定义 FfiError,再 impl From<AppError> for FfiError。这样 swarmnote-core 不需要依赖 UniFFI,桌面端也不会被移动端绑定约束污染。

内存和生命周期

UniFFI Object 背后是真实 Rust 资源。JS 对象被 GC 时,Rust 端引用会被释放,但 GC 时机不可控。

轻量对象可以交给 GC;持有文件、数据库、网络、同步任务的对象,建议显式释放:

ts 复制代码
try {
  const workspace = await appCore.openWorkspace(path);
  // ...
  await workspace.close();
  workspace.uniffiDestroy?.();
} finally {
  // 确保切换 workspace 前旧资源释放
}

SwarmNote 的 UniffiWorkspaceCore 就有明确生命周期约定:切换工作区前先 close(),保证 dirty docs flush,再释放句柄。

也可以用 uniffiUse() 做 RAII 风格的临时对象管理:

ts 复制代码
const result = manager.uniffiUse((m) => {
  return m.doSomething();
});

常见类型映射

| Rust | TypeScript |
|-----------------------------|-----------------------|-------------|
| String | string |
| bool | boolean |
| u8 / u32 / i32 | number |
| u64 / i64 | bigint |
| Vec<T> | T[] |
| Vec<u8> | ArrayBuffer |
| Option<T> | `T | undefined` |
| SystemTime | Date |
| #[derive(uniffi::Record)] | plain object |
| #[derive(uniffi::Object)] | object handle / class |
| #[derive(uniffi::Enum)] | enum / tagged union |
| #[derive(uniffi::Error)] | typed thrown error |

这里和 tauri-specta 有个差异:64 位整数通常是 bigint,前端不要随手和 number 混算。

多 crate 项目怎么组织

如果你已经有一个 Rust workspace,不建议直接在核心 crate 上到处加 UniFFI 宏。更推荐这样:

text 复制代码
rust/
├── core/              # 平台无关核心逻辑
├── p2p/               # 网络模块
└── mobile-core/       # UniFFI wrapper crate

mobile-core 负责:

  • 包装 core 的对象和方法
  • 定义 FFI-friendly DTO
  • 把 core error 转成 FfiError
  • 把 core event 转成 UniffiAppEvent
  • 处理移动端 path、keychain、event bus 等 adapter

SwarmNote 就是这种思路:

text 复制代码
TypeScript / RN
  ↓ generated bindings
mobile-core
  ↓ wrapper / adapter
swarmnote-core
  ↓
swarm-p2p-core / yrs / sea-orm

这和桌面端 Tauri 的分层是对称的:桌面 host 用 #[tauri::command] 包 core,移动 host 用 #[uniffi::export] 包 core。

常见坑

处理方式
Expo Go 跑不了 使用 development build
改 Rust 后 TS 没变化 重新跑 ubrn build ... --and-generate
u64 到 TS 变 bigint 前端按 bigint 处理,或在 wrapper 层转成安全 u32 / String
持有资源的 Object 只靠 GC 提供 close(),RN 侧配合 uniffiDestroy()
callback 里做重活 只转发事件,异步处理放到 RN store / queue
core 被 UniFFI 污染 单独建 wrapper crate,不在共享 core 里 derive UniFFI
enum 中间插 variant 新 variant 尽量追加到末尾

适合谁

适合:

  • 已经有 Rust core
  • RN 只是移动端 host
  • 需要数据库、文件系统、加密、P2P、同步协议等重逻辑
  • 想要生成 TypeScript 类型
  • 不想维护 Swift / Kotlin 两套桥

不太适合:

  • 只是普通 UI app
  • 没有 Rust core
  • 系统能力 Expo/RN 库已经完全覆盖
  • 必须使用 Expo Go

小结

uniffi-bindgen-react-native 的主线其实是:

  1. 用 builder-bob 建一个 RN Turbo Module 包。
  2. uniffi-bindgen-react-nativeubrn.config.yaml
  3. 在 Rust wrapper crate 里写 uniffi::setup_scaffolding!()
  4. #[uniffi::export]RecordObjectEnumError 描述 FFI API。
  5. ubrn build android/ios --and-generate 生成 TS + C++ + native library。
  6. RN 侧直接 import 生成 API,像调用普通 TS 对象一样调用 Rust。

如果上一篇的 tauri-specta 是"给 Tauri IPC 生成 TypeScript 契约",那这一篇的 uniffi-bindgen-react-native 就是"给 React Native 生成 Rust 原生模块契约"。

SwarmNote 最终采用的方式是:

text 复制代码
桌面端: Tauri + tauri-specta
移动端: React Native + uniffi-bindgen-react-native
共享核心: swarmnote-core

不是一套壳跑所有端,而是每个平台用适合自己的壳,真正复杂的核心逻辑留在 Rust。

系列文章:

延伸阅读

相关推荐
moshuying3 小时前
AI Coding 最大的 token 黑洞,可能根本不是 prompt
前端
红尘散仙3 小时前
一行 `#[specta::specta]`,让 Tauri IPC 有类型
前端·后端·rust
lichenyang4534 小时前
HarmonyOS HMRouter 接入记录:从普通 Tab Demo 到路由跳转
前端
木斯佳4 小时前
前端八股文面经大全:腾讯WXG暑期前端一面(2026-05-15)·面经深度解析
前端·面试·笔试
canonical_entropy5 小时前
NOP Chaos Flux 架构演变史:从 AMIS 重写到现代低代码运行时
前端·aigc·ai编程
张元清5 小时前
useEffect 之外:专门处理异步、深比较和 SSR 的 Effect Hook
前端·javascript·面试
小小小小宇5 小时前
前端双Token机制无感刷新(二)
前端
XinZong6 小时前
OpenClaw 中最经典的 6 款skill,真正能进工作流的 skills
javascript·后端
zhangxingchao6 小时前
AI Agent 基础问题系统整理:从 LangChain、LangGraph、MCP 到 Agent 架构、记忆、工具调用与评估体系
前端·人工智能·后端