函数调用
预计阅读时间: 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() 显式卸载库。在卸载库后尝试查找或调用该库中的函数会导致程序崩溃。
加载选项
新增于 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 约定:
| 约定 | 经典形式 | 原型形式 | 描述 |
|---|
| Cdecl | koffi.func(name, ret, params) | (默认) | 这是默认约定,也是其他平台上的唯一约定 |
| Stdcall | koffi.func('__stdcall', name, ret, params) | stdcall | 此约定在 Win32 API 中广泛使用 |
| Fastcall | koffi.func('__fastcall', name, ret, params) | fastcall | 很少使用,使用 ECX 和 EDX 作为前两个参数 |
| Thiscall | koffi.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 函数使用指针参数作为输出值或输入/输出值。
在接下来的页面中,你将了解以下内容: