概述

jside ffi是连接js生态和native生态的桥梁,可以让js宏加载用C/C++/Rust/Zig/go等编程语言编写的动态库,扩宽js宏的生态范围

本文档介绍jside ffi的功能和使用方法。为了准确描述各个函数和类型,本文档使用了typescript来定义api。

WPS 版本

功能

  1. 用js加载动态库和其中的函数
  2. 用js函数合成出C语言函数,用作回调函数等
  3. 用js合成C语言的结构体,数组等复合数据类型,其内存布局与C语言相同
  4. 用js计算和C语言的指针和读写堆内存
  5. 获取平台信息,如操作系统,CPU架构,指针大小等

快速入门

  1. 用C++语言(或其他native语言)编写导出函数,并编译成动态库

以windows平台为例

cpp
#include <cstdint>

#if defined(_WIN32) || defined(_WIN64)
    #define C_EXPORT __declspec(dllexport)
#else
    #define C_EXPORT __attribute__((visibility("default")))
#endif

C_EXPORT int32_t add(int32_t a, int32_t b) {
    return a + b;
}

C_EXPORT int32_t sum(int32_t count, int32_t* numbers) {
    int32_t total = 0;
    for (int32_t i = 0; i < count; ++i) {
        total += numbers[i];
    }
    return total;
}
  1. 用js宏加载函数
js
const {add, sum} = ffi.LoadLibrary("C:/path/to/library.dll",{
    add: { returnType: "int32", parameters: ["int32","int32"]},
    sum: { returnType: "int32", parameters: ["int32","pointer"]},
})
  1. 调用函数
js
console.log(add(1,2))

let array = new Int32Array([1,2,3,4])
console.log(sum(array.length, array.buffer))

定义

类型系统

类型的详细定义请参考api

基础数据类型

基础数据类型在内存中的结构等价于对应的C/C++类型。

基础数据类型包括:

部分基础数据类型有别名:

传参时,请避免整数类型溢出,以免引起不可预期的精度问题。可以使用Math.imul函数进行32位整数的乘法运算,使用Math.fround函数进行32位浮点数的运算。也可以用num|0将整数转为32位整数。 js中 num|0 可以将整数转为32位整数类型,这个整数在v8 JS引擎内部是用int32_t类型存储和计算,可以有效避免精度问题。int64和uint64类型在js中用bigint类型存储,其他整数类型用number类型存储。

相关api

typescript
type PrimaryType = "void" | "bool" | "int8" | "int16" | "int32" | "int64" | "uint8" | "uint16" | "uint32" | "uint64" | "float" | "double" | "pointer" | "string" | "u16string";

基础数据类型对应关系

jside ffijavascriptC/C++Rustziggolang
voidundefinedvoid()voidvoid
boolbooleanboolboolboolbool
int8numberint8_ti8i8int8
int16numberint16_ti16i16int16
int32numberint32_ti32i32int32
int64bigintint64_ti64i64int64
uint8numberint8_tu8u8uint8
uint16numberuint16_tu16u16uint16
uint32numberuint32_tu32u32uint32
uint64bigintuint64_tu64u64uint64
floatnumberfloatf32f32float32
doublenumberdoublef64f64float64
pointerFfiPointervoid**mut ()*anyopaqueunsafe.Pointer
stringstringchar*CString[*:0]u8*C.char
u16stringstringchar16_t*U16CString[*:0]u16*C.char16_t

指针类型

指针等价于C语言的void*指针。调用函数传参时,指针(FFI.FfiPointer)、ArrayBuffer、闭包(FFI.FfiClosure)、null都可以被转为C语言的void*类型。指针可以与bigint类型相互转换。

如果元素类型是一种结构体类型,那么指针类型会带有字段访问器,可以用js对象字段访问的方法操作结构体内存。

PointerValue是动态合成的js类型,其read函数可以进行额外的类型转换,比如将结构体值读取并转化为一个实现StructValue接口的js对象。其Write函数可以自动处理FunctionValue对象。 而FFI.FfiPointer是C++层实现的类型,其Read函数只能读取基础数据类型。其他函数也类似。

指针对应的基础数据类型是"pointer"

相关api

typescript
    
declare namespace FFI {

    // 不可变的C语言指针类型, 相当于C语言的void*
    interface FfiPointer {
        // 读取内存. 
        Read(type: PrimaryType, offset: NumberLike): Value;
        // 写入内存
        Write(type: PrimaryType, offset: NumberLike, value: Value): void;
        // 指针转数字
        ToBigInt(): bigint | number;
        // 指针加法操作
        AddOffset(offset: NumberLike): FfiPointer;
        // C语言字符串指针(const char*)转js字符串
        DerefString(): string;
        // C语言宽字符串指针(const char16_t*)转js字符串
        DerefU16String(): string;
    }

