- 首页
- Documentation
- 原生
- 移植到 pi-natives (N-API) — 实践笔记
移植到 pi-natives (N-API) — 实践笔记
这是一份将热路径迁移到 crates/pi-natives 并通过 JS 绑定进行连接的实践指南。它的存在是为了避免同样的错误重复发生。
何时进行移植
Section titled “何时进行移植”当以下任一条件成立时进行移植:
- 热路径运行在渲染循环、频繁的 UI 更新或大批量处理中。
- JS 分配占主导地位(字符串频繁创建销毁、正则回溯、大数组)。
- 你已经有 JS 基准线,可以并排对比两个版本的基准测试。
- 工作是 CPU 密集型或可以在 libuv 线程池上运行的阻塞 I/O。
- 工作是可以在 Tokio 运行时上运行的异步 I/O(例如 shell 执行)。
避免移植依赖于仅 JS 状态或动态导入的代码。N-API 导出应该是纯粹的、数据输入/数据输出。长时间运行的工作应通过 task::blocking(CPU 密集型/阻塞 I/O)或 task::future(异步 I/O)并支持取消。
原生导出的结构
Section titled “原生导出的结构”Rust 端:
- 实现代码位于
crates/pi-natives/src/<module>.rs。如果添加新模块,需在crates/pi-natives/src/lib.rs中注册。 - 使用
#[napi]导出;snake_case 导出会自动转换为 camelCase。仅在真正需要别名/非默认名称时使用显式js_name。结构体使用#[napi(object)]。 - 对于 CPU 密集型或阻塞工作,使用
task::blocking(tag, cancel_token, work)(参见crates/pi-natives/src/task.rs)。对于需要 Tokio 的异步工作(例如 shell 会话),使用task::future(env, tag, work)。当你暴露timeoutMs或AbortSignal时传递CancelToken。
JS 端:
packages/natives/src/bindings.ts包含基础NativeBindings接口。packages/natives/src/<module>/types.ts定义 TS 类型并通过声明合并扩展NativeBindings。packages/natives/src/native.ts导入每个<module>/types.ts文件以激活声明。packages/natives/src/<module>/index.ts封装来自packages/natives/src/native.ts的native绑定。packages/natives/src/native.ts加载插件,validateNative强制检查必需的导出。packages/natives/src/index.ts为packages/*中的调用者重新导出封装。
- 添加 Rust 实现
- 将核心逻辑放在一个普通的 Rust 函数中。
- 如果是新模块,将其添加到
crates/pi-natives/src/lib.rs。 - 使用
#[napi]导出,保持默认的 snake_case -> camelCase 映射一致性。 - 保持签名为拥有所有权且简单的类型:
String、Vec<String>、Uint8Array,或对于大字符串/字节输入使用Either<JsString, Uint8Array>。 - 对于 CPU 密集型或阻塞工作,使用
task::blocking;对于异步工作,使用task::future。传递CancelToken并在长循环中调用heartbeat()。
- 连接 JS 绑定
- 在
packages/natives/src/<module>/types.ts中添加类型和NativeBindings扩展。 - 在
packages/natives/src/native.ts中导入./<module>/types以触发声明合并。 - 在
packages/natives/src/<module>/index.ts中添加调用native的封装。 - 从
packages/natives/src/index.ts重新导出。
- 更新原生验证
- 在
validateNative(packages/natives/src/native.ts)中添加checkFn("newExport")。
- 添加基准测试
- 将基准测试放在所属包旁边(
packages/tui/bench、packages/natives/bench或packages/coding-agent/bench)。 - 在同一次运行中包含 JS 基准线和原生版本。
- 使用
Bun.nanoseconds()和固定的迭代次数。 - 保持基准测试输入小而真实(热路径中实际看到的数据)。
- 构建原生二进制文件
bun --cwd=packages/natives run build- 使用
bun --cwd=packages/natives run build,如果想在测试时获取加载器诊断信息,设置PI_DEV=1。
- 运行基准测试
bun run packages/<pkg>/bench/<bench>.ts(或bun --cwd=packages/natives run bench)
- 决定使用方式
- 如果原生更慢,保留 JS,让原生导出暂不使用。
- 如果原生更快,将调用点切换到原生封装。
痛点及避免方法
Section titled “痛点及避免方法”1) 过期的 pi_natives.node 阻止新导出
Section titled “1) 过期的 pi_natives.node 阻止新导出”加载器优先使用 packages/natives/native 中带平台标签的二进制文件(pi_natives.<platform>-<arch>.node)。PI_DEV=1 现在仅启用加载器诊断信息;它不再切换到单独的开发插件文件名。还有一个回退的 pi_natives.node。编译后的二进制文件会提取到 ~/.xcsh/natives/<version>/pi_natives.<platform>-<arch>.node。如果其中任何一个过期,导出都不会更新。
修复方法: 在重新构建之前删除过期文件。
rm packages/natives/native/pi_natives.linux-x64.noderm packages/natives/native/pi_natives.nodebun --cwd=packages/natives run build如果你正在运行编译后的二进制文件,删除缓存的插件目录:
rm -rf ~/.xcsh/natives/<version>然后验证二进制文件中是否存在该导出:
bun -e 'const tag = `${process.platform}-${process.arch}`; const mod = require(`./packages/natives/native/pi_natives.${tag}.node`); console.log(Object.keys(mod).includes("newExport"));'2) 来自 validateNative 的 “Missing exports” 错误
Section titled “2) 来自 validateNative 的 “Missing exports” 错误”这是好事 — 它防止了静默的不匹配。当你看到:
Native addon missing exports ... Missing: visibleWidth这意味着你的二进制文件过期了,Rust 导出名称(或使用时的显式别名)与 JS 名称不匹配,或者导出根本没有编译进去。修复构建和命名不匹配问题,不要削弱验证。
3) Rust 签名不匹配
Section titled “3) Rust 签名不匹配”保持简单且拥有所有权。String、Vec<String> 和 Uint8Array 可以正常工作。在公共导出中避免使用 &str 等引用。如果需要结构化数据,将其包装在 #[napi(object)] 结构体中。
4) 基准测试错误
Section titled “4) 基准测试错误”- 不要比较不同的输入或分配。
- 保持 JS 和原生使用完全相同的输入数组。
- 在同一个基准测试文件中运行两者以避免偏差。
基准测试模板
Section titled “基准测试模板”const ITERATIONS = 2000;
function bench(name: string, fn: () => void): number { const start = Bun.nanoseconds(); for (let i = 0; i < ITERATIONS; i++) fn(); const elapsed = (Bun.nanoseconds() - start) / 1e6; console.log(`${name}: ${elapsed.toFixed(2)}ms total (${(elapsed / ITERATIONS).toFixed(6)}ms/op)`); return elapsed;}
bench("feature/js", () => { jsImpl(sample);});
bench("feature/native", () => { nativeImpl(sample);});validateNative通过(没有缺失的导出)。NativeBindings在packages/natives/src/<module>/types.ts中已扩展,封装已在packages/natives/src/index.ts中重新导出。Object.keys(require(...))包含你的新导出。- 基准测试数据已记录在 PR/笔记中。
- 仅在原生更快或持平时才更新调用点。
- 如果原生更慢,不要切换。保留导出供将来使用,但 TUI 应该保持在更快的路径上。
- 如果原生更快,切换调用点并保留基准测试以捕获性能回退。