javascript 回调

预计阅读时间: 10 分钟

源文档 - Javascript callbacks

回调类型

在 Koffi 2.7 中有所更改

为了将一个 JS 函数传递给期望回调的 C 函数,您必须首先创建一个具有预期返回类型和参数的回调类型。其语法与从共享库中加载函数时使用的语法类似。

// ES6 语法:import koffi from 'koffi';
const koffi = require("koffi");

// 使用经典语法,此回调期望一个整数并返回无内容
const ExampleCallback = koffi.proto("ExampleCallback", "void", ["int"]);

// 使用原型解析器,此回调期望一个双精度浮点数和一个单精度浮点数,并以双精度浮点数的形式返回它们的和
const AddDoubleFloat = koffi.proto("double AddDoubleFloat(double d, float f)");
记录

koffi.proto() 函数是在 Koffi 2.4 中引入的,在早期版本中它被称为 koffi.callback()

对于替代调用约定(例如 Windows x86 32 位上的 stdcall),您可以使用经典语法将它指定为第一个参数,或者在原型字符串中将其放在返回类型之后,如下所示:

const HANDLE = koffi.pointer("HANDLE", koffi.opaque());
const HWND = koffi.alias("HWND", HANDLE);

// 这两个声明的作用相同,并在 Windows x86 上使用 __stdcall 约定
const EnumWindowsProc = koffi.proto("bool __stdcall EnumWindowsProc (HWND hwnd, long lParam)");
const EnumWindowsProc = koffi.proto("__stdcall", "EnumWindowsProc", "bool", ["HWND", "long"]);
警告

您必须确保 正确使用调用约定(例如为 Windows API 回调指定 stdcall),否则您的代码可能会在 Windows 32 位系统上崩溃。

在 Koffi 2.7 之前,无法使用经典语法来使用替代回调调用约定。使用原型字符串或 升级到 Koffi 2.7 可以解决这一限制。

一旦声明了回调类型,您就可以在结构体定义中、作为函数参数以及返回类型中使用指向它的指针,或者用来调用/解码函数指针。

记录

回调在 2.0 版本中发生了变化

在 Koffi 1.x 中,回调的定义方式使它们可以直接作为参数和返回类型使用,从而隐藏了底层指针。

现在,您必须通过指针来使用它们:在 Koffi 1.x 中的 void CallIt(CallbackType func) 变为在 2.0 及更高版本中的 void CallIt(CallbackType *func)

有关更多信息,请参阅迁移指南

临时回调和已注册回调

Koffi 仅使用预定义的静态跳板,无需在运行时生成代码,这使其与具有强化的 W^X 缓解措施的平台(例如 PaX mprotect)兼容。然而,这对接收回调的最大数量及其持续时间施加了一些限制。

因此,Koffi 区分了两种回调模式:

  • 临时回调 只能在传递给它们的 C 函数运行时被调用,并且在该函数返回时失效。如果 C 函数稍后调用该回调,则行为是未定义的,尽管 Koffi 会尝试检测此类情况。如果检测到,将抛出异常,但这并不能保证。然而,它们的使用非常简单,不需要任何特殊处理。
  • 已注册回调 可以在任何时间被调用,但必须手动注册和注销。同时可以存在的已注册回调的数量是有限的。

您需要在 x86 平台上指定正确的调用约定,否则行为是未定义的(Node 很可能会崩溃)。仅支持 cdeclstdcall 回调。

临时回调

当原生 C 函数仅在运行时需要调用它们时(例如 qsort、进度回调、sqlite3_exec),请使用临时回调。以下是一个包含 C 部分和 JS 部分的小示例。

#include <string.h>

int TransferToJS(const char *name, int age, int (*cb)(const char *str, int age))
{
    char buf[64];
    snprintf(buf, sizeof(buf), "Hello %s!", str);
    return cb(buf, age);
}
// ES6 语法:import koffi from 'koffi';
const koffi = require("koffi");

const lib = koffi.load("./callbacks.so"); // 假定路径

const TransferCallback = koffi.proto("int TransferCallback(const char *str, int age)");

const TransferToJS = lib.func("TransferToJS", "int", ["str", "int", koffi.pointer(TransferCallback)]);

let ret = TransferToJS("Niels", 27, (str, age) => {
	console.log(str);
	console.log("Your age is:", age);
	return 42;
});
console.log(ret);

// 此示例打印:
//   Hello Niels!
//   Your age is: 27
//   42

已注册回调

