函数调用

预计阅读时间: 10 分钟

源文档 - Function calls

加载库

要声明函数,请先使用 koffi.load(filename) 加载共享库。

// ES6 语法:import koffi from 'koffi';
const koffi = require("koffi");

const lib = koffi.load("/path/to/shared/library"); // 文件扩展名取决于平台:.so, .dll, .dylib 等。

当对库的所有引用(包括下面描述的所有使用它的函数)都不存在时,该库将自动卸载。

Koffi 2.3.20 开始,你可以通过调用 lib.unload() 显式卸载库。在卸载库后尝试查找或调用该库中的函数会导致程序崩溃。

记录

在某些平台(例如 Linux 上的 musl C 库)上,共享库无法被卸载,因此在调用 lib.unload()之后,库仍然会保持加载和内存映射状态。

加载选项

新增于 Koffi 2.6,修改于 Koffi 2.8.2 和 Koffi 2.8.6

load 函数可以接受一个可选的对象参数,具有以下选项:

const options = {
	lazy: true, // 在 POSIX 平台上使用 RTLD_LAZY(懒绑定),默认使用 RTLD_NOW
	global: true, // 在 POSIX 平台上使用 RTLD_GLOBAL,默认使用 RTLD_LOCAL
	deep: true, // 如果支持,则使用 RTLD_DEEPBIND(Linux, FreeBSD)
};

const lib = koffi.load("/path/to/shared/library.so", options);

如果需要,可能会添加更多选项。

函数定义

定义语法

使用 koffi.load() 返回的对象从库中加载 C 函数。为此,你可以使用两种语法:

  • 经典语法,受 node-ffi 启发
  • 类 C 原型

经典语法

声明函数时,你需要指定其未修饰的名称、返回类型及其参数。对于可变参数函数,使用省略号作为最后一个参数。

const printf = lib.func("printf", "int", ["str", "..."]);
const atoi = lib.func("atoi", "int", ["str"]);

Koffi 会自动尝试为非标准 x86 调用约定使用修饰名称。关于此主题的更多信息,请参阅调用约定部分。

类 C 原型

如果你愿意,可以使用简单的类 C 原型字符串声明函数,如下所示:

const printf = lib.func("int printf(const char *fmt, ...)");
const atoi = lib.func("int atoi(str)"); // 参数名称未被 Koffi 使用,且是可选的

对于不接受参数的函数,可以使用 ()(void)

可变参数函数

可变参数函数的声明以省略号作为最后一个参数。

为了调用可变参数函数,你必须为每个额外的 C 参数提供两个 JavaScript 参数,第一个是预期的类型,第二个是值。

const printf = lib.func("printf", "int", ["str", "..."]);

// 可变参数是:6 (int), 8.5 (double), 'THE END' (const char *)
printf("Integer %d, double %g, str %s", "int", 6, "double", 8.5, "str", "THE END");

在 x86 平台上,仅可变参数函数可以使用 Cdecl 约定。

调用约定

更改于 Koffi 2.7

默认情况下,调用 C 函数是同步的。

大多数架构仅支持每个进程的一种过程调用标准。32 位 x86 平台是这一规则的例外,Koffi 支持几种 x86 约定:

约定经典形式原型形式描述
Cdeclkoffi.func(name, ret, params)(默认)这是默认约定,也是其他平台上的唯一约定
Stdcallkoffi.func('__stdcall', name, ret, params)stdcall此约定在 Win32 API 中广泛使用
Fastcallkoffi.func('__fastcall', name, ret, params)fastcall很少使用,使用 ECX 和 EDX 作为前两个参数
Thiscallkoffi.func('__thiscall', name, ret, params)thiscall很少使用,使用 ECX 作为第一个参数

在非 x86 平台上,你可以安全地使用这些约定,它们将被忽略。

记录

在 Koffi 2.7 中引入了将约定作为经典形式的第一个参数的功能。

在早期版本中,你必须使用koffi.stdcall()和类似函数。这些函数仍然受支持,但已被弃用,将在 Koffi 3.0 中移除。

下面是一个使用非默认调用约定的小示例,展示了两种语法:

// ES6 语法:import koffi from 'koffi';
const koffi = require("koffi");

const lib = koffi.load("user32.dll");