    // 数值转指针
    function BigIntToPointer(value: bigint | number): FfiPointer;
}
declare namespace ffi {
    // 这些类型可以作为函数参数, 能自动转为指针类型
    type FfiPointerLike = FfiPointer | ArrayBuffer | FfiClosure | null;
    // 创建指针类型. 
    function Pointer(inner: TypeDecl): PointerType
    // 指针值. 可以为js的null
    type PointerValue = FFI.FfiPointer & {
        type: PointerType,
        // 相当于C语言指针的加法运算. index默认为0
        ref(index?: number): PointerValue,
        // 相当于C语言的指针读取, 或数组元素读取. index默认为0
        read(index?: number): any,
        // 相当于C语言的指针写入, 或数组元素写入. index默认为0
        write(value: any, index?: number): void,
    } | null;
    
    // 指针类型
    interface PointerType extends Type<PointerValue> {
        kind: 'pointer',
        pointeeType: Type<any>,
        create(pointer: FFI.FfiPointer | null): PointerValue,
        // 创建null指针值
        null(): PointerValue,
    }
}

函数类型

函数类型是指由C语言函数指针封装成的js函数。函数与闭包的JS封装类型统一,都是FunctionValue,但值的字段类型有区别。用isJSFunction字段区分函数和闭包。

函数类型对应的基础数据类型是pointer,C函数对象无法被隐式转换成C函数指针。函数类型不能作为实参,推荐使用其指针作为实参。可以通过ToPointer()函数显式获取函数指针。

相关api

typescript
    // 创建函数类型. 
    function Function(typeinfo: FunctionTypeDecl): FunctionType;
    // 函数类型的实例, 是一个带有额外属性的js函数, 或者为null
    type FunctionValue = ((...args: FFI.Value[]) => FFI.Value | undefined) & {
        type: FunctionType,
    } & (
            { isJSFunction: false, inner: FFI.FfiFunction }
            | { isJSFunction: true, inner: FFI.FfiClosure }
        ) | null;
    
    // 函数类型
    interface FunctionType extends Type<FunctionValue> {
        kind: 'function',
        // 参数类型
        parametersType: Type<any>[],
        // 返回值类型
        returnType: Type<any>,
        // 调用约定
        abi: FFI.Abi,
        // js函数转闭包对象
        createJsFunction(value: (...args: FFI.Value[]) => FFI.Value | undefined): FunctionValue,
        // C函数指针转js函数
        wrapCFunction(value: FFI.FfiPointer): FunctionValue,
    }

闭包类型

闭包类型的底层类型与函数类型不同,但 js 层将闭包和函数类型使用了同一个封装类型:FunctionValue,和同一中类型元数据类型FunctionType。可以使用FunctionType.createJsFunction创建闭包类型。

闭包类型对应的基础数据类型是pointerFFI.FfiPointer作为指针类型的参数时,会被隐式转换成C语言的函数指针。如果函数参数声明为一个函数类型,那么函数调用时会自动从FunctionValue中取出FFI.FfiClosure并转换成C语言的函数指针。

例子

javascript
let echoType = ffi.Function({
    abi: "",
    returnType: "int32",
    parameters: ["int32"],
})

let echoFunction = echoType.createJsFunction((arg0) => {
    return arg0
})

结构体

结构体类型是指C语言式结构体。包含C++的无虚函数的类、Rust的#[repr(C)]的结构体、zig的结构体。C语言式结构体不会重排序字段,且每个字段都对齐,结构体大小对齐到其对齐数。

js结构体值是一个ArrayBuffer。js封装的结构体的值对象上,每个字段都有访问器,可以直接通过js对象字段的语法获取或修改C结构体字段的值。

结构体类型不对应基础数据类型,不能直接作为参数类型。需要时,请使用ffi.Pointer(YourStructType)来创建其指针类型,用指针做参数。

相关api

typescript
    // 创建结构体类型. 返回值本身是个类构造函数, 可以用new创建值
    function Struct(fields: FieldDecl[]): StructType  & (new (value?: Record<string, any>) => StructValue);

    // 结构体值
    type StructValue = ArrayBuffer & {
        type: StructType,
        // 转为js对象
        toObject(): object,
    }
    
    // 结构体类型
    interface StructType extends Type<StructValue> {
        kind: 'struct',
        // 字段列表
        fields: RawFieldDecl[],
        // 按照对象的键值对创建结构体值
        create(value?: object): StructValue,
    }

例子

