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

用两个宏打通 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 类型定义。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.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 同步)

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

链接


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

相关推荐
蜡台2 小时前
Vue2 使用 typescript 教程
前端·vue.js·typescript
abigale033 小时前
LangChain 多轮对话记忆:基于 session_id 实现多会话隔离
typescript·langchain·uuid·session_id
GISHUB3 小时前
Express + TypeScript + ESM 后端服务搭建教程
javascript·typescript·express
OpenTiny社区18 小时前
操作ArkTS页面跳转及路由相关心得
前端·typescript·web·opentiny
柠檬の夏季18 小时前
TypeScript入门
typescript
万物皆对象66618 小时前
切换路由时页面空白问题(vue3)
前端·vue.js·typescript
突然好热18 小时前
TS 调试技巧
前端·javascript·typescript
晓杰'1 天前
从0到1实现Balatro游戏后端(5):得分计算与单局结算流程实现
后端·typescript·node.js·游戏开发·项目实战·nestjs·webscoket
追光者♂1 天前
【测评系列3】CSDN AI数字营销实测体验官:测试 开源项目——Superpowers 游戏引擎从零开发实战指南
人工智能·深度学习·机器学习·typescript·开源·游戏引擎·superpowers