// 以下两个声明是等效的,在 x86 上使用 stdcall(在其他平台上使用默认 ABI)
const MessageBoxA_1 = lib.func("__stdcall", "MessageBoxA", "int", ["void *", "str", "str", "uint"]);
const MessageBoxA_2 = lib.func("int __stdcall MessageBoxA(void *hwnd, str text, str caption, uint type)");

调用类型

同步调用

声明本地函数后,你可以像调用其他 JS 函数一样直接调用它。

const atoi = lib.func("int atoi(const char *str)");

let value = atoi("1257");
console.log(value);

对于可变参数函数,你必须为每个额外的参数指定类型和值。

const printf = lib.func("printf", "int", ["str", "..."]);

// 可变参数是:6 (int), 8.5 (double), 'THE END' (const char *)
printf("Integer %d, double %g, str %s", "int", 6, "double", 8.5, "str", "THE END");

异步调用

你可以通过调用函数的 async 成员来发起异步调用。在这种情况下,你需要提供一个回调函数作为最后一个参数,该回调函数带有 (err, res) 参数。

// ES6 语法:import koffi from 'koffi';
const koffi = require("koffi");

const lib = koffi.load("libc.so.6");

const atoi = lib.func("int atoi(const char *str)");

atoi.async("1257", (err, res) => {
	console.log("Result:", res);
});
console.log("Hello World!");

// 该程序将打印:
//   Hello World!
//   Result: 1257

这些调用由工作线程执行。你需要处理可能由多线程引起的本地代码中的数据共享问题

你可以轻松地使用 Node.js 标准库中的 util.promisify() 将这种基于回调的异步函数转换为基于 Promise 的版本。

可变参数函数不能异步调用。

警告

异步函数在工作线程上运行。如果在多个线程之间共享数据,你需要处理线程安全问题。

回调必须从主线程调用,或者更准确地说,从 V8 解释器所在的线程调用。从另一个线程调用回调是未定义的行为,可能会导致崩溃或混乱。你已被警告!

函数指针

新增于 Koffi 2.4

你可以通过两种方式调用函数指针:

  • 直接使用 koffi.call(ptr, type, ...) 调用函数指针
  • 使用 koffi.decode(ptr, type) 将函数指针解码为实际函数

以下示例展示了如何基于以下本地 C 库以两种方式调用 int (*)(int, int) C 函数指针:

typedef int BinaryIntFunc(int a, int b);

static int AddInt(int a, int b) { return a + b; }
static int SubstractInt(int a, int b) { return a - b; }

BinaryIntFunc *GetBinaryIntFunction(const char *type)
{
    if (!strcmp(type, "add")) {
        return AddInt;
    } else if (!strcmp(type, "substract")) {
        return SubstractInt;
    } else {
        return NULL;
    }
}

直接调用指针

使用 koffi.call(ptr, type, ...) 调用函数指针。前两个参数是指针本身和你尝试调用的函数类型(使用 koffi.proto() 声明,如下所示),其余参数用于调用。

// 声明函数类型
const BinaryIntFunc = koffi.proto("int BinaryIntFunc(int a, int b)");

const GetBinaryIntFunction = lib.func("BinaryIntFunc *GetBinaryIntFunction(const char *name)");

const add_ptr = GetBinaryIntFunction("add");
const substract_ptr = GetBinaryIntFunction("substract");

let sum = koffi.call(add_ptr, BinaryIntFunc, 4, 5);
let delta = koffi.call(substract_ptr, BinaryIntFunc, 100, 58);

console.log(sum, delta); // 打印 9 和 42

将指针解码为函数

使用 koffi.decode(ptr, type) 获取一个 JS 函数,然后你可以像使用其他 Koffi 函数一样使用它。

此方法还允许你通过解码函数的 async 成员进行异步调用

// 声明函数类型
const BinaryIntFunc = koffi.proto("int BinaryIntFunc(int a, int b)");

const GetBinaryIntFunction = lib.func("BinaryIntFunc *GetBinaryIntFunction(const char *name)");

const add = koffi.decode(GetBinaryIntFunction("add"), BinaryIntFunc);
const substract = koffi.decode(GetBinaryIntFunction("substract"), BinaryIntFunc);

let sum = add(4, 5);
let delta = substract(100, 58);

console.log(sum, delta); // 打印 9 和 42

参数转换

默认情况下,Koffi 只会转发并将 JavaScript 参数转换为 C 参数。然而,许多 C 函数使用指针参数作为输出值或输入/输出值。

在接下来的页面中,你将了解以下内容: