On this page

FFI

History
Source Code: lib/ffi.js

稳定性:1 - 实验性

node:ffi 模块为从 JavaScript 加载动态库并调用原生符号提供了一种实验性的外部函数接口(FFI)。

此 API 不安全。传入无效指针、使用错误的符号签名,或在内存被释放后访问其内容,可能导致进程崩溃或破坏内存。

要使用它:

在启用 FFI 支持的构建中,该模块仅在 node: 方案下可用,并且需要通过 --experimental-ffi 标志进行启用。

当前打包的 libffi 支持目标包括:

  • macOS:arm64x64
  • Windows:arm64x64
  • FreeBSD:armarm64x64
  • Linux:armarm64x64

其他目标需要使用 --shared-ffi 构建 Node.js,使其与共享版 libffi 链接。非官方的 GN 构建不支持 node:ffi

在使用[权限模型][]时,除非提供 --allow-ffi 标志,否则将限制 FFI API。

node:ffi 模块提供两组 API:

  • 动态库 API:用于加载库、解析符号以及创建可调用的 JavaScript 包装器。
  • 原始内存辅助工具:通过指针读取和写入基本值,将指针转换为 JavaScript 字符串、Buffer 实例和 ArrayBuffer 实例,并将数据复制回原生内存。

FFI 签名使用字符串类型名称。

支持的类型名称:

  • void
  • i8, int8
  • u8, uint8, bool, char
  • i16, int16
  • u16, uint16
  • i32, int32
  • u32, uint32
  • i64, int64
  • u64, uint64
  • f32, float
  • f64, double
  • pointer, ptr
  • string, str
  • buffer
  • arraybuffer
  • function

这些类型名称也作为常量暴露在 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'

类似指针的类型(pointerstringbufferarraybufferfunction)都会作为指针传递给原生层。

BufferArrayBuffer 或类型化数组值作为类似指针的参数传入时,Node.js 会在原生调用期间借用它们底层内存的一个原始指针。调用方必须确保底层存储在整个调用过程中保持有效且稳定。

在原生调用处于活动状态时(包括通过 FFI 回调等可重入 JavaScript 进行的操作),调整、转移、分离或以其他方式使该底层存储失效都不受支持且是危险的。这样做可能导致进程崩溃、产生不正确的输出,或破坏内存。

char 类型遵循平台 C ABI。在普通 C char 为有符号的那些平台上,它表现得如同 i8;否则表现得如同 u8

bool 类型会以 8 位无符号整数的形式进行封送(marshaled)。传入诸如 01 这类数值;不接受 JavaScript 的 truefalse

函数和回调用签名对象来描述。

支持的字段:

  • resultreturnreturns:用于返回类型。
  • parametersarguments:用于参数类型列表。

在单个签名对象中,只允许存在一个返回类型字段以及一个参数列表字段。

const signature = {
  result: 'i32',
  parameters: ['i32', 'i32'],
};
P

ffi.suffix

History
Attributes

当前平台的原生共享库后缀:

  • macOS:'dylib'
  • 类 Unix 平台:'so'
  • Windows:'dll'

这可用于构建可移植的库路径:

const { suffix } = require('node:ffi');

const path = `libsqlite3.${suffix}`;
M

ffi.dlopen

History
ffi.dlopen(path, definitions?): void
Attributes
动态库的路径,或使用  null 从当前进程映像中解析符号。
definitions:<Object>
要立即解析的符号定义。
返回: <Object>

加载一个动态库并解析所请求的函数定义。

在 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));
M

ffi.dlclose

History
ffi.dlclose(handle): void
  • handle {DynamicLibrary}

关闭一个动态库。

这等价于调用 handle.close()

M

ffi.dlsym

History
ffi.dlsym(handle, symbol): void

从已加载的库中解析符号地址。

这等价于调用 handle.getSymbol(symbol)

类:DynamicLibrary

History

表示一个已加载的动态库。

new DynamicLibrary(path): void
Attributes
动态库的路径,或使用  null 从当前进程映像中解析符号。

加载动态库,但不会急切解析任何函数。

在 Windows 上不支持传入 null

const { DynamicLibrary } = require('node:ffi');

