外部函数接口 (FFI)
Deno 的外部函数接口(FFI)允许 JavaScript 和 TypeScript 代码调用用 C、C++ 或 Rust 等语言编写的动态库中的函数。这使你能够将原生代码的性能和功能直接集成到你的 Deno 应用程序中。
Deno FFI 参考文档FFI 简介
FFI 在 Deno 的 JavaScript 运行时和原生代码之间搭建了一座桥梁。这使你能够:
- 在你的 Deno 应用程序中使用现有的原生库
- 用 Rust 或 C 等语言实现对性能要求严格的代码
- 访问 JavaScript 中不直接提供的操作系统 API 和硬件功能
Deno 的 FFI 实现基于 Deno.dlopen
API,该 API 加载动态库并为其导出的函数创建 JavaScript 绑定。
安全考量
FFI 需要使用 --allow-ffi
标志的明确许可,因为原生代码在 Deno 的安全沙盒之外运行
deno run --allow-ffi my_ffi_script.ts
重要安全警告:与在 Deno 沙盒中运行的 JavaScript 代码不同,通过 FFI 加载的原生库具有与 Deno 进程本身相同的访问级别。这意味着它们可以:
- 访问文件系统
- 建立网络连接
- 访问环境变量
- 执行系统命令
始终确保你信任通过 FFI 加载的原生库。
基本用法
在 Deno 中使用 FFI 的基本模式包括:
- 定义你想要调用的原生函数的接口
- 使用
Deno.dlopen()
加载动态库 - 调用已加载的函数
这是一个加载 C 库的简单示例
const dylib = Deno.dlopen("libexample.so", {
add: { parameters: ["i32", "i32"], result: "i32" },
});
console.log(dylib.symbols.add(5, 3)); // 8
dylib.close();
支持的类型
Deno 的 FFI 支持多种数据类型作为参数和返回值
FFI 类型 | Deno | C | Rust |
---|---|---|---|
i8 | number | char / signed char | i8 |
u8 | number | unsigned char | u8 |
i16 | number | short int | i16 |
u16 | number | unsigned short int | u16 |
i32 | number | int / signed int | i32 |
u32 | number | unsigned int | u32 |
i64 | bigint | long long int | i64 |
u64 | bigint | unsigned long long int | u64 |
usize | bigint | size_t | usize |
isize | bigint | size_t | isize |
f32 | number | float | f32 |
f64 | number | double | f64 |
void [1] | undefined | void | () |
pointer | {} | null | void * | *mut c_void |
buffer [2] | TypedArray | null | uint8_t * | *mut u8 |
function [3] | {} | null | void (*fun)() | Option<extern "C" fn()> |
{ struct: [...] } [4] | TypedArray | struct MyStruct | MyStruct |
自 Deno 1.25 起,pointer
类型已拆分为 pointer
和 buffer
类型,以确保用户可以利用 Typed Array 的优化,自 Deno 1.31 起,pointer
的 JavaScript 表示已变为不透明的指针对象,或对空指针表示为 null
。
- [1]
void
类型只能用作结果类型。 - [2]
buffer
类型接受 TypedArray 作为参数,但当它用作结果类型时(与pointer
类型类似),它总是返回一个指针对象或null
。 - [3]
function
类型作为参数和结果类型时,其工作方式与pointer
类型完全相同。 - [4]
struct
类型用于按值(复制)传递和返回 C 结构体。struct
数组必须按顺序枚举结构体每个字段的类型。结构体会自动进行填充:可以通过使用适量的u8
字段来定义紧凑结构体,以避免填充。只有 TypedArray 支持作为结构体,且结构体总是以Uint8Array
的形式返回。
使用结构体
你可以在 FFI 代码中定义和使用 C 结构体
// Define a struct type for a Point
const pointStruct = {
fields: {
x: "f64",
y: "f64",
},
} as const;
// Define the library interface
const signatures = {
distance: {
parameters: [
{ struct: pointStruct },
{ struct: pointStruct },
],
result: "f64",
},
} as const;
// Create struct instances
const point1 = new Deno.UnsafePointer(
new BigUint64Array([
BigInt(Float64Array.of(1.0).buffer),
BigInt(Float64Array.of(2.0).buffer),
]).buffer,
);
const point2 = new Deno.UnsafePointer(
new BigUint64Array([
BigInt(Float64Array.of(4.0).buffer),
BigInt(Float64Array.of(6.0).buffer),
]).buffer,
);
// Call the function with structs
const dist = dylib.symbols.distance(point1, point2);
使用回调
你可以将 JavaScript 函数作为回调传递给原生代码
const signatures = {
setCallback: {
parameters: ["function"],
result: "void",
},
runCallback: {
parameters: [],
result: "void",
},
} as const;
// Create a callback function
const callback = new Deno.UnsafeCallback(
{ parameters: ["i32"], result: "void" } as const,
(value) => {
console.log("Callback received:", value);
},
);
// Pass the callback to the native library
dylib.symbols.setCallback(callback.pointer);
// Later, this will trigger our JavaScript function
dylib.symbols.runCallback();
// Always clean up when done
callback.close();
FFI 最佳实践
-
始终关闭资源。完成后,使用
dylib.close()
关闭库,使用callback.close()
关闭回调。 -
优先使用 TypeScript。在使用 FFI 时,使用 TypeScript 以获得更好的类型检查。
-
将 FFI 调用包裹在 try/catch 块中,以优雅地处理错误。
-
使用 FFI 时务必小心,因为原生代码可以绕过 Deno 的安全沙盒。
-
尽量保持 FFI 接口小巧,以减少攻击面。
示例
使用 Rust 库
这里是创建和使用 Rust 库与 Deno 的示例
首先,创建一个 Rust 库
// lib.rs
#[no_mangle]
pub extern "C" fn fibonacci(n: u32) -> u32 {
if n <= 1 {
return n;
}
fibonacci(n - 1) + fibonacci(n - 2)
}
将其编译为动态库
rustc --crate-type cdylib lib.rs
然后从 Deno 中使用它
const libName = {
windows: "./lib.dll",
linux: "./liblib.so",
darwin: "./liblib.dylib",
}[Deno.build.os];
const dylib = Deno.dlopen(
libName,
{
fibonacci: { parameters: ["u32"], result: "u32" },
} as const,
);
// Calculate the 10th Fibonacci number
const result = dylib.symbols.fibonacci(10);
console.log(`Fibonacci(10) = ${result}`); // 55
dylib.close();
示例
这些由社区维护的仓库包含了 FFI 与不同操作系统上的各种原生库集成的实际示例。
原生代码集成的相关方法
虽然 Deno 的 FFI 提供了一种直接调用原生函数的方法,但还有其他集成原生代码的方法
在 Deno 中使用 Node-API (N-API)
Deno 支持 Node-API (N-API),以兼容原生 Node.js 插件。这使你能够使用为 Node.js 编写的现有原生模块。
直接加载 Node-API 插件
import process from "node:process";
process.dlopen(module, "./native_module.node", 0);
使用包含 Node-API 插件的 npm 包
import someNativeAddon from "npm:some-native-addon";
console.log(someNativeAddon.doSomething());
这与 FFI 有何不同?
方面 | FFI | Node-API 支持 |
---|---|---|
设置 | 无需构建步骤 | 需要预编译的二进制文件或构建步骤 |
可移植性 | 与库 ABI 绑定 | 跨版本 ABI 稳定 |
用例 | 直接库调用 | 重用 Node.js 插件 |
Node-API 支持非常适合利用现有的 Node.js 原生模块,而 FFI 最适合直接、轻量地调用原生库。
FFI 的替代方案
在使用 FFI 之前,请考虑以下替代方案:
- WebAssembly,用于在 Deno 沙盒中运行的可移植原生代码。
- 使用
Deno.command
以受控权限执行外部二进制文件和子进程。 - 检查 Deno 的原生 API 是否已提供你所需的功能。
Deno 的 FFI 功能提供了与原生代码的强大集成,实现了性能优化和对系统级功能的访问。然而,这种强大功能也伴随着重要的安全考量。在使用 FFI 时务必谨慎,并确保你信任正在使用的原生库。