js
let Vector3 = ffi.Struct([
    { name: "x", type: "float" },
    { name: "y", type: "float" },
    { name: "z", type: "float" },
])
let vector3 = new Vector3 ({x:4,y:5,z:6})
vector3.x = 1.0
vector3.y = 2.0
vector3.z = 3.0
console.log(vector3.toObject().x)

数组

数组类型是指C语言式定长数组。abi上等价于元素连续存储且第一个元素地址代表数组首地址的内存块。它在内存中是一块连续的空间,存储着相同类型的元素,访问数组元素时可通过数组首地址和偏移量来定位。

js中数组的值是一个ArrayBuffer。在js中,通过操作这个ArrayBuffer就可以对数组元素进行读取和修改。

定长数组类型不对应基础数据类型,不能直接作为参数类型。需要时,请使用ffi.Pointer(YourArrayType)来创建指针类型,并用指针作参数。

相关api

typescript
    // 创建定长数组类型. 返回值本身是个类构造函数, 可以用new创建值
    function Array(inner: TypeDecl, length: number): ArrayType & (new (value?: any[]) => ArrayValue); 
    // 定长数组值
    type ArrayValue = ArrayBuffer & {
        type: ArrayType,
        // 获取元素指针. index默认为0
        ref(index?: number): PointerValue,
        // 解码出元素的js值
        get(index: number): any,
        // 编码js值到元素
        set(index: number, value: any): void,
    }
    
    // 定长数组类型
    interface ArrayType extends Type<ArrayValue> {
        kind: 'array',
        // 元素类型
        elementType: Type<any>,
        // 数组长度
        arrayLength: number,
        // 从js数组
        create(value: FFI.Value[]): ArrayValue,
    }

例子

javascript
let Vector4 = ffi.Array("int32", 4)
let value = new Vector4([1,2,3,4])
value.set(1,5)
console.log(value.get(1))

缓冲区

缓存区类型是一块C式内存空间,可以存储指定数据类型的值,其存储内容与C语言内存布局相同。常用于函数输出数据的内存空间。 js中的缓冲区值是一个带有额外的read、write、ref等属性的ArrayBuffer。 缓冲区类型对应的基础数据类型是"pointer",作为参数时会被隐式转换成指针。

相关api

typescript
    // 创建缓冲区类型. 返回值本身是个类构造函数, 可以用new创建值
    function Buffer(inner: TypeDecl): BufferType & (new (value?: any) => BufferValue);

    // 缓冲区类型, 是一个ArrayBuffer, 用于持有一个C内存结构. 可以作为C api通过指针传出数据的缓冲区
    type BufferValue = ArrayBuffer & {
        type: BufferType,
        // 转指针
        ref(): PointerValue,
        // 解码出js值
        read(): any,
        // 编码js值
        write(value: any): void,
    };
    
    // 缓冲区类型
    interface BufferType extends Type<BufferValue> {
        kind: 'buffer',
        innerType: Type<any>,
        create(value: any): BufferValue,
    }

例子

javascript
let BufferInt32 = ffi.Buffer("int32")
let value = new BufferInt32()
value.write(1)
console.log(value.read())
console.log(value.ref().ToBigInt())

联合体

联合体类型是值C语言式联合体,是多种数据类型在内存空间上重合的结构,其每个变体的指针都是联合体指针。

js中联合体的值是一个ArrayBuffer。其每个变体都可以通过js访问器,用访问js对象字段的方式读取和修改。

联合体类型不对应基础数据类型,不能直接作为参数类型。需要时,请使用其指针类型。

相关api

typescript
    // 创建联合体类型. 返回值本身是个类构造函数, 可以用new创建值
    function Union(fields: FieldDecl[]): UnionType & (new () => UnionValue);
    
    // 联合体值
    type UnionValue = ArrayBuffer & {
        type: UnionType
    }
    
    // 联合体类型
    interface UnionType extends Type<UnionValue> {
        kind: 'union',
        // 变体列表
        fields: RawFieldDecl[],
    }

例子

javascript
let UnionType = ffi.Union([
    { name: "int32", type: "int32"},
    { name: "f32", type: "float" },
])
let value = new UnionType 
value.f32 = Math.PI
console.log(value.int32)

模板

jside ffi未提供任何模板相关机制,但可以仿照zig语言的模板机制,用函数来实现模板。

例子

例如一个简单的结构体模板

cpp
template<typename T>
struct Vector3 {
    T x;
    T y;
    T z;
};

写成FFI的模板结构体,用函数表示模板。 注意ffi.Struct函数本身有一定性能开销,建议将模板实例化后的结构体类型缓存起来重复使用。

