输入参数
预计阅读时间: 15 分钟
源文档 - Input parameters
基本类型
标准类型
尽管 C 标准允许大多数整数类型的大小有所变化,但 Koffi 为大多数基本类型强制执行相同的定义,如下表所示:
| C 类型 | JS 类型 | 字节数 | 有符号/无符号 | 备注 |
|---|
| void | Undefined | 0 | | 仅作为返回类型有效 |
| int8, int8_t | Number (integer) | 1 | 有符号 | |
| uint8, uint8_t | Number (integer) | 1 | 无符号 | |
| char | Number (integer) | 1 | 有符号 | |
| uchar, unsigned char | Number (integer) | 1 | 无符号 | |
| char16, char16_t | Number (integer) | 2 | 有符号 | |
| int16, int16_t | Number (integer) | 2 | 有符号 | |
| uint16, uint16_t | Number (integer) | 2 | 无符号 | |
| short | Number (integer) | 2 | 有符号 | |
| ushort, unsigned short | Number (integer) | 2 | 无符号 | |
| char32, char32_t | Number (integer) | 4 | 有符号 | |
| int32, int32_t | Number (integer) | 4 | 有符号 | |
| uint32, uint32_t | Number (integer) | 4 | 无符号 | |
| int | Number (integer) | 4 | 有符号 | |
| uint, unsigned int | Number (integer) | 4 | 无符号 | |
| int64, int64_t | Number (integer) | 8 | 有符号 | |
| uint64, uint64_t | Number (integer) | 8 | 无符号 | |
| longlong, long long | Number (integer) | 8 | 有符号 | |
| ulonglong, unsigned long long | Number (integer) | 8 | 无符号 | |
| float32 | Number (float) | 4 | | |
| float64 | Number (float) | 8 | | |
| float | Number (float) | 4 | | |
| double | Number (float) | 8 | | |
当从 JS 转换为 C 整数时,Koffi 也接受 BigInt 值。如果值超出了 C 类型的范围,Koffi 将数字转换为未定义值。在反向转换中,当需要处理大 64 位整数时,会自动使用 BigInt 值。
Koffi 还定义了一些其他类型,这些类型的大小会根据操作系统和架构而变化:
| C 类型 | JS 类型 | 有符号/无符号 | 备注 |
|---|
| bool | Boolean | | 通常为 1 字节 |
| long | Number (integer) | 有符号 | 根据平台(LP64, LLP64)为 4 或 8 字节 |
| ulong | Number (integer) | 无符号 | 根据平台(LP64, LLP64)为 4 或 8 字节 |
| unsigned long | Number (integer) | 无符号 | 根据平台(LP64, LLP64)为 4 或 8 字节 |
| intptr | Number (integer) | 有符号 | 根据寄存器宽度为 4 或 8 字节 |
| intptr_t | Number (integer) | 有符号 | 根据寄存器宽度为 4 或 8 字节 |
| uintptr | Number (integer) | 无符号 | 根据寄存器宽度为 4 或 8 字节 |
| uintptr_t | Number (integer) | 无符号 | 根据寄存器宽度为 4 或 8 字节 |
| wchar_t | Number (integer) | 未定义 | Windows 为 2 字节,Linux、macOS 和 BSD 为 4 字节 |
| str, string | String | | JS 字符串与 UTF-8 互相转换 |
| str16, string16 | String | | JS 字符串与 UTF-16 (LE) 互相转换 |
| str32, string32 | String | | JS 字符串与 UTF-32 (LE) 互相转换 |
基本类型可以通过名称(字符串)或通过 koffi.types 指定:
// 这两行的作用相同:
let struct1 = koffi.struct({ dummy: "long" });
let struct2 = koffi.struct({ dummy: koffi.types.long });
字节序敏感的整数
新增于 Koffi 2.1
Koffi 定义了一系列字节序敏感的类型,这些类型在处理二进制数据(网络负载、二进制文件格式等)时非常有用。
| C 类型 | 字节数 | 有符号/无符号 | 字节序 |
|---|
| int16_le, int16_le_t | 2 | 有符号 | 小端字节序 |
| int16_be, int16_be_t | 2 | 有符号 | 大端字节序 |
| uint16_le, uint16_le_t | 2 | 无符号 | 小端字节序 |
| uint16_be, uint16_be_t | 2 | 无符号 | 大端字节序 |
| int32_le, int32_le_t | 4 | 有符号 | 小端字节序 |
| int32_be, int32_be_t | 4 | 有符号 | 大端字节序 |
| uint32_le, uint32_le_t | 4 | 无符号 | 小端字节序 |
| uint32_be, uint32_be_t | 4 | 无符号 | 大端字节序 |
| int64_le, int64_le_t | 8 | 有符号 | 小端字节序 |
| int64_be, int64_be_t | 8 | 有符号 | 大端字节序 |
| uint64_le, uint64_le_t | 8 | 无符号 | 小端字节序 |
| uint64_be, uint64_be_t | 8 | 无符号 | 大端字节序 |
结构体类型
结构体定义
Koffi 可以将 JS 对象转换为 C 结构体,反之亦然。
与函数声明不同,目前创建结构体类型只有一种方法,即使用 koffi.struct() 函数。该函数接受两个参数:第一个是类型名称,第二个是一个包含结构体成员名称和类型的对象。你可以省略第一个参数来声明一个匿名结构体。
以下示例展示了如何在 C 和 JS 中使用 Koffi 声明相同的结构体:
typedef struct A {
int a;
char b;
const char *c;
struct {
double d1;
double d2;
} d;
} A;
const A = koffi.struct("A", {
a: "int",
b: "char",
c: "const char *", // Koffi 不关心 const,它会被忽略
d: koffi.struct({
d1: "double",
d2: "double",
}),
});
Koffi 自动遵循平台 C ABI 关于对齐和填充的规则。然而,如果需要,你可以通过以下方式覆盖这些规则:
- 使用
koffi.pack()(而不是 koffi.struct())将所有成员紧密打包,不进行填充
- 修改特定成员的对齐方式,如下所示
// 该结构体长度为 3 字节
const PackedStruct = koffi.pack("PackedStruct", {
a: "int8_t",
b: "int16_t",
});
// 该结构体长度为 10 字节,第二个成员的对齐要求为 8 字节
const BigStruct = koffi.struct("BigStruct", {
a: "int8_t",
b: [8, "int16_t"],
});
声明结构体后,你可以通过名称(使用字符串,就像处理基本类型一样)或通过 koffi.struct() 返回的值来使用它。对于匿名结构体,只能使用后者。
// 以下两个函数声明是等效的,都声明了一个接受 A 类型值并返回 A 类型的函数
const Function1 = lib.func("A Function(A value)");
const Function2 = lib.func("Function", A, [A]);
不透明类型
许多 C 库使用某种面向对象的 API,其中有一对函数专门用于创建和销毁对象。一个明显的例子可以在 stdio.h 中找到,即不透明的 FILE * 指针。你可以使用 fopen() 和 fclose() 打开和关闭文件,并使用其他函数(如 fread() 或 ftell())操作不透明指针。
在 Koffi 中,你可以通过不透明类型来管理这种情况。使用 koffi.opaque(name) 声明不透明类型,并将指向该类型的指针用作返回类型或某种输出参数(双指针)。
记录
不透明类型在 Koffi 2.0 和 Koffi 2.1 中发生了变化。
在 Koffi 1.x 中,不透明句柄的定义方式使其可以直接用作参数和返回类型,隐藏了底层指针。
现在,你必须通过指针使用它们,并使用数组作为输出参数。这在下面的示例中展示(查看 JS 部分中对 ConcatNewOut 的调用),并在输出参数部分中进行了描述。
此外,你应该使用 koffi.opaque()(在 Koffi 2.1 中引入)而不是已弃用的 koffi.handle(),后者将在 Koffi 3.0 中被移除。
有关更多信息,请参阅迁移指南。
以下完整示例在 C 中实现了一个迭代字符串构建器(连接器),并从 JavaScript 中使用它输出 Hello World 和 FizzBuzz 的混合结果。构建器隐藏在一个不透明类型后面,并使用一对 C 函数 ConcatNew(或 ConcatNewOut)和 ConcatFree 创建和销毁。
// 使用以下命令编译:clang -fPIC -o handles.so -shared handles.c -Wall -O2
#include <stdlib.h>
#include <stdbool.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
typedef struct Fragment {
struct Fragment *next;
size_t len;
char str[];
} Fragment;
typedef struct Concat {
Fragment *first;
Fragment *last;
size_t total;
} Concat;
bool ConcatNewOut(Concat **out)
{
Concat *c = malloc(sizeof(*c));
if (!c) {
fprintf(stderr, "Failed to allocate memory: %s\n", strerror(errno));
return false;
}
c->first = NULL;
c->last = NULL;
c->total = 0;
*out = c;
return true;
}
Concat *ConcatNew()
{
Concat *c = NULL;
ConcatNewOut(&c);
return c;
}
void ConcatFree(Concat *c)
{
if (!c)
return;
Fragment *f = c->first;
while (f) {
Fragment *next = f->next;
free(f);
f = next;
}
free(c);
}
bool ConcatAppend(Concat *c, const char *frag)
{
size_t len = strlen(frag);
Fragment *f = malloc(sizeof(*f) + len + 1);
if (!f) {
fprintf(stderr, "Failed to allocate memory: %s\n", strerror(errno));
return false;
}
f->next = NULL;
if (c->last) {
c->last->next = f;
} else {
c->first = f;
}
c->last = f;
c->total += len;
f->len = len;
memcpy(f->str, frag, len);
f->str[len] = 0;
return true;
}
const char *ConcatBuild(Concat *c)
{
Fragment *r = malloc(sizeof(*r) + c->total + 1);
if (!r) {
fprintf(stderr, "Failed to allocate memory: %s\n", strerror(errno));
return NULL;
}
r->next = NULL;
r->len = 0;
Fragment *f = c->first;
while (f) {
Fragment *next = f->next;
memcpy(r->str + r->len, f->str, f->len);
r->len += f->len;
free(f);
f = next;
}
r->str[r->len] = 0;
c->first = r;
c->last = r;
return r->str;
}
// ES6 语法:import koffi from 'koffi';
const koffi = require("koffi");
const lib = koffi.load("./handles.so");
const Concat = koffi.opaque("Concat");
const ConcatNewOut = lib.func("bool ConcatNewOut(_Out_ Concat **out)");
const ConcatNew = lib.func("Concat *ConcatNew()");
const ConcatFree = lib.func("void ConcatFree(Concat *c)");
const ConcatAppend = lib.func("bool ConcatAppend(Concat *c, const char *frag)");
const ConcatBuild = lib.func("const char *ConcatBuild(Concat *c)");
let c = ConcatNew();
if (!c) {
// 这种方式很蠢,但它可以做到相同的事情,尝试两种版本(返回值、输出参数)
let ptr = [null];
if (!ConcatNewOut(ptr)) throw new Error("内存分配失败");
c = ptr[0];
}
try {
if (!ConcatAppend(c, "Hello... ")) throw new Error("内存分配失败");
if (!ConcatAppend(c, "World!\n")) throw new Error("内存分配失败");
for (let i = 1; i <= 30; i++) {
let frag;
if (i % 15 == 0) {
frag = "FizzBuzz";
} else if (i % 5 == 0) {
frag = "Buzz";
} else if (i % 3 == 0) {
frag = "Fizz";
} else {
frag = String(i);
}
if (!ConcatAppend(c, frag)) throw new Error("内存分配失败");
if (!ConcatAppend(c, " ")) throw new Error("内存分配失败");
}
let str = ConcatBuild(c);
if (str == null) throw new Error("内存分配失败");
console.log(str);
} finally {
ConcatFree(c);
}
数组类型
固定大小的 C 数组
更改于 Koffi 2.7.1
固定大小的数组使用 koffi.array(type, length) 声明。与 C 一样,它们不能作为函数参数传递(它们会退化为指针),也不能按值返回。然而,你可以将它们嵌入结构体类型中。
当将数组传递给 C 或从 C 返回时,Koffi 应用以下转换规则:
- JS 到 C:Koffi 可以接受一个普通数组(例如
[1, 2])或一个正确类型的 TypedArray(例如 Uint8Array 用于 uint8_t 数组)。
- C 到 JS(返回值、输出参数、回调):Koffi 将使用 TypedArray(如果可能的话)。但你可以通过在创建数组类型时使用可选的提示参数来更改此行为:
koffi.array('uint8_t', 64, 'Array')。对于非数字类型(例如字符串或结构体数组),Koffi 创建普通数组。
参见以下示例:
// ES6 语法:import koffi from 'koffi';
const koffi = require("koffi");
// 这两个结构体完全相同,只是数组转换提示不同
const Foo1 = koffi.struct("Foo1", {
i: "int",
a16: koffi.array("int16_t", 2),
});
const Foo2 = koffi.struct("Foo2", {
i: "int",
a16: koffi.array("int16_t", 2, "Array"),
});
// 使用一个假设的 C 函数,该函数仅返回作为参数传递的结构体
const ReturnFoo1 = lib.func("Foo1 ReturnFoo(Foo1 p)");
const ReturnFoo2 = lib.func("Foo2 ReturnFoo(Foo2 p)");
console.log(ReturnFoo1({ i: 5, a16: [6, 8] })); // 打印 { i: 5, a16: Int16Array(2) [6, 8] }
console.log(ReturnFoo2({ i: 5, a16: [6, 8] })); // 打印 { i: 5, a16: [6, 8] }
你也可以在类型声明中使用类似 C 的简短语法声明数组,如下所示:
const StructType = koffi.struct("StructType", {
f8: "float [8]",
self4: "StructType *[4]",
});
记录
类似 C 的简短语法在 Koffi 2.7.1 中引入,旧版本中请使用 koffi.array()。
固定大小的字符串缓冲区
更改于 Koffi 2.9.0
Koffi 还可以将 JS 字符串转换为固定大小的数组,如下所示:
- char 数组:用 UTF-8 编码的字符串填充,必要时截断。缓冲区始终以 NUL 结尾。
- char16 (或 char16_t) 数组:用 UTF-16 编码的字符串填充,必要时截断。缓冲区始终以 NUL 结尾。
- char32 (或 char32_t) 数组:用 UTF-32 编码的字符串填充,必要时截断。缓冲区始终以 NUL 结尾。
记录
对 UTF-32 和宽字符(wchar_t)字符串的支持在 Koffi 2.9.0 中引入。
反之亦然,Koffi 也可以将 C 固定大小的缓冲区转换为 JS 字符串。对于 char、char16t 和 char32t 数组,默认情况下会进行这种转换,但你也可以通过 String 数组提示显式请求这种转换(例如 koffi.array('char', 8, 'String'))。
动态数组(指针)
在 C 中,动态大小的数组通常作为指针传递。更多关于数组指针的内容请参阅相关部分。
联合类型
联合类型的声明和使用将在后续部分中解释,这里仅简要提及,如果你需要它们。