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 很可能会崩溃)。仅支持 cdecl 和 stdcall 回调。
临时回调
当原生 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 进行处理。