javascript
let Vector3 = (T) => ffi.Struct([
    { name: "x", type: T },
    { name: "y", type: T },
    { name: "z", type: T },
])
let Vector3f = Vector3("float")
let vector3 = new Vector3f ({x:4,y:5,z:6})

基本原理

使用时需要注意生命周期问题和内存越界问题。ArrayBuffer的生命周期受到GC机制管理,需要避免过早被回收导致野指针问题。如果用ArrayBuffer代替了malloc,那么建议在js中保存ArrayBuffer的引用,防止其过早的被GC回收。注意内存分配与释放的方法统一,避免让C/C++用free或delete释放了ArrayBuffer分配的内存。

二进制接口

abi是应用程序二进制接口,规定了不同编程语言间函数调用的约定和规范。CPU类型以及C语言的调用约定决定了函数的abi类型。 abi 决定了函数参数如何传递,返回值如何传递,寄存器的使用等。 如果函数定义中没有规定abi类型,一般使用默认abi。

相关api

typescript
    type Abi = "" // 默认abi
        | "stdcall" | "thiscall" | "fastcall" | "cdecl" | "pascal" | "sysv" // x86和x64的abi,包括Intel和AMD,兆芯,海光等CPU
        | "win64" /*非windows平台*/ | "gnuw64" | "unix64" // x64架构
        | "win64" | "sysv" // aarch64架构,包括,飞腾,鲲鹏,麒麟,苹果M1/M2等CPU
        | "n64" | "n64_soft_float" // mips64el架构,龙芯3A4000及以下
        | "lp64s" | "lp64f" | "lp64d";  // loongarch64架构,龙芯3A5000及以上

动态库加载

当调用LoadLibrary函数时, wps会根据提供的动态库名称搜索动态库文件, 并加载到内存中. 然后根据提供的函数声明, 获取动态库中的函数地址, 并封装成js函数。

推荐使用ffi.LoadLibrary函数加载动态库, 而不是使用FFI.LoadLibrary函数。前者可以同时加载多个函数, 并且可以指定函数的签名,还可以为C语言函数创建js代理函数,代理函数能够自动转换参数类型,更加易用。

动态库查找

动态库名可以是绝对路径,也可以是文件名。如果使用文件名, wps按照各操作系统的路径查找规则查早动态库,获取绝对路径。

文件名会被自动加上当前系统的动态库文件后缀。

如果动态库不是绝对路径,wps会按照以下顺序查找动态库文件:

出于安全策略限制,禁止加载wps的office6目录下的WPS核心动态库。LoadLibrary的安全策略未来有可能会调整

相关api

typescript
declare namespace ffi {
    // 加载动态库, 以及动态库函数
    function LoadLibrary(name: string, functions: Record<string, LibraryFunctionDecl>): FFI.FFILibrary & Record<string, (...args: any[]) => any>;
    
    interface FunctionTypeDecl {
        parameters?: TypeDecl[],
        returnType?: TypeDecl,
        abi?: FFI.Abi
    }

    interface LibraryFunctionDecl extends FunctionTypeDecl {
        // 函数在动态库中的符号名, 如果与函数名相同, 则可以省略
        symbol?: string,
    }
}
declare namespace FFI {
    // 加载动态库. name可以是绝对路径或名字
    function LoadLibrary(name: string): FFILibrary;

    // 动态库对象
    interface FFILibrary {
        // 加载动态库函数
        LoadFunction(name: string, symbol: string, abi: Abi, returnType: PrimaryType, argumentsType: PrimaryType[]): FfiFunction;
    }
}

局限性

参数无法直接传递结构体

不支持通过参数直接传递结构体。参数传递结构体的行为与编译器和编程语言关联太大。

建议改为使用结构体指针。

不支持可变数量参数

不支持C语言式可变数量参数。

不支持C++式异常处理

jside本身会捕获C++异常,但由于C++的abi兼容性等问题,无法获取异常对象和调用栈等信息。因此,在调用的C++代码中应避免使用C++式异常处理,可考虑使用返回错误码的方式来处理错误情况。 如果在函数调用过程中捕获到C++异常,那么jside会抛出js异常,但不携带C++异常相关信息。

不支持js的异常捕获

由于C语言没有异常机制,且不少C语言程序在C++异常面前没有异常安全性,所以闭包调用机制不支持将JS异常转换为C++异常。 如果闭包调用的JS函数抛出异常,那么闭包对应的C函数会返回未初始化值。 推荐在闭包调用的JS函数中捕获异常,输出异常日志或者用alert显示异常信息,然后按照C语言的方式返回。如果C api本身有某种错误码,推荐返回错误码给调用者。

只支持部分调用约定和C语言式数据类型

如果需要更加复杂的数据类型或者golang等语言的函数调用约定,可以用C语言编写一个封装层

异常处理

C函数调用失败

完整api

如果使用vscode等支持lsp的ide来编写js宏,可以将完整api复制到一个typescript文件中,然后在vscode等ide中查看。让ai读取这些api,借助ai辅助编程。

推荐使用ffi命名空间的api,而不是FFI命名空间的api。前者进行了细致的封装,功能更强大,且更易用。

typescript
// 在C++中实现的api, 其类型转换受到C++层的限制. 比如bigint类型的返回值实际可能被转为number
declare namespace FFI {
    type Abi = "" // 默认abi
        | "stdcall" | "thiscall" | "fastcall" | "cdecl" | "pascal" | "sysv" // x86和x64的abi,包括Intel和AMD,兆芯,海光等CPU
        | "win64" /*非windows平台*/ | "gnuw64" | "unix64" // x64架构
        | "win64" | "sysv" // aarch64架构,包括,飞腾,鲲鹏,麒麟,苹果M1/M2等CPU
        | "n64" | "n64_soft_float" // mips64el架构,龙芯3A4000及以下
        | "lp64s" | "lp64f" | "lp64d";  // loongarch64架构,龙芯3A5000及以上
    type PrimaryType = "void" | "bool" | "int8" | "int16" | "int32" | "int64" | "uint8" | "uint16" | "uint32" | "uint64" | "float" | "double" | "pointer" | "string" | "u16string";
    // 大多数jsapi可以自动转为整数的类型
    type NumberLike = number | bigint | string;

    // 不可变的C语言指针类型, 相当于C语言的void*
    interface FfiPointer {
        // 读取内存. 
        Read(type: PrimaryType, offset: NumberLike): Value;
        // 写入内存
        Write(type: PrimaryType, offset: NumberLike, value: Value): void;
        // 指针转数字
        ToBigInt(): bigint | number;
        // 指针加法操作
        AddOffset(offset: NumberLike): FfiPointer;
        // C语言字符串指针(const char*)转js字符串
        DerefString(): string;
        // C语言宽字符串指针(const char16_t*)转js字符串
        DerefU16String(): string;
    }

    // 闭包类型, 是可调用的函数对象, 内部包含运行期生成的机器码
    interface FfiClosure {
        // 用于设置回调函数. 闭包的机器码被调用时会调用回调函数。
        // 如果手动用CreateCallback创建闭包对象, 则必须设置此回调函数。否则调用闭包时只会返回未定义值给C函数
        OnCall?: (...args: Value[]) => Value | undefined;
        // 获取内部的机器码的指针
        ToPointer(): FfiPointer;
    }

    // 函数对象, 用于调用函数
    interface FfiFunction {
        // 调用函数. 要求参数数量正确, 参数类型正确
        Call(...args: Value[]): Value | undefined;
        // 获取函数指针
        ToPointer(): FfiPointer;
    }

    // 动态库对象
    interface FFILibrary {
        // 加载动态库函数
        LoadFunction(name: string, symbol: string, abi: Abi, returnType: PrimaryType, argumentsType: PrimaryType[]): FfiFunction;
    }

    type FfiPointerLike = FfiPointer | ArrayBuffer | FfiClosure | null;
    type Value = number | bigint | string | boolean | FfiPointerLike;
    
    // 加载动态库. name可以是绝对路径或名字
    function LoadLibrary(name: string): FFILibrary;
    // 创建闭包对象
    function CreateCallback(abi: Abi, returnType: PrimaryType, argumentsType: PrimaryType[]): FfiClosure;
    // 函数指针转为函数
    function CreateFunction(pointer: FfiPointer, abi: Abi, returnType: PrimaryType, argumentsType: PrimaryType[]): FfiFunction;

    // 获取js缓冲区中特定未知的指针
    function DerefPointer(array: ArrayBuffer, offset?: number): FfiPointer;
    // 获取js缓冲区开头的指针
    function RefPointer(pointer: ArrayBuffer | FfiPointer): FfiPointer;
    // 数值转指针
    function BigIntToPointer(value: bigint | number): FfiPointer;
    // 分配内存. size会被转为无符号整数
    function Malloc(size: number): FfiPointer;
    // 释放分配的内存
    function Free(pointer: FfiPointer): void;
    
    // 复制内存
    // dest: 复制目标
    // destOffset: 复制目标上的偏移量
    // destSize: 复制目标上的复制区域大小
    // src: 复制源
    // srcOffset: 复制源上的偏移量
    // srcSize: 复制源上的复制区域大小
    function Memcpy(dest: FfiPointer | ArrayBuffer, destOffset: NumberLike, destSize: number, src: FfiPointer | ArrayBuffer, srcOffset: number, secSize: number): void;

    // 当前系统的指针大小(byte). 值为4或8,对应32位或64位WPS
    const PointerSize: number;
}

// ffi模块是在js中实现的, 是对FFI模块的一层封装. 其使用方式更复合js习惯
declare namespace ffi {
    // 转换函数调用的参数
    type ParamsEncoder = (value: any) => any;
    
    // 各种api接受的类型值的类型
    type TypeDecl = Type<any> | FFI.PrimaryType
    
    // 把值编码到Uint8Array中
    type EncoderFn<V> = (buffer: Uint8Array<ArrayBuffer>, offset: number, value: V) => void;
    // 从Uint8Array解码出值
    type DecoderFn<V> = (buffer: Uint8Array<ArrayBuffer>, offset: number) => V;
    // 转换函数调用的参数, 和闭包的返回值
    type ParamsEncodersFn<V> = (value: V) => any;
    // 转换函数调用的返回值, 和闭包的参数
    type ReturnDecodersFn<V> = (value: any) => V;
    // 从指针读取值
    type Reader<V> = (pointer: FFI.FfiPointer, offset: number) => V;
    // 向指针写入值
    type Writer<V> = (pointer: FFI.FfiPointer, offset: number, value: V) => void;
    
    // 类型的定义
    interface Type<V> {
        primaryType: FFI.PrimaryType,
        // 类型大小 以byte为单位
        size: number,
        // 类型的对齐值, 必须是2的n次方
        align: number,
        // Uint8Array编码器
        encoder: EncoderFn<V>,
        // Uint8Array解码器
        decoder: DecoderFn<V>,
        // 指针读取器
        reader: Reader<V>,
        // 指针写入器
        writer: Writer<V>,
        // 参数转换器. 如果为undefined, 则不会转换
        paramEncoder?: ParamsEncodersFn<V>,
        // 返回值转换器. 如果为undefined, 则不会转换
        returnValueDecoder?: ReturnDecodersFn<V>,
    }
    
    // 函数类型的实例, 是一个带有额外属性的js函数, 或者为null
    type FunctionValue = ((...args: FFI.Value[]) => FFI.Value | undefined) & {
        type: FunctionType,
    } & (
            { isJSFunction: false, inner: FFI.FfiFunction }
            | { isJSFunction: true, inner: FFI.FfiClosure }
        ) | null;
    
    // 函数类型
    interface FunctionType extends Type<FunctionValue> {
        kind: 'function',
        // 参数类型
        parametersType: Type<any>[],
        // 返回值类型
        returnType: Type<any>,
        // 调用约定
        abi: FFI.Abi,
        // js函数转闭包对象
        createJsFunction(value: (...args: FFI.Value[]) => FFI.Value | undefined): FunctionValue,
        // C函数指针转js函数
        wrapCFunction(value: FFI.FfiPointer): FunctionValue,
    }
    
    // 缓冲区类型, 是一个ArrayBuffer, 用于持有一个C内存结构. 可以作为C api通过指针传出数据的缓冲区
    type BufferValue = ArrayBuffer & {
        type: BufferType,
        // 转指针
        ref(): PointerValue,
        // 解码出js值
        read(): any,
        // 编码js值
        write(value: any): void,
    };
    
    // 缓冲区类型
    interface BufferType extends Type<BufferValue> {
        kind: 'buffer',
        innerType: Type<any>,
        create(value: any): BufferValue,
    }
    
    // 结构体值
    type StructValue = ArrayBuffer & {
        type: StructType,
        // 转为js对象
        toObject(): object,
    }
    
    // 结构体类型
    interface StructType extends Type<StructValue> {
        kind: 'struct',
        // 字段列表
        fields: RawFieldDecl[],
        // 按照对象的键值对创建结构体值
        create(value?: object): StructValue,
    }
    
    // 联合体值
    type UnionValue = ArrayBuffer & {
        type: UnionType
    }
    
    // 联合体类型
    interface UnionType extends Type<UnionValue> {
        kind: 'union',
        // 变体列表
        fields: RawFieldDecl[],
    }
    
    // 指针值. 可以为js的null
    type PointerValue = FFI.FfiPointer & {
        type: PointerType,
        // 相当于C语言指针的加法运算. index默认为0
        ref(index?: number): PointerValue,
        // 相当于C语言的指针读取, 或数组元素读取. index默认为0
        read(index?: number): any,
        // 相当于C语言的指针写入, 或数组元素写入. index默认为0
        write(value: any, index?: number): void,
    } | null;
    
