# JAVASCRIPT 回调

*Koffi 2.4 中已更改*

{% hint style="info" %}
该函数`koffi.proto()`在Koffi 2.4中引入，`koffi.callback()`在早期版本中被调用。
{% endhint %}

## 回调类型

*Koffi 2.7 中已更改*

为了将 JS 函数传递给需要回调的 C 函数，您必须首先创建一个具有预期返回类型和参数的回调类型。该语法类似于用于从共享库加载函数的语法。

```typescript
// 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)');
```

对于替代[调用约定](https://nongchatea.gitbook.io/koffi-chinese/han-shu-diao-yong#tiao-yong-yue-ding-calling-conventions)（例如`stdcall`在 Windows x86 32 位上），您可以使用经典语法指定为第一个参数，或者在原型字符串中的返回类型之后指定，如下所示：

```typescript
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']);
```

{% hint style="warning" %}
您必须确保**调用约定正确**（例如为 Windows API 回调指定 \_\_stdcall），否则您的代码将在 Windows 32 位上崩溃。

在 Koffi 2.7 之前，不可能*使用经典语法的替代回调调用约定*。使用原型字符串或*升级到 Koffi 2.7*可以解决此限制。
{% endhint %}

声明回调类型后，您可以在结构定义中使用指向它的指针，作为函数参数和/或返回类型，或者调用/解码函数指针。

{% hint style="info" %}
回调**在 2.0 版本中发生了变化**。

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

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

有关更多信息，请参阅[迁移指南。](https://koffi.dev/migration)
{% endhint %}

## 瞬态回调和注册回调

Koffi 仅使用预定义的静态 Trampolines，不需要在运行时生成代码，这使得它与具有硬化 W^X 迁移的平台（例如 PaX mprotect）兼容。但是，这对回调的最大数量及其持续时间施加了一些限制。

因此，Koffi 区分了两种回调模式：

* [瞬态回调](#shun-tai-hui-tiao)只能在传递给它们的 C 函数运行时调用，并且在返回时失效。如果 C 函数稍后调用回调，则行为是未定义的，尽管 Koffi 尝试检测此类情况。如果确实如此，将会抛出异常，但这并不能保证。然而，它们使用起来很简单，不需要任何特殊处理。
* [已注册的回调](#zhu-ce-hui-tiao)可以随时调用，但必须手动注册和取消注册。可以同时存在有限数量的注册回调。

您需要在 x86 平台上指定正确的[调用约定](https://nongchatea.gitbook.io/koffi-chinese/han-shu-diao-yong#tiao-yong-yue-ding-calling-conventions)，否则行为未定义（Node 可能会崩溃）。仅支持*cdecl*和*stdcall回调。*

### 瞬态回调

当本机 C 函数仅需要在运行时调用它们时，请使用瞬态回调（例如 qsort、进度回调、`sqlite3_exec`等）。这是一个包含 C 部分和 JS 部分的小示例。

```c
#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);
}
```

```typescript
// 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 个回调。如果不这样做，槽就会泄漏，一旦所有槽都被使用，后续注册可能会失败（有例外）。

下面的示例展示了如何注册和取消注册延迟回调。

```c
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);
}
```

```typescript
// 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`函数的值指定为第一个参数。

```typescript
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()](https://koffi.dev/variables#decode-to-js-values)函数来解码指针参数。

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

```typescript
// 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 的回调进行排队，以便在主线程上运行。

{% hint style="warning" %}
请注意，如果您从辅助线程调用回调并且主线程从不让 JS 事件循环运行（例如，如果主线程等待辅助线程自行完成某些操作），您很容易陷入死锁情况。
{% endhint %}

## 异常处理

如果 JS 回调内部发生异常，C API 将收到 0 或 NULL（取决于返回值类型）。

如果您需要以不同方式处理异常，请自行处理异常（使用 try/catch）。<br>