const lib = new DynamicLibrary('./mylib.so');
Attributes

用于加载该库的路径。

Attributes

包含先前已解析函数包装器的对象。

Attributes

包含先前已解析的符号地址(作为 bigint 值)的对象。

library.close(): void

关闭库句柄。

在库关闭之后:

  • 已解析的函数包装器会变为无效。
  • 进一步的符号与函数解析将抛出异常。
  • 已注册的回调将被失效。

关闭库不会使之前导出的回调指针变得安全可复用。Node.js 不会跟踪或撤销已经交给原生代码的回调指针。

如果原生代码在 library.close() 之后或在 library.unregisterCallback(pointer) 之后仍持有回调指针,那么调用该指针将产生未定义行为,不被允许且很危险:它可能导致进程崩溃、产生不正确的输出,或破坏内存。原生代码必须在库关闭之前,或在回调被取消注册之前停止使用回调地址。

从库的某个活动回调内部调用 library.close() 不受支持且危险。回调必须在库被关闭之前返回。

library.getFunction(name, signature): void
Attributes
signature:<Object>
返回: <Function>

解析一个符号并返回一个可调用的 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
Attributes
definitions:<Object>
返回: <Object>

当提供 definitions 时,会解析每个具名符号并返回一个包含可调用包装器的对象。

当省略 definitions 时,会为该库中已经解析过的所有函数返回包装器。

library.getSymbol(name): void
Attributes
返回: <bigint>

解析一个符号,并将其原生地址作为 bigint 返回。

library.getSymbols(): void

返回一个包含所有先前已解析符号地址的对象。

library.registerCallback(signature?,  callback): void
Attributes
signature:<Object>
callback:<Function>
返回: <bigint>

创建一个由 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
Attributes
pointer:<bigint>

释放一个先前由 library.registerCallback() 创建的回调。

对当前正在执行的回调调用 library.unregisterCallback(pointer) 不受支持且危险。回调必须在被注销前返回。

library.unregisterCallback(pointer) 返回之后,从原生代码调用该回调指针将产生未定义行为,不被允许且很危险:它可能导致进程崩溃、产生不正确的输出,或破坏内存。

library.refCallback(pointer): void
Attributes
pointer:<bigint>

强引用由 JavaScript 持有的回调。

library.unrefCallback(pointer): void
Attributes
pointer:<bigint>

允许该回调被 JavaScript 以弱引用的方式持有。

如果该回调函数随后被垃圾回收,那么后续的原生调用将变成空操作(no-op)。在返回给原生代码之前,非 void 的返回值会被初始化为零。

参数转换取决于声明的 FFI 类型。

对于 8 位、16 位和 32 位整数类型以及浮点类型,请传入与声明类型匹配的 JavaScript number 值。

对于 64 位整数类型(i64u64),请传入 JavaScript bigint 值。

对于类似指针的参数:

  • nullundefined 会作为空指针传入。
  • 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 不会跟踪 所有权、边界或生命周期。传入无效指针、使用错误的偏移,或通过已经过期的指针写入, 都可能破坏内存或导致进程崩溃。

M

ffi.toString

History
ffi.toString(pointer): void
Attributes
pointer:<bigint>
返回: <string> | <null>

从原生内存读取一个 NUL-终止的 UTF-8 字符串。

如果 pointer0n,则返回 null

此函数不会验证 pointer 是否指向可读取的内存,也不会验证其指向的数据是否以 \0 终止。 传入无效指针、指向已释放内存的指针,或指向不包含终止 NUL 字节的字节序列, 可能读取到无关的内存、导致进程崩溃,或产生截断/乱码的输出。

const { toString } = require('node:ffi');

const value = toString(ptr);
M

ffi.toBuffer

History
ffi.toBuffer(pointer, length, copy?): void
Attributes
pointer:<bigint>
length:<number>
当  false 时,创建零拷贝视图。 默认: true
返回: {Buffer}

从原生内存创建一个 Buffer

copytrue 时,返回的 Buffer 拥有其自身拷贝的内存。 当 copyfalse 时,返回的 Buffer 将直接引用原始的原生内存。

