FFI
History
稳定性:1 - 实验性
node:ffi 模块为从 JavaScript 加载动态库并调用原生符号提供了一种实验性的外部函数接口(FFI)。
此 API 不安全。传入无效指针、使用错误的符号签名,或在内存被释放后访问其内容,可能导致进程崩溃或破坏内存。
要使用它:
import ffi from 'node:ffi';在启用 FFI 支持的构建中,该模块仅在 node: 方案下可用,并且需要通过 --experimental-ffi 标志进行启用。
当前打包的 libffi 支持目标包括:
- macOS:
arm64和x64 - Windows:
arm64和x64 - FreeBSD:
arm、arm64和x64 - Linux:
arm、arm64和x64
其他目标需要使用 --shared-ffi 构建 Node.js,使其与共享版 libffi 链接。非官方的 GN 构建不支持 node:ffi。
在使用[权限模型][]时,除非提供 --allow-ffi 标志,否则将限制 FFI API。
node:ffi 模块提供两组 API:
- 动态库 API:用于加载库、解析符号以及创建可调用的 JavaScript 包装器。
- 原始内存辅助工具:通过指针读取和写入基本值,将指针转换为 JavaScript 字符串、
Buffer实例和ArrayBuffer实例,并将数据复制回原生内存。
FFI 签名使用字符串类型名称。
支持的类型名称:
voidi8,int8u8,uint8,bool,chari16,int16u16,uint16i32,int32u32,uint32i64,int64u64,uint64f32,floatf64,doublepointer,ptrstring,strbufferarraybufferfunction
这些类型名称也作为常量暴露在 ffi.types 上:
ffi.types.VOID='void'ffi.types.POINTER='pointer'ffi.types.BUFFER='buffer'ffi.types.ARRAY_BUFFER='arraybuffer'ffi.types.FUNCTION='function'ffi.types.BOOL='bool'ffi.types.CHAR='char'ffi.types.STRING='string'ffi.types.FLOAT='float'ffi.types.DOUBLE='double'ffi.types.INT_8='int8'ffi.types.UINT_8='uint8'ffi.types.INT_16='int16'ffi.types.UINT_16='uint16'ffi.types.INT_32='int32'ffi.types.UINT_32='uint32'ffi.types.INT_64='int64'ffi.types.UINT_64='uint64'ffi.types.FLOAT_32='float32'ffi.types.FLOAT_64='float64'
类似指针的类型(pointer、string、buffer、arraybuffer 和 function)都会作为指针传递给原生层。
当 Buffer、ArrayBuffer 或类型化数组值作为类似指针的参数传入时,Node.js 会在原生调用期间借用它们底层内存的一个原始指针。调用方必须确保底层存储在整个调用过程中保持有效且稳定。
在原生调用处于活动状态时(包括通过 FFI 回调等可重入 JavaScript 进行的操作),调整、转移、分离或以其他方式使该底层存储失效都不受支持且是危险的。这样做可能导致进程崩溃、产生不正确的输出,或破坏内存。
char 类型遵循平台 C ABI。在普通 C char 为有符号的那些平台上,它表现得如同 i8;否则表现得如同 u8。
bool 类型会以 8 位无符号整数的形式进行封送(marshaled)。传入诸如 0 和 1 这类数值;不接受 JavaScript 的 true 和 false。
函数和回调用签名对象来描述。
支持的字段:
result、return或returns:用于返回类型。parameters或arguments:用于参数类型列表。
在单个签名对象中,只允许存在一个返回类型字段以及一个参数列表字段。
const signature = {
result: 'i32',
parameters: ['i32', 'i32'],
};当前平台的原生共享库后缀:
- macOS:
'dylib' - 类 Unix 平台:
'so' - Windows:
'dll'
这可用于构建可移植的库路径:
const { suffix } = require('node:ffi');
const path = `libsqlite3.${suffix}`;ffi.dlopen(path, definitions?): void加载一个动态库并解析所请求的函数定义。
在 Windows 上不支持传入 null。
当省略 definitions 时,在显式解析符号之前,functions 会作为空对象返回。
返回的对象包含:
lib{DynamicLibrary}:已加载的库句柄。functions<Object>:所请求符号的可调用包装器。
import { dlopen } from 'node:ffi';
const { lib, functions } = dlopen('./mylib.so', {
add_i32: { parameters: ['i32', 'i32'], result: 'i32' },
string_length: { parameters: ['pointer'], result: 'u64' },
});
console.log(functions.add_i32(20, 22));ffi.dlclose(handle): voidhandle{DynamicLibrary}
关闭一个动态库。
这等价于调用 handle.close()。
ffi.dlsym(handle, symbol): void从已加载的库中解析符号地址。
这等价于调用 handle.getSymbol(symbol)。
类:DynamicLibrary
History
表示一个已加载的动态库。
new DynamicLibrary(path): void加载动态库,但不会急切解析任何函数。
在 Windows 上不支持传入 null。
const { DynamicLibrary } = require('node:ffi');
const lib = new DynamicLibrary('./mylib.so');用于加载该库的路径。
包含先前已解析函数包装器的对象。
包含先前已解析的符号地址(作为 bigint 值)的对象。
library.close(): void关闭库句柄。
在库关闭之后:
- 已解析的函数包装器会变为无效。
- 进一步的符号与函数解析将抛出异常。
- 已注册的回调将被失效。
关闭库不会使之前导出的回调指针变得安全可复用。Node.js 不会跟踪或撤销已经交给原生代码的回调指针。
如果原生代码在 library.close() 之后或在 library.unregisterCallback(pointer) 之后仍持有回调指针,那么调用该指针将产生未定义行为,不被允许且很危险:它可能导致进程崩溃、产生不正确的输出,或破坏内存。原生代码必须在库关闭之前,或在回调被取消注册之前停止使用回调地址。
从库的某个活动回调内部调用 library.close() 不受支持且危险。回调必须在库被关闭之前返回。
library.getFunction(name, signature): void解析一个符号并返回一个可调用的 JavaScript 包装器。
返回的函数具有一个 .pointer 属性,其中包含原生函数地址(作为 bigint)。
如果同一个符号已经被解析过,随后使用不同的签名再次请求它将抛出异常。
const { DynamicLibrary } = require('node:ffi');
const lib = new DynamicLibrary('./mylib.so');
const add = lib.getFunction('add_i32', {
parameters: ['i32', 'i32'],
result: 'i32',
});
console.log(add(20, 22));
console.log(add.pointer);library.getFunctions(definitions?): void当提供 definitions 时,会解析每个具名符号并返回一个包含可调用包装器的对象。
当省略 definitions 时,会为该库中已经解析过的所有函数返回包装器。
library.getSymbol(name): void解析一个符号,并将其原生地址作为 bigint 返回。
library.getSymbols(): void- 返回:
<Object>
返回一个包含所有先前已解析符号地址的对象。
library.registerCallback(signature?, callback): void创建一个由 JavaScript 函数支撑的原生回调指针。
当省略 signature 时,回调使用默认的 void () 签名。
返回值是回调指针地址(作为 bigint)。它可以传递给期望回调指针的原生函数。
const { DynamicLibrary } = require('node:ffi');
const lib = new DynamicLibrary('./mylib.so');
const callback = lib.registerCallback(
{ parameters: ['i32'], result: 'i32' },
(value) => value * 2,
);回调受以下限制:
- 必须在创建它们的同一系统线程上被调用。
- 必须不能抛出异常。
- 必须不能返回 Promise。
- 必须返回一个与声明的结果类型兼容的值。
- 在运行时不得对其所属库调用
library.close()。 - 在运行时不得取消自身的注册。
在回调内部关闭其所属库,或在回调执行时从内部对当前执行的回调进行注销,不受支持且危险。这样做可能导致进程崩溃、产生不正确的输出,或破坏内存。
library.unregisterCallback(pointer): void<bigint>释放一个先前由 library.registerCallback() 创建的回调。
对当前正在执行的回调调用 library.unregisterCallback(pointer) 不受支持且危险。回调必须在被注销前返回。
在 library.unregisterCallback(pointer) 返回之后,从原生代码调用该回调指针将产生未定义行为,不被允许且很危险:它可能导致进程崩溃、产生不正确的输出,或破坏内存。
library.refCallback(pointer): void<bigint>强引用由 JavaScript 持有的回调。
library.unrefCallback(pointer): void<bigint>允许该回调被 JavaScript 以弱引用的方式持有。
如果该回调函数随后被垃圾回收,那么后续的原生调用将变成空操作(no-op)。在返回给原生代码之前,非 void 的返回值会被初始化为零。
参数转换取决于声明的 FFI 类型。
对于 8 位、16 位和 32 位整数类型以及浮点类型,请传入与声明类型匹配的
JavaScript number 值。
对于 64 位整数类型(i64 和 u64),请传入 JavaScript bigint 值。
对于类似指针的参数:
null和undefined会作为空指针传入。string值会被复制为临时的 NUL 终止 UTF-8 字符串,持续时间仅为该调用期间。Buffer、类型化数组(typed arrays)以及DataView实例会传入指向其底层内存的指针。ArrayBuffer会传入指向其底层内存的指针。bigint值会以原始指针地址的形式传入。
指针返回值将以 bigint 地址形式暴露。
以下辅助函数在原生指针处读取和写入原语值,且可以选择带字节偏移:
ffi.getInt8(pointer[, offset])ffi.getUint8(pointer[, offset])ffi.getInt16(pointer[, offset])ffi.getUint16(pointer[, offset])ffi.getInt32(pointer[, offset])ffi.getUint32(pointer[, offset])ffi.getInt64(pointer[, offset])ffi.getUint64(pointer[, offset])ffi.getFloat32(pointer[, offset])ffi.getFloat64(pointer[, offset])ffi.setInt8(pointer, offset, value)ffi.setUint8(pointer, offset, value)ffi.setInt16(pointer, offset, value)ffi.setUint16(pointer, offset, value)ffi.setInt32(pointer, offset, value)ffi.setUint32(pointer, offset, value)ffi.setInt64(pointer, offset, value)ffi.setUint64(pointer, offset, value)ffi.setFloat32(pointer, offset, value)ffi.setFloat64(pointer, offset, value)
这些辅助函数执行直接的内存读取和写入。pointer 必须是一个
bigint,指向有效的可读或可写的原生内存。若提供 offset,则将其解释为相对于 pointer 的字节偏移。
用于读取的辅助函数会对 8 位、16 位和 32 位整数类型以及浮点类型返回 JavaScript 的
number 值。对于 64 位整数类型,它们返回 bigint 值。
用于写入的辅助函数需要显式的字节偏移,并在写入内存之前将提供的 JavaScript 值
与目标原生类型进行校验。
对于 setInt64() 和 setUint64(),直接接受 bigint 值;
数值输入必须是 JavaScript 安全整数范围内的整数。
const {
getInt32,
setInt32,
} = require('node:ffi');
setInt32(ptr, 0, 42);
console.log(getInt32(ptr, 0));与本模块中的其他“原始内存”辅助函数一样,这些 API 不会跟踪 所有权、边界或生命周期。传入无效指针、使用错误的偏移,或通过已经过期的指针写入, 都可能破坏内存或导致进程崩溃。
ffi.toString(pointer): void从原生内存读取一个 NUL-终止的 UTF-8 字符串。
如果 pointer 为 0n,则返回 null。
此函数不会验证 pointer 是否指向可读取的内存,也不会验证其指向的数据是否以 \0 终止。
传入无效指针、指向已释放内存的指针,或指向不包含终止 NUL 字节的字节序列,
可能读取到无关的内存、导致进程崩溃,或产生截断/乱码的输出。
const { toString } = require('node:ffi');
const value = toString(ptr);ffi.toBuffer(pointer, length, copy?): void从原生内存创建一个 Buffer。
当 copy 为 true 时,返回的 Buffer 拥有其自身拷贝的内存。
当 copy 为 false 时,返回的 Buffer 将直接引用原始的原生内存。
使用 copy: false 是一个零拷贝“逃生通道”。返回的 Buffer 是对外部内存的可写视图,
因此在 JavaScript 中的写入会直接更新原始的原生内存。
调用者必须保证:
pointer在返回的Buffer的整个生命周期内保持有效。length保持在已分配的原生内存区域范围内。- 当 JavaScript 仍在使用该
Buffer时,没有原生代码释放或重新利用该内存。
如果无法满足这些保证,读取或写入 Buffer 可能会破坏内存或导致进程崩溃。
ffi.toArrayBuffer(pointer, length, copy?): void从原生内存创建一个 ArrayBuffer。
当 copy 为 true 时,返回的 ArrayBuffer 包含已拷贝的字节。
当 copy 为 false 时,返回的 ArrayBuffer 直接引用原始的原生内存。
此处同样适用为
ffi.toBuffer(pointer, length, copy) 描述的生命周期和边界要求。
当 copy: false 时,返回的 ArrayBuffer 是对外部内存的零拷贝视图,
且仅在该内存保持已分配、布局不变且对整个暴露范围都有效时才是安全的。
ffi.exportString(string, pointer, length, encoding?): void将一个 JavaScript 字符串复制到原生内存,并追加一个结尾 NUL 终止符。
length 必须足以容纳完整的已编码字符串以及后续的 NUL 终止符。
对于 UTF-16 和 UCS-2 编码,结尾终止符使用两个零字节。
pointer 必须指向可写的原生内存,并且可用存储至少为 length 字节。
此函数不会自行分配内存。
string 必须是一个 JavaScript 字符串。encoding 必须是一个字符串。
ffi.exportBuffer(buffer, pointer, length): void将字节从一个 Buffer 复制到原生内存。
length 至少必须等于 buffer.length。
pointer 必须指向可写的原生内存,并且可用存储至少为 length 字节。
此函数不会自行分配内存。
buffer 必须是一个 Node.js Buffer。
ffi.exportArrayBuffer(arrayBuffer, pointer, length): void将字节从一个 ArrayBuffer 复制到原生内存。
length 至少必须等于 arrayBuffer.byteLength。
pointer 必须指向可写的原生内存,并且可用存储至少为 length 字节。
此函数不会自行分配内存。
ffi.exportArrayBufferView(arrayBufferView, pointer, length): void将字节从一个 ArrayBufferView 复制到原生内存。
length 至少必须等于 arrayBufferView.byteLength。
pointer 必须指向可写的原生内存,并且可用存储至少为 length 字节。
此函数不会自行分配内存。
ffi.getRawPointer(source): void<ArrayBuffer><bigint>返回由 JavaScript 管理的字节存储的原始内存地址。
这不安全且危险。如果底层内存被分离、重新调整大小、传递到别处或以其他方式失效,返回的指针可能会变得无效。 使用过期指针可能导致内存损坏或进程崩溃。
node:ffi 模块不会跟踪指针有效性、内存所有权或原生对象的生命周期。
特别是:
- 不要从已释放的内存中读取或写入。
- 原生内存释放后,不要在零拷贝视图上继续使用。
- 不要为原生符号声明不正确的签名。
- 当原生代码可能仍会调用回调时,不要取消注册回调。
- 不要在调用
library.close()后,或在调用library.unregisterCallback(pointer)后再调用回调指针。 - 不要假设未定义的回调行为不会崩溃进程、产生错误输出或破坏内存。
- 不要假设指针返回值意味着所有权;调用者是否必须释放该返回地址完全取决于原生 API。
作为一般规则,除非必须要零拷贝访问,否则请优先使用拷贝值,并在原生侧保持回调和指针生命周期的显式管理。