概述
jside ffi是连接js生态和native生态的桥梁,可以让js宏加载用C/C++/Rust/Zig/go等编程语言编写的动态库,扩宽js宏的生态范围
本文档介绍jside ffi的功能和使用方法。为了准确描述各个函数和类型,本文档使用了typescript来定义api。
WPS 版本
- windows: WPS 12.1.0.23542 版本及以上
功能
- 用js加载动态库和其中的函数
- 用js函数合成出C语言函数,用作回调函数等
- 用js合成C语言的结构体,数组等复合数据类型,其内存布局与C语言相同
- 用js计算和C语言的指针和读写堆内存
- 获取平台信息,如操作系统,CPU架构,指针大小等
快速入门
- 用C++语言(或其他native语言)编写导出函数,并编译成动态库
以windows平台为例
#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;
}
- 用js宏加载函数
const {add, sum} = ffi.LoadLibrary("C:/path/to/library.dll",{
add: { returnType: "int32", parameters: ["int32","int32"]},
sum: { returnType: "int32", parameters: ["int32","pointer"]},
})
- 调用函数
console.log(add(1,2))
let array = new Int32Array([1,2,3,4])
console.log(sum(array.length, array.buffer))
定义
- ffi:foreign function interface,是不同编程语言之间相互调用函数的一种机制。
- C函数:泛指具有与C语言相同的调用约定和数据结构的函数机器码。包括C/C++的函数,zig的函数,Rust的extern "C"函数,它们都可以被js宏通过jside ffi加载和调用。
- 符号名:函数在动态库中的名称。C++函数的符号名会被编译器修饰,C语言函数的符号名与其定义的名称相同。可以使用objdump和readelf等工具查看动态库中的符号名。 可以使用http://demangler.com/网站来转换符号名和函数签名
- 动态库:一种包含可由多个程序同时使用的代码和数据的库文件,在运行时被加载到内存中,可以按照符号名动态获取其中的函数。一般的文件后缀名为
.dll(Windows系统)、.so(Linux系统、鸿蒙系统)、.dylib(macOS系统)。 - 闭包:指将js的函数合成成C语言的函数指针。它可以作为回调函数传递给C语言代码,让C语言函数可以调用JS函数。
类型系统
类型的详细定义请参考api
基础数据类型
基础数据类型在内存中的结构等价于对应的C/C++类型。
基础数据类型包括:
- void: 相当于C语言的
void类型。只能作为函数返回值类型,不能作为参数类型。 - bool: 相当于C++的bool类型。其值为0时表示false,为1时表示true。bool类型在内存中占1个字节。
- int8, int16, int32, int64: 分别对应C语言的
int8_t、int16_t、int32_t、int64_t类型。 - uint8, uint16, uint32, uint64: 分别对应C语言的
uint8_t、uint16_t、uint32_t、uint64_t类型。 - float, double: 分别对应C语言的
float和double类型。分别占4、8个字节。在js中用number类型存储。 - string:指utf8编码的C语言式字符串的指针,以'\0'结尾。
- u16string:指utf16编码的C语言式字符串的字符串的指针,以'\0'结尾。
- pointer:等价于C语言的
void*指针。指针的值在js中用一个FFI.FfiPointer类型的对象表示。
部分基础数据类型有别名:
size_t: 在32位wps上等价于uint32,在64位wps上等价于uint64char*: 等价于stringvoid*: 等价于pointer
传参时,请避免整数类型溢出,以免引起不可预期的精度问题。可以使用Math.imul函数进行32位整数的乘法运算,使用Math.fround函数进行32位浮点数的运算。也可以用num|0将整数转为32位整数。
js中 num|0 可以将整数转为32位整数类型,这个整数在v8 JS引擎内部是用int32_t类型存储和计算,可以有效避免精度问题。int64和uint64类型在js中用bigint类型存储,其他整数类型用number类型存储。
相关api
type PrimaryType = "void" | "bool" | "int8" | "int16" | "int32" | "int64" | "uint8" | "uint16" | "uint32" | "uint64" | "float" | "double" | "pointer" | "string" | "u16string";
基础数据类型对应关系
| jside ffi | javascript | C/C++ | Rust | zig | golang |
|---|---|---|---|---|---|
| void | undefined | void | () | void | void |
| bool | boolean | bool | bool | bool | bool |
| int8 | number | int8_t | i8 | i8 | int8 |
| int16 | number | int16_t | i16 | i16 | int16 |
| int32 | number | int32_t | i32 | i32 | int32 |
| int64 | bigint | int64_t | i64 | i64 | int64 |
| uint8 | number | int8_t | u8 | u8 | uint8 |
| uint16 | number | uint16_t | u16 | u16 | uint16 |
| uint32 | number | uint32_t | u32 | u32 | uint32 |
| uint64 | bigint | uint64_t | u64 | u64 | uint64 |
| float | number | float | f32 | f32 | float32 |
| double | number | double | f64 | f64 | float64 |
| pointer | FfiPointer | void* | *mut () | *anyopaque | unsafe.Pointer |
| string | string | char* | CString | [*:0]u8 | *C.char |
| u16string | string | char16_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
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
// 创建函数类型.
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创建闭包类型。
闭包类型对应的基础数据类型是pointer。FFI.FfiPointer作为指针类型的参数时,会被隐式转换成C语言的函数指针。如果函数参数声明为一个函数类型,那么函数调用时会自动从FunctionValue中取出FFI.FfiClosure并转换成C语言的函数指针。
例子
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
// 创建结构体类型. 返回值本身是个类构造函数, 可以用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,
}
例子
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
// 创建定长数组类型. 返回值本身是个类构造函数, 可以用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,
}
例子
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
// 创建缓冲区类型. 返回值本身是个类构造函数, 可以用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,
}
例子
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
// 创建联合体类型. 返回值本身是个类构造函数, 可以用new创建值
function Union(fields: FieldDecl[]): UnionType & (new () => UnionValue);
// 联合体值
type UnionValue = ArrayBuffer & {
type: UnionType
}
// 联合体类型
interface UnionType extends Type<UnionValue> {
kind: 'union',
// 变体列表
fields: RawFieldDecl[],
}
例子
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语言的模板机制,用函数来实现模板。
例子
例如一个简单的结构体模板
template<typename T>
struct Vector3 {
T x;
T y;
T z;
};
写成FFI的模板结构体,用函数表示模板。 注意ffi.Struct函数本身有一定性能开销,建议将模板实例化后的结构体类型缓存起来重复使用。
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})
基本原理
-
ffi调用:使用libffi的调用功能实现。它封装一段汇编代码,将参数按照调用约定放到指定寄存器和栈位置上,然后用call指令跳转到函数地址上执行,函数返回后将返回值从寄存器中取出并返回给js
-
ffi闭包:使用libffi的闭包功能实现。它将一段汇编编译成的机器码放到内存数据结构中,汇编被调用时间寄存器的值保存下来,并按照函数签名整理成函数参数列表,然后转换参数类型并调用JS函数。JS函数返回后,再将返回值转换成C语言类型并放到寄存器中,最后用ret指令返回。
-
ArrayBuffer内存机制:ArrayBuffer是js的内置类型,它封装了一段C++堆内存空间,它的内存空间可以用来存储C语言的数据结构。ArrayBuffer的内存空间无法被扩容或缩容,也不会随着GC堆整理而移动,但可以被释放。ArrayBuffer可以转为指针,其指针可以被传递到C函数中。ArrayBuffer中存储的数据可以在js中高效读写。
使用时需要注意生命周期问题和内存越界问题。ArrayBuffer的生命周期受到GC机制管理,需要避免过早被回收导致野指针问题。如果用ArrayBuffer代替了malloc,那么建议在js中保存ArrayBuffer的引用,防止其过早的被GC回收。注意内存分配与释放的方法统一,避免让C/C++用free或delete释放了ArrayBuffer分配的内存。
- C语言指针:C语言指针可以用来表示函数地址,数组地址,结构体地址等。函数和闭包以及结构体等机制的底层依赖于对指针的封装。
二进制接口
abi是应用程序二进制接口,规定了不同编程语言间函数调用的约定和规范。CPU类型以及C语言的调用约定决定了函数的abi类型。 abi 决定了函数参数如何传递,返回值如何传递,寄存器的使用等。 如果函数定义中没有规定abi类型,一般使用默认abi。
相关api
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按照各操作系统的路径查找规则查早动态库,获取绝对路径。
文件名会被自动加上当前系统的动态库文件后缀。
- windows系统:
.dll - linux系统和鸿蒙系统:
.so - macos系统:
.dylib
如果动态库不是绝对路径,wps会按照以下顺序查找动态库文件:
-
windows系统:
- 系统目录(System32)
- Windows目录
PATH环境变量中包含的目录
-
linux系统:
LD_LIBRARY_PATH环境变量中包含的目录/lib/usr/lib
-
macos系统
LD_LIBRARY_PATH环境变量中包含的目录DYLD_LIBRARY_PATH环境变量中包含的目录- 工作目录
DYLD_FALLBACK_LIBRARY_PATH环境变量中包含的目录,默认位/usr/local/lib和/usr/lib
出于安全策略限制,禁止加载wps的office6目录下的WPS核心动态库。LoadLibrary的安全策略未来有可能会调整
相关api
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++: 手写C封装层
- rust: 可以使用代码生成工具(比如
rust-bindgen库)制作出C abi层。
异常处理
C函数调用失败
- 有可能是因为被调用函数内部抛出野指针、空指针或者数组越界、ffi函数参数类型与实际参数不匹配等问题等情况导致的异常
- 有可能是因为被调用的C++函数内部主动抛异常
- 也有可能是JS传递的实参基础数据类型与形参不匹配,且无法转换
完整api
如果使用vscode等支持lsp的ide来编写js宏,可以将完整api复制到一个typescript文件中,然后在vscode等ide中查看。让ai读取这些api,借助ai辅助编程。
推荐使用ffi命名空间的api,而不是FFI命名空间的api。前者进行了细致的封装,功能更强大,且更易用。
// 在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
- windows 64位:
choco install sqlite - windows 32位:从sqlite官网下载动态库压缩包:https://www.sqlite.org/download.html
- linux ubuntu:
sudo apt install libsqlite3-dev - macos:
brew install sqlite
// 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()
}
}