deno.com

外部函数接口 (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 的基本模式包括:

  1. 定义你想要调用的原生函数的接口
  2. 使用 Deno.dlopen() 加载动态库
  3. 调用已加载的函数

这是一个加载 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 类型DenoCRust
i8numberchar / signed chari8
u8numberunsigned charu8
i16numbershort inti16
u16numberunsigned short intu16
i32numberint / signed inti32
u32numberunsigned intu32
i64bigintlong long inti64
u64bigintunsigned long long intu64
usizebigintsize_tusize
isizebigintsize_tisize
f32numberfloatf32
f64numberdoublef64
void[1]undefinedvoid()
pointer{} | nullvoid **mut c_void
buffer[2]TypedArray | nulluint8_t **mut u8
function[3]{} | nullvoid (*fun)()Option<extern "C" fn()>
{ struct: [...] }[4]TypedArraystruct MyStructMyStruct

自 Deno 1.25 起,pointer 类型已拆分为 pointerbuffer 类型,以确保用户可以利用 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 最佳实践

  1. 始终关闭资源。完成后,使用 dylib.close() 关闭库,使用 callback.close() 关闭回调。

  2. 优先使用 TypeScript。在使用 FFI 时,使用 TypeScript 以获得更好的类型检查。

  3. 将 FFI 调用包裹在 try/catch 块中,以优雅地处理错误。

  4. 使用 FFI 时务必小心,因为原生代码可以绕过 Deno 的安全沙盒。

  5. 尽量保持 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 有何不同?

方面FFINode-API 支持
设置无需构建步骤需要预编译的二进制文件或构建步骤
可移植性与库 ABI 绑定跨版本 ABI 稳定
用例直接库调用重用 Node.js 插件

Node-API 支持非常适合利用现有的 Node.js 原生模块,而 FFI 最适合直接、轻量地调用原生库。

FFI 的替代方案

在使用 FFI 之前,请考虑以下替代方案:

  • WebAssembly,用于在 Deno 沙盒中运行的可移植原生代码。
  • 使用 Deno.command 以受控权限执行外部二进制文件和子进程。
  • 检查 Deno 的原生 API 是否已提供你所需的功能。

Deno 的 FFI 功能提供了与原生代码的强大集成,实现了性能优化和对系统级功能的访问。然而,这种强大功能也伴随着重要的安全考量。在使用 FFI 时务必谨慎,并确保你信任正在使用的原生库。

您找到所需内容了吗?

隐私政策