输入参数

预计阅读时间: 15 分钟

源文档 - Input parameters

基本类型

标准类型

尽管 C 标准允许大多数整数类型的大小有所变化,但 Koffi 为大多数基本类型强制执行相同的定义,如下表所示:

C 类型JS 类型字节数有符号/无符号备注
voidUndefined0仅作为返回类型有效
int8, int8_tNumber (integer)1有符号
uint8, uint8_tNumber (integer)1无符号
charNumber (integer)1有符号
uchar, unsigned charNumber (integer)1无符号
char16, char16_tNumber (integer)2有符号
int16, int16_tNumber (integer)2有符号
uint16, uint16_tNumber (integer)2无符号
shortNumber (integer)2有符号
ushort, unsigned shortNumber (integer)2无符号
char32, char32_tNumber (integer)4有符号
int32, int32_tNumber (integer)4有符号
uint32, uint32_tNumber (integer)4无符号
intNumber (integer)4有符号
uint, unsigned intNumber (integer)4无符号
int64, int64_tNumber (integer)8有符号
uint64, uint64_tNumber (integer)8无符号
longlong, long longNumber (integer)8有符号
ulonglong, unsigned long longNumber (integer)8无符号
float32Number (float)4
float64Number (float)8
floatNumber (float)4
doubleNumber (float)8

当从 JS 转换为 C 整数时,Koffi 也接受 BigInt 值。如果值超出了 C 类型的范围,Koffi 将数字转换为未定义值。在反向转换中,当需要处理大 64 位整数时,会自动使用 BigInt 值。

Koffi 还定义了一些其他类型,这些类型的大小会根据操作系统和架构而变化:

C 类型JS 类型有符号/无符号备注
boolBoolean通常为 1 字节
longNumber (integer)有符号根据平台(LP64, LLP64)为 4 或 8 字节
ulongNumber (integer)无符号根据平台(LP64, LLP64)为 4 或 8 字节
unsigned longNumber (integer)无符号根据平台(LP64, LLP64)为 4 或 8 字节
intptrNumber (integer)有符号根据寄存器宽度为 4 或 8 字节
intptr_tNumber (integer)有符号根据寄存器宽度为 4 或 8 字节
uintptrNumber (integer)无符号根据寄存器宽度为 4 或 8 字节
uintptr_tNumber (integer)无符号根据寄存器宽度为 4 或 8 字节
wchar_tNumber (integer)未定义Windows 为 2 字节,Linux、macOS 和 BSD 为 4 字节
str, stringStringJS 字符串与 UTF-8 互相转换
str16, string16StringJS 字符串与 UTF-16 (LE) 互相转换
str32, string32StringJS 字符串与 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_t2有符号小端字节序
int16_be, int16_be_t2有符号大端字节序
uint16_le, uint16_le_t2无符号小端字节序
uint16_be, uint16_be_t2无符号大端字节序
int32_le, int32_le_t4有符号小端字节序
int32_be, int32_be_t4有符号大端字节序
uint32_le, uint32_le_t4无符号小端字节序
uint32_be, uint32_be_t4无符号大端字节序
int64_le, int64_le_t8有符号小端字节序
int64_be, int64_be_t8有符号大端字节序
uint64_le, uint64_le_t8无符号小端字节序
uint64_be, uint64_be_t8无符号大端字节序

结构体类型

结构体定义

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.0Koffi 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 中,动态大小的数组通常作为指针传递。更多关于数组指针的内容请参阅相关部分。

联合类型

联合类型的声明和使用将在后续部分中解释,这里仅简要提及,如果你需要它们。