新增于 Koffi 2.0(Koffi 2.2 中显式支持 this 绑定)

当函数需要在稍后时间被调用时(例如日志处理器、事件处理器、fopencookie/funopen),请使用已注册回调。调用 koffi.register(func, type) 来注册一个回调函数,它有两个参数:JS 函数和回调类型。

完成后,调用 koffi.unregister()(使用 koffi.register() 返回的值)来释放插槽。同时可以存在最多 8192 个回调。未能做到这一点会导致插槽泄漏,一旦所有插槽都被占用,后续注册可能会失败(抛出异常)。

以下示例展示了如何注册和注销延迟回调。

static const char *(*g_cb1)(const char *name);
static void (*g_cb2)(const char *str);

void RegisterFunctions(const char *(*cb1)(const char *name), void (*cb2)(const char *str))
{
    g_cb1 = cb1;
    g_cb2 = cb2;
}

void SayIt(const char *name)
{
    const char *str = g_cb1(name);
    g_cb2(str);
}
// ES6 语法:import koffi from 'koffi';
const koffi = require("koffi");

const lib = koffi.load("./callbacks.so"); // 假定路径

const GetCallback = koffi.proto("const char *GetCallback(const char *name)");
const PrintCallback = koffi.proto("void PrintCallback(const char *str)");

const RegisterFunctions = lib.func("void RegisterFunctions(GetCallback *cb1, PrintCallback *cb2)");
const SayIt = lib.func("void SayIt(const char *name)");

let cb1 = koffi.register(name => "Hello " + name + "!", koffi.pointer(GetCallback));
let cb2 = koffi.register(console.log, "PrintCallback *");

RegisterFunctions(cb1, cb2);
SayIt("Kyoto"); // 打印 Hello Kyoto!

koffi.unregister(cb1);
koffi.unregister(cb2);

Koffi 2.2 开始,您可以可选地将函数的 this 值指定为第一个参数。

class ValueStore {
	constructor(value) {
		this.value = value;
	}
	get() {
		return this.value;
	}
}

let store = new ValueStore(42);

let cb1 = koffi.register(store.get, "IntCallback *"); // 如果 C 函数调用 cb1,它将失败,因为 this 将是 undefined
let cb2 = koffi.register(store, store.get, "IntCallback *"); // 然而,在这种情况下,this 将匹配 store 对象

特殊注意事项

解码指针参数

新增于 Koffi 2.2,更改于 Koffi 2.3

Koffi 没有足够的信息将回调指针参数转换为适当的 JS 值。在这种情况下,您的 JS 函数将接收到一个不透明的 External 对象。

您可以将此值传递给期望相同类型指针的另一个 C 函数,或者使用 koffi.decode() 函数来解码指针参数。

以下示例使用它通过标准 C 函数 qsort() 就地对字符串数组进行排序:

// ES6 语法:import koffi from 'koffi';
const koffi = require("koffi");

const lib = koffi.load("libc.so.6");

const SortCallback = koffi.proto("int SortCallback(const void *first, const void *second)");
const qsort = lib.func("void qsort(_Inout_ void *array, size_t count, size_t size, SortCallback *cb)");

let array = ["foo", "bar", "123", "foobar"];

qsort(koffi.as(array, "char **"), array.length, koffi.sizeof("void *"), (ptr1, ptr2) => {
	let str1 = koffi.decode(ptr1, "char *");
	let str2 = koffi.decode(ptr2, "char *");

	return str1.localeCompare(str2);
});

console.log(array); // 打印 ['123', 'bar', 'foo', 'foobar']

异步回调

新增于 Koffi 2.2.2

JS 执行本质上是单线程的,因此 JS 回调必须在主线程上运行。您可能希望从另一个线程调用回调函数,有两种方式:

  • 从异步 FFI 调用中调用回调(例如 waitpid.async
  • 在同步 FFI 调用中,将回调传递给另一个线程

在这两种情况下,Koffi 都会将回调排队,以便在主线程上运行,只要 JS 事件循环有机会运行(例如当您等待一个 Promise 时)。

警告

请注意,如果从辅助线程调用回调,而主线程从未让 JS 事件循环运行(例如,如果主线程等待辅助线程完成某项操作),则很容易陷入死锁状态。

异常处理

如果在 JS 回调中发生异常,C API 将接收到 0 或 NULL(具体取决于返回值类型)。

如果需要以不同的方式处理异常,请自行使用 try/catch 进行处理。