输出参数

预计阅读时间: 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!