致敬napi-rs,我用两个宏打通 TypeScript ↔ Rust:TSFFI.B 双向 FFI 框架

0 阅读5分钟

用两个宏打通 TypeScript ↔ Rust:TSFFI.B 双向 FFI 框架

背景

Rust 在 Node.js 生态中的使用越来越广泛,napi-rs 已经成为事实标准。但有一个痛点始终没解决:Rust 无法主动回调 TypeScript

现有的 napi-rs 方案是单向的——TS 调 Rust 没问题,但如果你想在 Rust 后台线程里实时推送进度、流式返回数据、或者常驻监听系统事件,就需要手写大量 ThreadsafeFunction 样板代码,动辄 40+ 行,而且极易出错。

TSFFI.B 就是来解决这个问题的。

什么是 TSFFI.B

TSFFI.B 是基于 napi-rs 的双向 FFI 框架,核心思路很简单:

用两个宏,把 TypeScript 和 Rust 的边界彻底打通。

#[tsffi::callback]
fn on_progress(pct: f64, msg: String) -> bool;

#[tsffi::export]
fn start_task(name: String, callback: OnProgress) -> Result<()> {
    std::thread::spawn(move || {
        for i in 0..=100 {
            callback.call((i as f64, format!("{} 进度: {}%", name, i)));
            std::thread::sleep(Duration::from_millis(50));
        }
    });
    Ok(())
}

TypeScript 侧:

import { startTask } from '@tsffib/tsffib'

startTask('文件处理', (err, pct, msg) => {
  console.log(`${pct}% - ${msg}`)
})

就这些。Rust 后台线程每 50ms 回调一次 TS,进度从 0% 到 100%,不需要手写任何 TSFN 绑定代码。

和手写 napi-rs 对比

同样的功能,纯 napi-rs v2 需要这样写:

use napi::bindgen_prelude::*;
use napi::threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode};
use napi_derive::napi;

#[napi]
pub fn start_task(
    name: String,
    callback: ThreadsafeFunction<(f64, String)>,
) -> Result<()> {
    let tsfn = callback.clone();
    std::thread::spawn(move || {
        for i in 0..=100 {
            let status = tsfn.call(
                Ok((i as f64, format!("{} 进度: {}%", name, i))),
                ThreadsafeFunctionCallMode::Blocking,
            );
            if status.is_err() {
                eprintln!("回调失败:JS 上下文可能已销毁");
                break;
            }
            std::thread::sleep(std::time::Duration::from_millis(50));
        }
    });
    Ok(())
}

看起来也不算太长?但这是最简单的情况。一旦涉及:

  • 多个回调类型 → 每个都要手写 TSFN 泛型参数
  • 结构体回调 → 需要 #[napi(object)] + 手动对齐 TS 类型
  • 异常隔离 → 需要自己写 catch_unwind + set_hook
  • 生命周期管理 → 需要手动 Arc<Weak<>> 追踪回调句柄
  • 类型声明 → 需要手写 .d.ts 并保持同步

这些加起来,一个生产级双向回调模块轻松 200+ 行。用 TSFFI.B,5 行。

核心特性

1. 双向 FFI

TS 调 Rust,Rust 也能回调 TS。真正的双向互调,不是单向 + 轮询模拟。

2. 极简注解

#[tsffi::callback] 定义回调类型,#[tsffi::export] 导出函数,#[tsffi::struct] 定义双向传递的结构体。零样板代码。

3. 自动类型生成

Rust 类型自动映射为 TypeScript 类型定义。f64numberStringstringVec<u8>number[],自定义 struct → interface。不需要手写 .d.ts

4. 异常隔离

Rust 子线程的 panic 不会崩溃 Node.js 进程。PanicHook 捕获异常,转换为 JS Error 透传到 TS 侧。

5. 全平台预编译

预编译 Windows / macOS / Linux 原生二进制,npm install @tsffib/tsffib 即用,不需要本地 Rust 环境。

6. 回调生命周期管理

LifecycleManagerWeak 句柄追踪回调,自动清理已释放的回调,防止内存泄漏。

性能

基于 asyncCalc 单次异步回调的基准测试:

指标
吞吐量17,439 ops/sec
平均延迟57.34 µs

对于跨语言 FFI 调用,这个延迟完全在可接受范围内。

和 node-rs 对比

特性TSFFI.Bnode-rs
调用方向TS ↔ Rust 双向TS → Rust 单向
回调支持#[tsffi::callback] 原生支持需手动绑定 ThreadsafeFunction
类型生成自动生成 .d.ts需手动维护
构建配置零配置需配置 napi
异常隔离PanicHook 捕获进程崩溃
预编译二进制全平台部分平台

注意:TSFFI.B 是基于 napi-rs 构建的,不是替代 napi-rs,而是在它之上提供双向 FFI 的便利层。如果你只需要 TS→Rust 单向调用,直接用 napi-rs 就够了。

5 分钟快速上手

环境准备

# 需要 Node.js >= 18 和 Rust stable
node -v   # v18+
rustc --version  # 1.70+

# 安装 CLI
npm install -g @tsffib/cli

创建项目

tsffib init my-project --template=bidirectional
cd my-project

Rust 代码(src/lib.rs)

use napi::bindgen_prelude::*;
use napi::threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode};
use napi_derive::napi;

#[napi]
pub fn task_progress_demo(
    task_name: String,
    callback: ThreadsafeFunction<(f64, String)>,
) -> Result<()> {
    let tsfn = callback;
    std::thread::spawn(move || {
        for i in 0..=100 {
            tsfn.call(
                Ok((i as f64, format!("{} 执行进度:{}%", task_name, i))),
                ThreadsafeFunctionCallMode::Blocking,
            );
            std::thread::sleep(std::time::Duration::from_millis(50));
        }
    });
    Ok(())
}

TypeScript 代码(tests/index.ts)

const { taskProgressDemo } = require('./index.node')

taskProgressDemo('文件处理', (err, percent, message) => {
  if (err) return
  console.log(`[回调] ${percent}% - ${message}`)
})

构建运行

npm install
npx napi build --js false
node __tests__/index.ts

输出:

[回调] 0% - 文件处理 执行进度:0%
[回调] 1% - 文件处理 执行进度:1%
...
[回调] 100% - 文件处理 执行进度:100%

生产级场景示例

AI 推理进度反馈

Rust 调用 ONNX Runtime 做推理,每完成一个 batch 回调 TS 报告置信度和标签:

#[napi(object)]
pub struct InferenceResult {
    pub model_name: String,
    pub batch: u32,
    pub total_batches: u32,
    pub confidence: f64,
    pub label: String,
}

#[napi]
pub fn run_inference(
    model: String,
    batches: u32,
    callback: ThreadsafeFunction<InferenceResult>,
) -> Result<()> { ... }

数据库流式读取

Rust 逐批从 SQLite 读取,避免一次性加载全部数据到内存:

#[napi]
pub fn stream_query(
    table: String,
    batch_size: u32,
    callback: ThreadsafeFunction<(Vec<DbRow>, StreamProgress)>,
) -> Result<()> { ... }

大文件处理

Rust 逐块读取大文件并计算 hash,实时回调进度、速率和 ETA:

#[napi]
pub fn process_file(
    file_name: String,
    total_mb: u32,
    callback: ThreadsafeFunction<FileProgress>,
) -> Result<()> { ... }

Electron 适配

TSFFI.B 支持 Electron,通过 contextBridge 在渲染进程中接收 Rust 回调:

// preload.ts
const { startMonitor } = require('./index.node')

contextBridge.exposeInMainWorld('tsffib', {
  onHeartbeat: (callback) => startMonitor(callback)
})
// renderer.ts
window.tsffib.onHeartbeat((err, data) => {
  updateUI(data)  // 渲染进程直接收到 Rust 回调
})

环境诊断

遇到问题?tsffib doctor 一键诊断:

$ tsffib doctor

TSFFIB Doctor - 环境诊断

  ✓ Node.js v20.11.0 (>= 18)
  ✓ Rust rustc 1.95.0
  ✓ @napi-rs/cli 2.18.0
  ✓ Cargo.toml napi 依赖已配置
  ✓ .node 文件 index.node (5 分钟前编译)
  ✓ .d.ts 文件 index.d.ts(与 .node 同步)

所有检查通过!环境正常。

链接


如果你在做 Rust + Node.js 的项目,且需要 Rust 主动回调 TS,试试 TSFFI.B。两个宏,零样板,双向打通。