数据指针
预计阅读时间: 13 分钟
源文档 - Data pointers
指针的使用
在 C 语言中,指针参数用于不同的目的。区分这些用例非常重要,因为 Koffi 为每种情况提供了不同的处理方式:
- 结构体指针:C 库中使用结构体指针的情况分为两种:避免(可能的)昂贵的复制操作,以及允许函数修改结构体的内容(输出或输入/输出参数)。
- 不透明指针:库不会暴露结构体的内容,只提供一个指向它的指针(例如
FILE *)。只有库提供的函数可以操作这个指针,在 Koffi 中我们称其为不透明类型。这通常是为了 ABI 稳定性,防止库的使用者直接操作库的内部实现。
- 指向原始类型的指针:这种情况较为少见,通常用于输出或输入/输出参数。Win32 API 中有许多这样的例子。
- 数组:在 C 语言中,动态大小的数组通常通过指针传递给函数,要么是通过空指针终止(或任何其他哨兵值),要么是通过额外的长度参数。
指针类型
结构体指针
以下是一个 Win32 的示例,使用GetCursorPos()(带有输出参数)来获取并显示当前光标位置。
// ES6语法:import koffi from 'koffi';
const koffi = require("koffi");
const lib = koffi.load("user32.dll");
// 类型声明
const POINT = koffi.struct("POINT", {
x: "long",
y: "long",
});
// 函数声明
const GetCursorPos = lib.func("int __stdcall GetCursorPos(_Out_ POINT *pos)");
// 获取并显示光标位置
let pos = {};
if (!GetCursorPos(pos)) throw new Error("Failed to get cursor position");
console.log(pos);
不透明指针
Koffi 2.0 新增
一些 C 库使用句柄,这些句柄的行为类似于指向不透明结构体的指针。例如 Win32 API 中的HANDLE类型。如果你想重现这种行为,可以定义一个指向不透明类型的命名指针类型,如下所示:
const HANDLE = koffi.pointer("HANDLE", koffi.opaque());
// 现在你可以这样使用它:
const GetHandleInformation = lib.func("bool __stdcall GetHandleInformation(HANDLE h, _Out_ uint32_t *flags)");
const CloseHandle = lib.func("bool __stdcall CloseHandle(HANDLE h)");
指向原始类型的指针
在 JavaScript 中,无法通过引用将原始值传递给另一个函数。这意味着你不能调用一个函数并期望它修改其数字或字符串参数的值。
然而,数组和对象(以及其他类型)是引用类型值。将一个数组或对象从一个变量赋值给另一个变量不会涉及任何复制。相反,正如以下示例所示,新变量引用的是与第一个变量相同的数组:
let list1 = [1, 2];
let list2 = list1;
list2[1] = 42;
console.log(list1); // 打印 [1, 42]
所有这些意味着,期望修改其原始输出值的 C 函数(例如int *参数)不能直接使用。然而,得益于 Koffi 的透明数组支持,你可以使用 JavaScript 数组来近似引用语义,使用单元素数组。
下面是一个加法函数的示例,结果存储在int *输入/输出参数中,以及如何从 Koffi 使用这个函数。
void AddInt(int *dest, int add)
{
*dest += add;
}
你可以简单地将单元素数组作为第一个参数传递:
const AddInt = lib.func("void AddInt(_Inout_ int *dest, int add)");
let sum = [36];
AddInt(sum, 6);
console.log(sum[0]); // 打印 42
动态数组
在 C 语言中,动态大小的数组通常通过指针传递。长度要么作为额外的参数传递,要么从数组内容本身推断出来,例如通过终止哨兵值(例如在字符串数组中使用空指针)。
Koffi 可以将 JS 数组和 TypedArrays 转换为指针参数。然而,由于 C 语言没有动态大小数组的真正概念(胖指针),你需要根据 API 自己提供长度或哨兵值。
以下是一个简单的 C 函数示例,它接受一个以空指针终止的字符串数组作为输入,以计算所有字符串的总长度。
// 使用以下命令构建:clang -fPIC -o length.so -shared length.c -Wall -O2
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
int64_t ComputeTotalLength(const char **strings)
{
int64_t total = 0;
for (const char **ptr = strings; *ptr; ptr++) {
const char *str = *ptr;
total += strlen(str);
}
return total;
}
// ES6语法:import koffi from 'koffi';
const koffi = require("koffi");
const lib = koffi.load("./length.so");
const ComputeTotalLength = lib.func("int64_t ComputeTotalLength(const char **strings)");
let strings = ["Get", "Total", "Length", null];
let total = ComputeTotalLength(strings);
console.log(total); // 打印 14
默认情况下,就像对象一样,数组参数会从 JS 复制到 C,但不会从 C 复制回 JS。然而,你可以根据输出参数部分的文档更改方向。
处理 void 指针
Koffi 2.1 新增
许多 C 函数使用void *参数来传递多态对象和数组,这意味着数据格式会根据另一个参数或某种结构体标签成员而改变。
Koffi 提供了两个功能来处理这种情况:
- 你可以使用
koffi.as(value, type)告诉 Koffi 实际期望的类型,如下例所示。
- 缓冲区和 JS 的 TypedArrays 可以在任何需要指针的地方作为值使用。有关更多信息,请参见动态数组,无论是输入还是输出。
以下示例展示了如何使用koffi.as()直接将 PNG 文件头通过fread()读取到 JS 对象中。
// ES6语法:import koffi from 'koffi';
const koffi = require("koffi");
const lib = koffi.load("libc.so.6");
const FILE = koffi.opaque("FILE");
const PngHeader = koffi.pack("PngHeader", {
signature: koffi.array("uint8_t", 8),
ihdr: koffi.pack({
length: "uint32_be_t",
chunk: koffi.array("char", 4),
width: "uint32_be_t",
height: "uint32_be_t",
depth: "uint8_t",
color: "uint8_t",
compression: "uint8_t",
filter: "uint8_t",
interlace: "uint8_t",
crc: "uint32_be_t",
}),
});
const fopen = lib.func("FILE *fopen(const char *path, const char *mode)");
const fclose = lib.func("int fclose(FILE *fp)");
const fread = lib.func("size_t fread(_Out_ void *ptr, size_t size, size_t nmemb, FILE *fp)");
let filename = process.argv[2];
if (filename == null) throw new Error("Usage: node png.js <image.png>");
let hdr = {};
{
let fp = fopen(filename, "rb");
if (!fp) throw new Error(`Failed to open '${filename}'`);
try {
let len = fread(koffi.as(hdr, "PngHeader *"), 1, koffi.sizeof(PngHeader), fp);
if (len < koffi.sizeof(PngHeader)) throw new Error("Failed to read PNG header");
} finally {
fclose(fp);
}
}
console.log("PNG header:", hdr);
可释放类型
Koffi 2.0 新增
可释放类型允许你注册一个函数,该函数将在 Koffi 执行每次 C 到 JS 转换后自动调用。这可以用来避免泄漏堆分配的字符串,例如。
一些 C 函数直接通过输出参数返回堆分配的值。虽然 Koffi 会自动将值从 C 转换为 JS(转换为字符串或对象),但它不知道何时需要释放,或者如何释放。
对于不透明类型,例如FILE,这并不重要,因为你会显式地调用fclose()。但对于一些值(如字符串),它们会被 Koffi 隐式转换,你会失去对原始指针的访问。如果字符串是堆分配的,这就会导致泄漏。
为了避免这种情况,你可以通过koffi.dispose(name, type, func)创建一个可释放类型,指示 Koffi 在转换完成后对原始指针调用一个函数。这会创建一个派生自另一种类型的类型,唯一的区别是,一旦值被转换且不再需要,就会调用func。
name可以省略以创建匿名可释放类型。如果func被省略或为 null,Koffi 将使用koffi.free(ptr)(底层调用标准 C 库的free函数)。
const AnonHeapStr = koffi.disposable("str"); // 匿名类型(不能在函数原型中使用)
const NamedHeapStr = koffi.disposable("HeapStr", "str"); // 同上,但有名称,可在函数原型中使用
const ExplicitFree = koffi.disposable("HeapStr16", "str16", koffi.free); // 你可以指定任何其他JS函数
以下示例展示了如何使用派生自str的可释放类型。
// ES6语法:import koffi from 'koffi';
const koffi = require("koffi");
const lib = koffi.load("libc.so.6");
const HeapStr = koffi.disposable("str");
const strdup = lib.cdecl("strdup", HeapStr, ["str"]);
let copy = strdup("Hello!");
console.log(copy); // 打印 Hello!
在使用原型语法声明函数时,你可以使用命名的可释放类型,或者使用'!'快捷限定符与兼容类型,如下例所示。这个限定符会创建一个匿名可释放类型,调用koffi.free(ptr)。
// ES6语法:import koffi from 'koffi';
const koffi = require("koffi");
const lib = koffi.load("libc.so.6");
// 你也可以使用:const strdup = lib.func('const char *! strdup(const char *str)')
const strdup = lib.func("str! strdup(const char *str)");
let copy = strdup("World!");
console.log(copy); // 打印 World!
可释放类型只能从指针或字符串类型创建。
警告
在 Windows 上要注意:如果你的共享库使用了不同的 CRT(例如 msvcrt),内存可能是由不同的 malloc/free 实现或堆分配的,如果你使用koffi.free(),可能会导致未定义行为。
外部缓冲区(视图)
Koffi 2.11.0 新增
你可以通过koffi.view(ptr, len)访问未管理的内存。这个函数接受一个指针和长度,并创建一个 ArrayBuffer,通过它可以访问底层内存,而无需复制。
记录
一些运行时(如 Electron)禁止使用外部缓冲区。在这种情况下,尝试创建视图将触发异常。
以下是一个 Linux 示例,通过 mmaped 内存将字符串"Hello World!"写入名为"hello.txt"的文件,以演示koffi.view()的用法:
// ES6语法:import koffi from 'koffi';
const koffi = require("koffi");
const libc = koffi.load("libc.so.6");
const mode_t = koffi.alias("mode_t", "uint32_t");
const off_t = koffi.alias("off_t", "int64_t");
// 这些值在Linux上使用,可能在其他系统上有所不同
const O_RDONLY = 00000000;
const O_WRONLY = 00000001;
const O_RDWR = 00000002;
const O_CREAT = 00000100;
const O_EXCL = 00000200;
const O_CLOEXEC = 02000000;
// 这些值在Linux上使用,可能在其他系统上有所不同
const PROT_READ = 0x01;
const PROT_WRITE = 0x02;
const PROT_EXEC = 0x04;
const MAP_SHARED = 0x01;
const MAP_PRIVATE = 0x02;
const open = libc.func("int open(const char *path, int flags, uint32_t mode)");
const close = libc.func("int close(int fd)");
const ftruncate = libc.func("int ftruncate(int fd, off_t length)");
const mmap = libc.func("void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset)");
const munmap = libc.func("int munmap(void *addr, size_t length)");
const strerror = libc.func("const char *strerror(int errnum)");
write("hello.txt", "Hello World!");
function write(filename, str) {
let fd = -1;
let ptr = null;
// 处理编码字符串
str = Buffer.from(str);
try {
fd = open(filename, O_CREAT | O_EXCL | O_RDWR | O_CLOEXEC, 0644);
if (fd < 0) throw new Error(`Failed to create '${filename}': ` + strerror(koffi.errno()));
if (ftruncate(fd, str.length) < 0) throw new Error(`Failed to resize '${filename}': ` + strerror(koffi.errno()));
ptr = mmap(null, str.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (ptr == null) throw new Error(`Failed to map '${filename}' to memory: ` + strerror(koffi.errno()));
let ab = koffi.view(ptr, str.length);
let view = new Uint8Array(ab);
str.copy(view);
} finally {
if (ptr != null) munmap(ptr, str.length);
close(fd);
}
}
解包指针
你可以使用koffi.address(ptr)对指针进行操作,以获取其数值形式的 BigInt 对象。