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

对于替代[调用约定](/koffi-chinese/han-shu-diao-yong.md#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 平台上指定正确的[调用约定](/koffi-chinese/han-shu-diao-yong.md#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>


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://nongchatea.gitbook.io/koffi-chinese/javascript-hui-tiao.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
