什么是 SenRi FFI?
概述
SenRi FFI(千里 FFI)是一个跨运行时 FFI(外部函数接口)统一抽象库,让你用同一套 API 在 KossJS、Node.js、Bun 和 Deno 四个 JavaScript 运行时上调用原生 C 动态库。
TIP
不同运行时各自提供了不同的 FFI API(KossJS 的 _senri_ffi、Bun 的 Bun.FFI、Deno 的 Deno.dlopen、Node.js 依赖 koffi)。SenRi FFI 在这些底层 API 之上提供一个统一的抽象层,使得编写一次 FFI 代码即可在四个运行时上运行,同时为同步和异步调用提供一致的 API。
开源协议
本项目采用 Apache 2.0 协议开源发布。
问题背景
在 JavaScript 生态中调用原生 C 库,不同运行时的做法完全不同:
| 运行时 | FFI 方式 | 类型名称示例 |
|---|---|---|
| KossJS | globalThis._senri_ffi(内置) | int32 |
| Bun | Bun.FFI(内置,v1.3+) | i32 |
| Deno | Deno.dlopen(内置) | i32 |
| Node.js | koffi(需安装) | 'int32' (v3 字符串类型) |
直接在代码中使用运行时的原生 API 会锁死你的代码到某个特定运行时。SenRi FFI 解决了这个问题。
技术架构
您的应用程序 (JavaScript / TypeScript)
↓
SenRi FFI (统一 API 层)
↓
┌─────────┬─────────┬─────────┬─────────┐
↓ ↓ ↓ ↓ ↓
KossJSAdapter BunAdapter DenoAdapter NodeAdapter
↓ ↓ ↓ ↓
_senri_ffi Bun.FFI Deno.dlopen koffi v3
(内置) (内置) (内置) (可选安装)适配器模式
SenRi FFI 使用适配器模式,通过 FFIAdapter 接口统一各运行时:
- 定义统一的
FFIAdapter接口(14 个方法) - 四个运行时各自实现适配器:
KossJSAdapter— 包装globalThis._senri_ffiBunAdapter— 包装Bun.FFI(Bun 1.3+ 重命名)DenoAdapter— 包装Deno.dlopen,自动转换 pointer/string 类型NodeAdapter— 包装koffi v3(字符串类型映射)
- 启动时通过
detectRuntime()自动选择合适的适配器
核心特性
统一类型系统
使用 types.int32、types.cstring 等统一名称,各适配器自动映射为运行时原生类型:
import { types } from '@tt23xrstudio/senri_ffi';
// 在所有运行时上都一样
types.int32 // KossJS/Bun/Deno/String: 'int32', Bun: 'i32'
types.float64 // KossJS: 'float64', Bun: 'f64', Deno: 'f64', Node: 'float64'内部使用 NormalizedType 标准化类型描述符(primitive/pointer/array/struct),用 serializeType() 生成稳定的缓存键,解决以往 JSON.stringify 对象属性顺序不确定的问题。
结构体 / 指针 / 数组
支持复合类型描述符:
import { pointer, array } from '@tt23xrstudio/senri_ffi';
pointer(types.int32); // 指向 int32 的指针
array(types.uint8, 256); // uint8[256]跨平台异步 FFI(funcAsync)
所有运行时提供统一的异步 API:
const fn = lib.funcAsync('heavy_work', types.int32, [types.int32]);
const result = await fn(42); // 返回 Promise,不阻塞主线程| 运行时 | 异步实现 |
|---|---|
| KossJS | 真实系统线程 (std::thread),支持回调 + AbortController |
| Deno | 原生 nonblocking: true |
| Node.js | 单 Worker 模拟(worker_threads) |
| Bun | 单 Worker 模拟(Bun 的 worker_threads polyfill) |
Pointer.address 现在是 bigint
为支持 64 位地址(特别是 Deno 的 UnsafePointer),Pointer.address 返回类型从 number 改为 bigint:
const ptr = alloc(64);
ptr.address; // → bigint(如 140732311111111n)
ptr.isNull(); // 新增辅助方法
ptr.numberAddress; // 兼容旧代码(超过安全整数抛 RangeError)回调自动管理
通过 FinalizationRegistry 自动释放 C 回调,防止内存泄漏。
统一错误类型
所有运行时抛出统一的 FFIError / FFITypeError,便于统一处理。
支持的运行时
| 运行时 | 版本要求 | FFI 后端 | 同步 | 异步 |
|---|---|---|---|---|
| KossJS | ≥ 0.1.0-dev.6 | _senri_ffi(内置) | ✅ | ✅ 原生 |
| Deno | ≥ 2.0 | Deno.dlopen(内置) | ✅ | ✅ 原生 |
| Bun | ≥ 1.3 | Bun.FFI(内置) | ✅ | ✅ 模拟 |
| Node.js | ≥ 18 | koffi v3(需安装) | ✅ | ✅ 模拟 |
运行时检测优先级
SenRi FFI 按以下优先级自动检测运行时:
- KossJS — 如果
globalThis._senri_ffi为真值 - Bun — 如果
Bun.FFI可用 - Deno — 如果
Deno.dlopen可用 - Node.js — 如果
process.versions.node存在
检测发生在模块首次加载时,之后使用缓存的适配器实例。
下一步:
