输出参数
预计阅读时间: 9 分钟
源文档 - Output parameters
输出和输入/输出
为了简化操作,并且由于 JavaScript 只对原始类型具有值语义,Koffi 可以将多种类型的参数输出(或输入/输出):
要将参数从仅输入更改为输出或输入/输出,请使用以下函数:
- 在指针上调用
koffi.out(),例如 koffi.out(koffi.pointer(timeval))(其中 timeval 是一个结构体类型)
- 对于双向输入/输出参数,使用
koffi.inout()
在使用类似 C 的原型字符串声明函数时,也可以使用类似 MSDN 的类型限定符:
_Out_ 用于输出参数
_Inout_ 用于双向输入/输出参数
提示
Win32 API 提供了许多函数,这些函数接受一个指向空结构体的指针用于输出,但结构体的第一个成员(通常命名为 cbSize)必须在调用函数之前设置为结构体的大小。例如 GetLastInputInfo() 函数。
要在 Koffi 中使用这些函数,必须将参数定义为 _Inout_:必须将值复制进去(以向函数提供 cbSize),然后将填充好的结构体复制到 JS。
更多详细信息请参阅下面的
Win32 示例。
原始值
这个 Windows 示例枚举了所有 Chrome 窗口及其 PID 和标题。GetWindowThreadProcessId() 函数展示了如何从输出参数中获取原始值。
// ES6 语法:import koffi from 'koffi';
const koffi = require("koffi");
const user32 = koffi.load("user32.dll");
const DWORD = koffi.alias("DWORD", "uint32_t");
const HANDLE = koffi.pointer("HANDLE", koffi.opaque());
const HWND = koffi.alias("HWND", HANDLE);
const FindWindowEx = user32.func(
"HWND __stdcall FindWindowExW(HWND hWndParent, HWND hWndChildAfter, const char16_t *lpszClass, const char16_t *lpszWindow)",
);
const GetWindowThreadProcessId = user32.func(
"DWORD __stdcall GetWindowThreadProcessId(HWND hWnd, _Out_ DWORD *lpdwProcessId)",
);
const GetWindowText = user32.func("int __stdcall GetWindowTextA(HWND hWnd, _Out_ uint8_t *lpString, int nMaxCount)");
for (let hwnd = null; ; ) {
hwnd = FindWindowEx(0, hwnd, "Chrome_WidgetWin_1", null);
if (!hwnd) break;
// 获取 PID
let pid;
{
let ptr = [null];
let tid = GetWindowThreadProcessId(hwnd, ptr);
if (!tid) {
// 进程可能在中间结束了?
continue;
}
pid = ptr[0];
}
// 获取窗口标题
let title;
{
let buf = Buffer.allocUnsafe(1024);
let length = GetWindowText(hwnd, buf, buf.length);
if (!length) {
// 进程可能在中间结束了?
continue;
}
title = koffi.decode(buf, "char", length);
}
console.log({ PID: pid, Title: title });
}
结构体示例
POSIX 结构体示例
此示例调用了 POSIX 函数 gettimeofday(),并使用了类似原型的语法。
// ES6 语法:import koffi from 'koffi';
const koffi = require("koffi");
const lib = koffi.load("libc.so.6");
const timeval = koffi.struct("timeval", {
tv_sec: "unsigned int",
tv_usec: "unsigned int",
});
const timezone = koffi.struct("timezone", {
tz_minuteswest: "int",
tz_dsttime: "int",
});
// `_Out_` 限定符指示 Koffi 将值输出
const gettimeofday = lib.func("int gettimeofday(_Out_ timeval *tv, _Out_ timezone *tz)");
let tv = {};
gettimeofday(tv, null);
console.log(tv);
Win32 结构体示例
许多使用结构体输出的 Win32 函数要求你设置一个大小成员(通常命名为 cbSize)。这些函数不能与 _Out_ 一起使用,因为大小值必须从 JS 复制到 C,这种情况下应使用 _Inout_。
// ES6 语法:import koffi from 'koffi';
const koffi = require("koffi");
const user32 = koffi.load("user32.dll");
const LASTINPUTINFO = koffi.struct("LASTINPUTINFO", {
cbSize: "uint",
dwTime: "uint32",
});
const GetLastInputInfo = user32.func("bool __stdcall GetLastInputInfo(_Inout_ LASTINPUTINFO *plii)");
let info = { cbSize: koffi.sizeof(LASTINPUTINFO) };
let success = GetLastInputInfo(info);
console.log(success, info);
不透明类型示例
此示例打开一个内存中的 SQLite 数据库,并使用类似 node-ffi 的函数声明语法。
// ES6 语法:import koffi from 'koffi';
const koffi = require("koffi");
const lib = koffi.load("sqlite3.so");
const sqlite3 = koffi.opaque("sqlite3");
// 在双指针上调用 `koffi.out()`,以便在调用后从 C 复制到 JS
const sqlite3_open_v2 = lib.func("sqlite3_open_v2", "int", ["str", koffi.out(koffi.pointer(sqlite3, 2)), "int", "str"]);
const sqlite3_close_v2 = lib.func("sqlite3_close_v2", "int", [koffi.pointer(sqlite3)]);
const SQLITE_OPEN_READWRITE = 0x2;
const SQLITE_OPEN_CREATE = 0x4;
let out = [null];
if (sqlite3_open_v2(":memory:", out, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, null) != 0)
throw new Error("Failed to open database");
let db = out[0];
sqlite3_close_v2(db);
字符串缓冲区示例
新增于 Koffi 2.2
此示例调用一个 C 函数,将两个字符串连接到一个预先分配的字符串缓冲区。由于 JS 字符串是不可变的,因此必须传递一个包含单个字符串的数组。
void ConcatToBuffer(const char *str1, const char *str2, char *out)
{
size_t len = 0;
for (size_t i = 0; str1[i]; i++) {
out[len++] = str1[i];
}
for (size_t i = 0; str2[i]; i++) {
out[len++] = str2[i];
}
out[len] = 0;
}
const ConcatToBuffer = lib.func("void ConcatToBuffer(const char *str1, const char *str2, _Out_ char *out)");
let str1 = "Hello ";
let str2 = "Friends!";
// 我们需要为输出缓冲区预留空间!包括 NUL 终止符
// 因为 ConcatToBuffer() 期望如此,但 Koffi 可以在没有它的情况下将其转换回 JS 字符串
// (如果我们预留了正确的大小)。
let out = ["\0".repeat(str1.length + str2.length + 1)];
ConcatToBuffer(str1, str2, out);
console.log(out[0]);
输出缓冲区
在大多数情况下,你可以使用缓冲区和类型化数组作为输出缓冲区。只要缓冲区仅在调用本地 C 函数时使用即可。关于瞬态指针的示例,请参阅下面的内容。
警告
将指针保留在本地代码中,或在提供它的函数调用之外更改其内容是不安全的。
如果需要提供一个将被保留的指针,请使用 koffi.alloc() 分配内存。
瞬态指针
新增于 Koffi 2.3
你可以使用缓冲区和类型化数组作为输出(以及输入/输出)指针参数。只需将缓冲区作为参数传递,本地函数将接收指向其内容的指针。
本地函数返回后,你可以使用 koffi.decode(value, type) 解码内容,如下例所示:
// ES6 语法:import koffi from 'koffi';
const koffi = require("koffi");
const lib = koffi.load("libc.so.6");
const Vec3 = koffi.struct("Vec3", {
x: "float32",
y: "float32",
z: "float32",
});
const memcpy = lib.func("void *memcpy(_Out_ void *dest, const void *src, size_t size)");
let vec1 = { x: 1, y: 2, z: 3 };
let vec2 = null;
// 通过 memcpy 以复杂的方式复制向量
{
let src = koffi.as(vec1, "Vec3 *");
let dest = Buffer.allocUnsafe(koffi.sizeof(Vec3));
memcpy(dest, src, koffi.sizeof(Vec3));
vec2 = koffi.decode(dest, Vec3);
}
// 更改 vector1,保留副本不变
[vec1.x, vec1.y, vec1.z] = [vec1.z, vec1.y, vec1.x];
console.log(vec1); // { x: 3, y: 2, z: 1 }
console.log(vec2); // { x: 1, y: 2, z: 3 }
关于解码函数的更多信息,请参阅解码变量。
稳定指针
新增于 Koffi 2.8
在某些情况下,本地代码可能需要在稍后更改输出缓冲区,可能是在后续调用期间或从另一个线程中。
在这种情况下,使用缓冲区或类型化数组是不安全的!
然而,你可以使用 koffi.alloc(type, len) 分配内存并获取一个不会移动的指针,该指针可以由本地代码在任何时间安全地使用。需要时可以使用 koffi.decode() 从指针中读取数据。
下面的示例设置了一些内存,用作输出缓冲区,每次调用时,一个连接函数都会在其中附加一个字符串。
#include <assert.h>
#include <stddef.h>
static char *buf_ptr;
static size_t buf_len;
static size_t buf_size;
void reset_buffer(char *buf, size_t size)
{
assert(size > 1);
buf_ptr = buf;
buf_len = 0;
buf_size = size - 1; // 为尾部的 NUL 留出空间
buf_ptr[0] = 0;
}
void append_str(const char *str)
{
for (size_t i = 0; str[i] && buf_len < buf_size; i++, buf_len++) {
buf_ptr[buf_len] = str[i];
}
buf_ptr[buf_len] = 0;
}
const reset_buffer = lib.func("void reset_buffer(char *buf, size_t size)");
const append_str = lib.func("void append_str(const char *str)");
let output = koffi.alloc("char", 64);
reset_buffer(output, 64);
append_str("Hello");
console.log(koffi.decode(output, "char", -1)); // 打印 Hello
append_str(" World!");
console.log(koffi.decode(output, "char", -1)); // 打印 Hello World!