用两个宏打通 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 的边界彻底打通。
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 侧:
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 需要这样写:
rust
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 类型定义。f64 → number,String → string,Vec<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. 回调生命周期管理
LifecycleManager 用 Weak 句柄追踪回调,自动清理已释放的回调,防止内存泄漏。
性能
基于 asyncCalc 单次异步回调的基准测试:
| 指标 | 值 |
|---|---|
| 吞吐量 | 17,439 ops/sec |
| 平均延迟 | 57.34 µs |
对于跨语言 FFI 调用,这个延迟完全在可接受范围内。
和 node-rs 对比
| 特性 | TSFFI.B | node-rs |
|---|---|---|
| 调用方向 | TS ↔ Rust 双向 | TS → Rust 单向 |
| 回调支持 | #[tsffi::callback] 原生支持 |
需手动绑定 ThreadsafeFunction |
| 类型生成 | 自动生成 .d.ts | 需手动维护 |
| 构建配置 | 零配置 | 需配置 napi |
| 异常隔离 | PanicHook 捕获 | 进程崩溃 |
| 预编译二进制 | 全平台 | 部分平台 |
注意:TSFFI.B 是基于 napi-rs 构建的,不是替代 napi-rs,而是在它之上提供双向 FFI 的便利层。如果你只需要 TS→Rust 单向调用,直接用 napi-rs 就够了。
5 分钟快速上手
环境准备
bash
# 需要 Node.js >= 18 和 Rust stable
node -v # v18+
rustc --version # 1.70+
# 安装 CLI
npm install -g @tsffib/cli
创建项目
bash
tsffib init my-project --template=bidirectional
cd my-project
Rust 代码(src/lib.rs)
rust
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)
typescript
const { taskProgressDemo } = require('./index.node')
taskProgressDemo('文件处理', (err, percent, message) => {
if (err) return
console.log(`[回调] ${percent}% - ${message}`)
})
构建运行
bash
npm install
npx napi build --js false
node __tests__/index.ts
输出:
erlang
[回调] 0% - 文件处理 执行进度:0%
[回调] 1% - 文件处理 执行进度:1%
...
[回调] 100% - 文件处理 执行进度:100%
生产级场景示例
AI 推理进度反馈
Rust 调用 ONNX Runtime 做推理,每完成一个 batch 回调 TS 报告置信度和标签:
rust
#[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 读取,避免一次性加载全部数据到内存:
rust
#[napi]
pub fn stream_query(
table: String,
batch_size: u32,
callback: ThreadsafeFunction<(Vec<DbRow>, StreamProgress)>,
) -> Result<()> { ... }
大文件处理
Rust 逐块读取大文件并计算 hash,实时回调进度、速率和 ETA:
rust
#[napi]
pub fn process_file(
file_name: String,
total_mb: u32,
callback: ThreadsafeFunction<FileProgress>,
) -> Result<()> { ... }
Electron 适配
TSFFI.B 支持 Electron,通过 contextBridge 在渲染进程中接收 Rust 回调:
typescript
// preload.ts
const { startMonitor } = require('./index.node')
contextBridge.exposeInMainWorld('tsffib', {
onHeartbeat: (callback) => startMonitor(callback)
})
typescript
// renderer.ts
window.tsffib.onHeartbeat((err, data) => {
updateUI(data) // 渲染进程直接收到 Rust 回调
})
环境诊断
遇到问题?tsffib doctor 一键诊断:
bash
$ 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 同步)
所有检查通过!环境正常。
链接
- 官网:tsffi.org
- GitHub:github.com/itszzl-sudo...
- NPM 主包:
@tsffib/tsffib - NPM CLI:
@tsffib/cli - 教程:tsffi.org/tutorial
如果你在做 Rust + Node.js 的项目,且需要 Rust 主动回调 TS,试试 TSFFI.B。两个宏,零样板,双向打通。