本页内容

外部函数接口

从 Deno 1.13 版本开始,FFI(外部函数接口)API 允许用户使用 Deno.dlopen 调用使用 C ABI(C/C++、Rust、Zig、V 等)编写的原生语言库。

用法 跳转到标题

以下示例展示了如何从 Deno 调用 Rust 函数

// add.rs
#[no_mangle]
pub extern "C" fn add(a: isize, b: isize) -> isize {
    a + b
}

将其编译为 C 动态库(在 Linux 上为 libadd.so

rustc --crate-type cdylib add.rs

在 C 中,你可以这样写

// add.c
int add(int a, int b) {
  return a + b;
}

然后编译它

// unix
cc -c -o add.o add.c
cc -shared -W -o libadd.so add.o
// Windows
cl /LD add.c /link /EXPORT:add

从 Deno 调用库

// ffi.ts

// Determine library extension based on
// your OS.
let libSuffix = "";
switch (Deno.build.os) {
  case "windows":
    libSuffix = "dll";
    break;
  case "darwin":
    libSuffix = "dylib";
    break;
  default:
    libSuffix = "so";
    break;
}

const libName = `./libadd.${libSuffix}`;
// Open library and define exported symbols
const dylib = Deno.dlopen(
  libName,
  {
    "add": { parameters: ["isize", "isize"], result: "isize" },
  } as const,
);

// Call the symbol `add`
const result = dylib.symbols.add(35, 34); // 69

console.log(`Result from external addition of 35 and 34: ${result}`);

使用 --allow-ffi--unstable 标志运行

deno run --allow-ffi --unstable ffi.ts

非阻塞 FFI 跳转到标题

在许多情况下,用户可能希望在后台运行 CPU 密集型 FFI 函数,而不会阻塞主线程上的其他任务。

从 Deno 1.15 版本开始,可以在 Deno.dlopen 中将符号标记为 nonblocking。这些函数调用将在专用阻塞线程上运行,并返回一个解析为所需 resultPromise

使用 Deno 执行昂贵的 FFI 调用的示例

// sleep.c
#ifdef _WIN32
#include <Windows.h>
#else
#include <time.h>
#endif

int sleep(unsigned int ms) {
  #ifdef _WIN32
  Sleep(ms);
  #else
  struct timespec ts;
  ts.tv_sec = ms / 1000;
  ts.tv_nsec = (ms % 1000) * 1000000;
  nanosleep(&ts, NULL);
  #endif
}

从 Deno 调用它

// nonblocking_ffi.ts
const library = Deno.dlopen(
  "./sleep.so",
  {
    sleep: {
      parameters: ["usize"],
      result: "void",
      nonblocking: true,
    },
  } as const,
);

library.symbols.sleep(500).then(() => console.log("After"));
console.log("Before");

结果

$ deno run --allow-ffi --unstable unblocking_ffi.ts
Before
After

回调 跳转到标题

Deno FFI API 支持从 JavaScript 函数创建 C 回调,以便从动态库回调到 Deno。以下是如何创建和使用回调的示例

// callback_ffi.ts
const library = Deno.dlopen(
  "./callback.so",
  {
    set_status_callback: {
      parameters: ["function"],
      result: "void",
    },
    start_long_operation: {
      parameters: [],
      result: "void",
    },
    check_status: {
      parameters: [],
      result: "void",
    },
  } as const,
);

const callback = new Deno.UnsafeCallback(
  {
    parameters: ["u8"],
    result: "void",
  } as const,
  (success: number) => {},
);

// Pass the callback pointer to dynamic library
library.symbols.set_status_callback(callback.pointer);
// Start some long operation that does not block the thread
library.symbols.start_long_operation();

// Later, trigger the library to check if the operation is done.
// If it is, this call will trigger the callback.
library.symbols.check_status();

如果 UnsafeCallback 的回调函数抛出错误,该错误将传播到触发回调调用的函数(上面是 check_status()),并可以在那里捕获。如果返回值的回调抛出异常,则 Deno 将返回 0(指针的空指针)作为结果。

UnsafeCallback 默认情况下不会被释放,因为它会导致使用后释放错误。要正确释放 UnsafeCallback,必须调用其 close() 方法。

const callback = new Deno.UnsafeCallback(
  { parameters: [], result: "void" } as const,
  () => {},
);

// After callback is no longer needed
callback.close();
// It is no longer safe to pass the callback as a parameter.

原生库也可以设置中断处理程序,并让这些处理程序直接触发回调。但是,不建议这样做,因为这可能会导致意外的副作用和未定义的行为。最好任何中断处理程序只设置一个标志,该标志可以稍后像上面使用 check_status() 一样进行轮询。

支持的类型 跳转到标题

以下是 Deno FFI API 目前支持的类型列表。

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 number | bigint long long int i64
u64 number | bigint unsigned long long int u64
usize number | bigint size_t usize
isize number | bigint size_t isize
f32 number | bigint float f32
f64 number | bigint 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 类型被拆分为 pointerbuffer 类型,以确保用户能够利用 Typed Arrays 的优化。从 Deno 1.31 版本开始,pointer 的 JavaScript 表示形式变为不透明指针对象,对于空指针则为 null

  • [1] void 类型只能用作结果类型。
  • [2] buffer 类型接受 TypedArrays 作为参数,但当用作结果类型时,它始终返回指针对象或 null,与 pointer 类型相同。
  • [3] function 类型在作为参数和结果类型时与 pointer 类型的工作方式完全相同。
  • [4] struct 类型用于按值(复制)传递和返回 C 结构体。struct 数组必须按顺序枚举结构体中每个字段的类型。结构体将自动填充:可以通过使用适当数量的 u8 字段来定义打包的结构体,以避免填充。仅支持 TypedArrays 作为结构体,结构体始终以 Uint8Array 的形式返回。

deno_bindgen 跳转到标题

deno_bindgen 是简化 Rust 编写的 Deno FFI 库的粘合代码生成的官方工具。

它类似于 Rust WASM 生态系统中的 wasm-bindgen

以下是一个示例,展示了它的用法

// mul.rs
use deno_bindgen::deno_bindgen;

#[deno_bindgen]
struct Input {
  a: i32,
  b: i32,
}

#[deno_bindgen]
fn mul(input: Input) -> i32 {
  input.a * input.b
}

运行 deno_bindgen 生成绑定。现在可以直接将它们导入到 Deno 中

// mul.ts
import { mul } from "./bindings/bindings.ts";
mul({ a: 10, b: 2 }); // 20

任何与 deno_bindgen 相关的问题都应在 https://github.com/denoland/deno_bindgen/issues 上报告。