JAVASCRIPT 回调
Koffi 2.4 中已更改
回调类型
Koffi 2.7 中已更改
为了将 JS 函数传递给需要回调的 C 函数,您必须首先创建一个具有预期返回类型和参数的回调类型。该语法类似于用于从共享库加载函数的语法。
// ES6 syntax: import koffi from 'koffi';
const koffi = require('koffi');
// With the classic syntax, this callback expects an integer and returns nothing
const ExampleCallback = koffi.proto('ExampleCallback', 'void', ['int']);
// With the prototype parser, this callback expects a double and float, and returns the sum as a double
const AddDoubleFloat = koffi.proto('double AddDoubleFloat(double d, float f)');
对于替代调用约定(例如stdcall
在 Windows x86 32 位上),您可以使用经典语法指定为第一个参数,或者在原型字符串中的返回类型之后指定,如下所示:
const HANDLE = koffi.pointer('HANDLE', koffi.opaque());
const HWND = koffi.alias('HWND', HANDLE);
// These two declarations work the same, and use the __stdcall convention on Windows x86
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可以解决此限制。
声明回调类型后,您可以在结构定义中使用指向它的指针,作为函数参数和/或返回类型,或者调用/解码函数指针。
瞬态回调和注册回调
Koffi 仅使用预定义的静态 Trampolines,不需要在运行时生成代码,这使得它与具有硬化 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 syntax: import koffi from 'koffi';
const koffi = require('koffi');
const lib = koffi.load('./callbacks.so'); // Fake path
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);
// This example prints:
// Hello Niels!
// Your age is: 27
// 42
注册回调
Koffi 2.0 中的新增功能(在 Koffi 2.2 中明确此绑定)
当函数需要稍后调用时(例如日志处理程序、事件处理程序等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 syntax: import koffi from 'koffi';
const koffi = require('koffi');
const lib = koffi.load('./callbacks.so'); // Fake path
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'); // Prints 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 *'); // If a C function calls cb1 it will fail because this will be undefined
let cb2 = koffi.register(store, store.get, 'IntCallback *'); // However in this case, this will match the store object
特别注意事项
解码指针参数
Koffi 2.2 中的新增功能,Koffi 2.3 中的更改
Koffi 没有足够的信息来将回调指针参数转换为适当的 JS 值。在这种情况下,您的 JS 函数将接收一个不透明的外部对象。
您可以将此值传递给另一个需要相同类型指针的 C 函数,或者您可以使用koffi.decode()函数来解码指针参数。
以下示例使用它通过标准 C 函数qsort()
对字符串数组进行快速排序
// ES6 syntax: 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); // Prints ['123', 'bar', 'foo', 'foobar']
异步回调
Koffi 2.2.2 中的新功能
JS 执行本质上是单线程的,因此 JS 回调必须在主线程上运行。您可能想通过两种方式从另一个线程调用回调函数:
从异步 FFI 调用中调用回调(例如
waitpid.async
)在同步 FFI 调用内,将回调传递给另一个线程
在这两种情况下,只要 JS 事件循环有机会运行(例如,当您等待一个 Promise 时),Koffi 就会对 JS 的回调进行排队,以便在主线程上运行。
请注意,如果您从辅助线程调用回调并且主线程从不让 JS 事件循环运行(例如,如果主线程等待辅助线程自行完成某些操作),您很容易陷入死锁情况。
异常处理
如果 JS 回调内部发生异常,C API 将收到 0 或 NULL(取决于返回值类型)。
如果您需要以不同方式处理异常,请自行处理异常(使用 try/catch)。
Last updated