    // 指针类型
    interface PointerType extends Type<PointerValue> {
        kind: 'pointer',
        pointeeType: Type<any>,
        create(pointer: FFI.FfiPointer | null): PointerValue,
        // 创建null指针值
        null(): PointerValue,
    }
    
    // 定长数组值
    type ArrayValue = ArrayBuffer & {
        type: ArrayType,
        // 获取元素指针. index默认为0
        ref(index?: number): PointerValue,
        // 解码出元素的js值
        get(index: number): any,
        // 编码js值到元素
        set(index: number, value: any): void,
    }
    
    // 定长数组类型
    interface ArrayType extends Type<ArrayValue> {
        kind: 'array',
        // 元素类型
        elementType: Type<any>,
        // 数组长度
        arrayLength: number,
        // 从js数组
        create(value: FFI.Value[]): ArrayValue,
    }
    
    type BottomType = (Type<any> & { kind: undefined }) | FunctionType | StructType | UnionType | PointerType | ArrayType | BufferType;
    
    interface FieldDecl {
        name: string,
        type: TypeDecl,
    }
    
    interface ResolvedField {
        name: string,
        type: Type<any>,
    }
    
    interface RawFieldDecl {
        name: string,
        type: Type<any>,
        offset: number,
    }
    
    interface FunctionTypeDecl {
        parameters?: TypeDecl[],
        returnType?: TypeDecl,
        abi?: FFI.Abi
    }

    interface LibraryFunctionDecl extends FunctionTypeDecl {
        // 函数在动态库中的符号名, 如果与函数名相同, 则可以省略
        symbol?: string,
    }

    // 各种内置类型
    let types: {
        void: Type<void>,
        bool: Type<boolean>,
        int8: Type<number>,
        int16: Type<number>,
        int32: Type<number>,
        int64: Type<bigint>,
        uint8: Type<number>,
        uint16: Type<number>,
        uint32: Type<number>,    
        uint64: Type<bigint>,
        float: Type<number>,
        double: Type<number>,
        string: Type<string>,
        "char*": Type<string>,
        u16string: Type<string>,
        pointer: Type<FFI.FfiPointer | null>,
        "void*": Type<FFI.FfiPointer | null>,
        size_t: Type<number> | Type<bigint>,
    };
    // 加载动态库, 以及动态库函数
    function LoadLibrary(name: string, functions: Record<string, LibraryFunctionDecl>): FFI.FFILibrary & Record<string, (...args: any[]) => any>;
    // 解析类型声明
    function resolveType(type: TypeDecl): Type<any>;
    // 指针大小
    let pointerSize: number;
    // 创建结构体类型. 返回值本身是个类构造函数, 可以用new创建值
    function Struct(fields: FieldDecl[]): StructType  & (new (value?: Record<string, any>) => StructValue);
    // 创建联合体类型. 返回值本身是个类构造函数, 可以用new创建值
    function Union(fields: FieldDecl[]): UnionType & (new () => UnionValue);
    // 创建函数类型. 
    function Function(typeinfo: FunctionTypeDecl): FunctionType;
    // 创建定长数组类型. 返回值本身是个类构造函数, 可以用new创建值
    function Array(inner: TypeDecl, length: number): ArrayType & (new (value?: any[]) => ArrayValue); 
    // 创建指针类型. 
    function Pointer(inner: TypeDecl): PointerType
    // 创建缓冲区类型. 返回值本身是个类构造函数, 可以用new创建值
    function Buffer(inner: TypeDecl): BufferType & (new (value?: any) => BufferValue);
}

例子

以操作sqlite数据库为例。sqlite是一个轻量级的嵌入式数据库,广泛应用于各种软件中。sqlite以动态库形式加载,通过C语言API操作sqlite。

提前安装sqlite3

javascript
// sqlite 的api中没有结构体, 定长数组和联合体类型. 几乎所有参数都是C语言基础数据类型
    
// 数据库指针类型
let DbPtrType = "pointer"
// 数据库二重指针类型,传参时其值可以被转为二重指针
let DbPtrPtrType = ffi.Buffer(DbPtrType)
// 字段名列表类型
let FieldArray = ffi.Pointer("string")
// 值列表类型
let DataArray = ffi.Pointer("string")
// sqlite select时的回调函数类型, 传参时会被转为可调用的C函数指针类型
let CallbackType = ffi.Function({ffi:"",returnType:"int32",parameters:[DbPtrType, "int32", FieldArray, DataArray]})
// 报错字符串类型,传参时其值可以被转为二重指针
let ErrorStringType = ffi.Buffer("string")