使用 copy: false 是一个零拷贝“逃生通道”。返回的 Buffer 是对外部内存的可写视图, 因此在 JavaScript 中的写入会直接更新原始的原生内存。 调用者必须保证:

  • pointer 在返回的 Buffer 的整个生命周期内保持有效。
  • length 保持在已分配的原生内存区域范围内。
  • 当 JavaScript 仍在使用该 Buffer 时,没有原生代码释放或重新利用该内存。

如果无法满足这些保证,读取或写入 Buffer 可能会破坏内存或导致进程崩溃。

M

ffi.toArrayBuffer

History
ffi.toArrayBuffer(pointer, length, copy?): void
Attributes
pointer:<bigint>
length:<number>
当  false 时,创建零拷贝视图。 默认: true

从原生内存创建一个 ArrayBuffer

copytrue 时,返回的 ArrayBuffer 包含已拷贝的字节。 当 copyfalse 时,返回的 ArrayBuffer 直接引用原始的原生内存。

此处同样适用为 ffi.toBuffer(pointer, length, copy) 描述的生命周期和边界要求。 当 copy: false 时,返回的 ArrayBuffer 是对外部内存的零拷贝视图, 且仅在该内存保持已分配、布局不变且对整个暴露范围都有效时才是安全的。

M

ffi.exportString

History
ffi.exportString(string, pointer, length, encoding?): void
Attributes
string:<string>
pointer:<bigint>
length:<number>
encoding:<string>
默认: 'utf8'

将一个 JavaScript 字符串复制到原生内存,并追加一个结尾 NUL 终止符。

length 必须足以容纳完整的已编码字符串以及后续的 NUL 终止符。 对于 UTF-16 和 UCS-2 编码,结尾终止符使用两个零字节。

pointer 必须指向可写的原生内存,并且可用存储至少为 length 字节。 此函数不会自行分配内存。

string 必须是一个 JavaScript 字符串。encoding 必须是一个字符串。

M

ffi.exportBuffer

History
ffi.exportBuffer(buffer, pointer, length): void

将字节从一个 Buffer 复制到原生内存。

length 至少必须等于 buffer.length

pointer 必须指向可写的原生内存,并且可用存储至少为 length 字节。 此函数不会自行分配内存。

buffer 必须是一个 Node.js Buffer

M

ffi.exportArrayBuffer

History
ffi.exportArrayBuffer(arrayBuffer, pointer, length): void
Attributes
arrayBuffer:<ArrayBuffer>
pointer:<bigint>
length:<number>

将字节从一个 ArrayBuffer 复制到原生内存。

length 至少必须等于 arrayBuffer.byteLength

pointer 必须指向可写的原生内存,并且可用存储至少为 length 字节。 此函数不会自行分配内存。

M

ffi.exportArrayBufferView

History
ffi.exportArrayBufferView(arrayBufferView, pointer, length): void

将字节从一个 ArrayBufferView 复制到原生内存。

length 至少必须等于 arrayBufferView.byteLength

pointer 必须指向可写的原生内存,并且可用存储至少为 length 字节。 此函数不会自行分配内存。

M

ffi.getRawPointer

History
ffi.getRawPointer(source): void
Attributes
返回: <bigint>

返回由 JavaScript 管理的字节存储的原始内存地址。

这不安全且危险。如果底层内存被分离、重新调整大小、传递到别处或以其他方式失效,返回的指针可能会变得无效。 使用过期指针可能导致内存损坏或进程崩溃。

node:ffi 模块不会跟踪指针有效性、内存所有权或原生对象的生命周期。

特别是:

  • 不要从已释放的内存中读取或写入。
  • 原生内存释放后,不要在零拷贝视图上继续使用。
  • 不要为原生符号声明不正确的签名。
  • 当原生代码可能仍会调用回调时,不要取消注册回调。
  • 不要在调用 library.close() 后,或在调用 library.unregisterCallback(pointer) 后再调用回调指针。
  • 不要假设未定义的回调行为不会崩溃进程、产生错误输出或破坏内存。
  • 不要假设指针返回值意味着所有权;调用者是否必须释放该返回地址完全取决于原生 API。

作为一般规则,除非必须要零拷贝访问,否则请优先使用拷贝值,并在原生侧保持回调和指针生命周期的显式管理。