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 可能会崩溃)。仅支持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 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