// 按照平台设置动态库路径
let os = Platform.OS()
let arch = Platform.Arch()
let sqlite_path = ""
if (os=="Darwin") {
    sqlite_path = "/usr/lib/sqlite3/libtclsqlite3.dylib"
}else if(os=="Linux"){
    // 以ubuntu的路径为例
    if (arch == "aarch64")
    {
        sqlite_path = "/usr/lib/aarch64-linux-gnu/libsqlite3.so"
    }else
        sqlite_path = "/usr/lib/x86_64-linux-gnu/libsqlite3.so"
}else {
    // 用choco安装sqlite后, 其动态库在这个路径
    // 动态库的位数要与wps匹配
    if (Platform.PointerSize() == 4)
        // 替换成你的下载的路径
        sqlite_path = "D:/temp/sqlite3.dll"
    else
        sqlite_path = "C:/ProgramData/chocolatey/lib/SQLite/tools/sqlite3.dll"
}

// 加载sqlite动态库和函数
const sqlite_lib = ffi.LoadLibrary(sqlite_path, {
    // 用Buffer类型(DbPtrPtrType)来代替二重指针类型
    sqlite3_open: { returnType: "int32", parameters: ["string",DbPtrPtrType]},
    sqlite3_close: { returnType: "int32", parameters: [DbPtrType]},
    // 这里使用CallbackType类型而不是pointer, 传参时自动把FunctionValue类型转为C++能识别的FfiClosure, 进而被转为函数指针
    sqlite3_exec: {returnType: "int32",parameters: [DbPtrType,"string",CallbackType,"pointer",ErrorStringType ]},
})
// 枚举值定义
const SQLITE_OK = 0
const SQLITE_DONE = 101

// 对sqlite的封装
class Sqlite{
    constructor(path){
        let db_ptr_ptr = new DbPtrPtrType()
        // 打开数据库. db_ptr_ptr是个ArrayBuffer, 会被转为指针
        this.call_api(()=>sqlite_lib.sqlite3_open(path, db_ptr_ptr))
        // 取出db_ptr_ptr内部被写入的指针
        this.db = db_ptr_ptr.read()
        
    }
    // 对错误处理流程进行封装
    call_api(func) {
        let code = func.apply(this)
        if (code != SQLITE_OK && code != SQLITE_DONE){
            throw new Error("sqlite api error: "+code)
        }
    }
    // 关闭数据库
    close(){
        this.call_api(()=>sqlite_lib.sqlite3_close(this.db))
    }
    // 调用sql语句
    exec(sql, callback){
        console.log("exec: " + sql)
        let error_string_ptr = new ErrorStringType()
        // sqlite执行sql语句. 这里闭包类型(CallbackType)会被转为函数指针, Buffer类型(ErrorStringType)会被取指针
        let code = sqlite_lib.sqlite3_exec(this.db,sql,callback || null,null,error_string_ptr)
        // 错误处理
        if (code != SQLITE_OK && code != SQLITE_DONE){
            let message = error_string_ptr.read()
            throw new Error(`sqlite api error: ${code}, message: ${message}`)
        }
    }
    // 调用select语句, 并获取数据
    // types是字段类型的字符串解析函数的数组
    select(sql,types) {
        let table = []
        // 合成闭包
        let callback = CallbackType.createJsFunction((p, cols /*: number*/ , argv /*: PointerValue*/, colv /*: PointerValue*/)=>{
            // 闭包内部需要用try-catch , 因为js异常和C++异常无法被C语言处理
            try{
                let record = {}
                // 遍历C语言式数组指针
                for (let i =0; i< cols; i++){
                    // 读取字段名
                    let colName = colv.read(i)
                    // 读取字段值
                    let colData = argv.read(i)
                    record[colName] = types[i](colData)
                }
                table.push(record)
                return SQLITE_OK;
            } catch (e) {
                alert(e)
            }
            
        })
        this.exec(sql,callback)
        
        return table
    }
}

function macro_main(){
    
    let db_path = ":memory:"
    let db = new Sqlite(db_path)
    // js没有RAII, 需要用finally语句来关闭资源
    try {
        // 创建数据库表
        db.exec("CREATE TABLE IF NOT EXISTS foo (field1 REAL, field2 REAL);")
        db.exec("delete from foo;")
        let records = [
            [1.1, 2.2],
            [3.3, 4.4],
            [5.5, 6.6],
        ] 
        // 插入记录
        for (const row of records) {
            let field1 = Number(row[0])
            let field2 = Number(row[1])
            db.exec(`INSERT INTO foo VALUES(${field1}, ${field2});`)
        }
        
        // 读取数据库表
        let data = db.select("select field1, field2 from foo;",[Number, Number])
        console.log("table foo: " + JSON.stringify(data))
    } finally {
        // 关闭数据库
        db.close()
    }

}