sqlite3.wasm
命名空间1(在本页的其余部分中缩写为 wasm
)包含许多用于处理 WASM 端构造的例程。它们包括用于以下任务的 API……
- 内存管理。
- 分配和释放内存。
- 用于处理 WASM 堆内存的帮助程序,例如从 WASM 堆获取和设置原始值。
- 可配置的结果值和参数类型转换,用于 WASM 导出的函数。
- JS/C 字符串转换。
- 将 JS 函数绑定到 WASM 运行时,以便可以从 WASM 代码(即从 C)调用它们。
简而言之,如果在 sqlite3 JS API 的开发过程中需要 WASM 特定的功能,则会将其添加到此命名空间中。在大多数情况下,高级客户端代码很少需要使用其中的一部分,而使用C 样式 API的客户端可能会大量使用它们。
sqlite3.wasm.exports
命名空间
sqlite3.wasm.exports
命名空间对象是 WASM 模块文件的 WASM 标准部分,包含内置于 WASM 模块的所有“导出”C 函数,以及作为 WASM 模块一部分的某些非函数值。就 JS/C 绑定而言,此对象中存在的函数处于最低级别。它们不会对其参数或结果值执行任何自动类型转换,并且许多(也许大多数)由于此原因而难以从 JS 使用。此级别的 API 通常不建议客户端使用,但可供希望使用它的用户使用。此对象中旨在供客户端使用的函数会重新导出到 sqlite3.capi
命名空间中,并对其应用自动类型转换(如果适用)。一小部分函数会被重新导出到 sqlite3.wasm
命名空间中。
exports
中唯一属于本项目 API 的符号是
- 名为
sqlite3_...()
的函数,以下情况除外- 所有在
sqlite3
后有两个下划线的函数,例如sqlite3__wasm_...()
是内部使用 API,随时可能更改或删除。 - 名为
sqlite3_wasm_...()
的函数不是客户端 API 的一部分,除非它们被重新导出到sqlite3.wasm
命名空间中。其余部分供 JS 绑定内部使用,并且没有稳定的 API。类似地…… - 名为
sqlite3_wasm_test_...()
的函数仅供本项目的测试使用,并且可能会从任何给定构建中省略。
- 所有在
- 内存分配函数:语义上为
free()
和malloc()
,但规范构建使用sqlite3_free()
和sqlite3_malloc()
。这些函数作为下面所述公开给客户端,但 C 级别形式sqlite3_malloc()
在客户端需要不因内存不足情况而抛出异常的变体时很有用。 - WASM 内存对象(根据构建选项,可能是可选的):
memory
__indirect_function_table
是导出函数表的事实上的标准名称。此 API 通过sqlite3.wasm.functionTable()
公开它。
构建过程将在 exports
命名空间中包含其他函数和对象,这些函数和对象不是本项目公共接口的一部分,不应由客户端代码使用。它们在 WASM 文件的任何给定构建中可能有所不同,并且在构建环境之间肯定会有所不同。
内存管理
就像在 C 中一样,WASM 提供了一个内存“堆”,并且在 JS 和 WASM 之间传输值通常需要操作该内存,包括对其进行低级分配和释放。以下小节描述了各种内存管理 API。
低级管理
最低级别的内存管理类似于 C 的标准 malloc()
、realloc()
和 free()
,唯一的区别是使用异常来报告内存不足情况。为了避免由混合不同的分配器引起的某些 API 误用,规范 sqlite3.js
构建包装 sqlite3_malloc()
、sqlite3_realloc()
和 sqlite3_free()
而不是 malloc()
、realloc()
和 free()
,但这两对的语义实际上是相同的。
按字母顺序排列……
alloc()
pointer alloc(n)
pointer alloc.impl(n)
(不抛出异常)
从 WASM 堆分配 n
字节的内存,并返回块中第一个字节的地址。如果分配失败,alloc()
会抛出 WasmAllocError
。如果需要不抛出异常的分配,请使用 alloc.impl(n)
,如果分配失败,它会返回 WASM NULL 指针(整数 0)。
请注意,以这种方式分配的内存不会自动清零。在实践中,这并没有被证明是一个问题(至少在 JS 中),因为只有在内存有特定用途时才会显式分配内存,并且将由分配它的代码填充。
allocCString()
pointer allocCString(jsString, returnWithLength=false)
使用 alloc()
为给定 JS 字符串的字节长度加上 1(用于 NUL 终止符)分配足够的内存,使用 jstrcpy()
将给定的 JS 字符串复制到该内存中,用 NUL 终止它,并返回指向该 C 字符串的指针。指针的所有权被转移到调用方,调用方最终必须将指针传递给 dealloc()
以释放它。
如果传递了一个真值作为第二个参数,则其返回值语义会发生变化:它返回 [ptr,n]
,其中 ptr
是 C 字符串的指针,n
是其 cstrlen()
。
allocMainArgv()
pointer allocMainArgv(list)
使用 alloc()
创建一个 C 样式数组,适合传递给 C 级别 main()
例程。输入是一个具有 length
属性和 forEach()
方法的集合。分配一个内存块,其长度为 list.length
个条目,并且该内存的每个指针大小块都填充了每个元素的 (''+value)
的 allocCString()
转换结果。返回指向列表开头的指针,适合作为 C 样式 main()
函数的第二个参数传递。
如果 list.length
为假值,则会抛出异常。
请注意,返回值难以释放,但它旨在用于调用 C 级别 main()
函数,其中字符串必须与应用程序一样长。有关易于释放的变体,请参阅 scopedAllocMainArgv()
。
allocPtr()
pointer allocPtr(howMany=1, safePtrSize=true)
将一个或多个指针分配为单个内存块,并将其清零。
第一个参数是要分配的指针数。第二个参数指定它们是否应使用“安全”指针大小(8 字节),或者是否可以使用默认指针大小(通常为 4,但也可能为 8)。
结果的返回方式取决于其第一个参数:如果传递 1,则返回分配的内存地址。如果传递多个,则返回一个指针地址数组,可以选择将其与“解构赋值”一起使用,如下所示
const [p1, p2, p3] = allocPtr(3);
注意:释放内存时,仅将第一个结果值传递给 dealloc()
。其他值是同一内存块的一部分,不得单独释放。
第二个参数的原因是……
当返回的指针之一将引用 64 位值(例如 double 或 int64),并且必须写入或获取该值(例如,使用 poke()
或 peek()
)时,重要的是所讨论的指针必须与 8 字节边界对齐,否则它将无法正确获取或写入,并且会损坏或读取相邻内存。只有在客户端代码确定它只会获取/获取 4 字节值(或更小)时,才能安全地传递 false。
dealloc()
void dealloc(pointer)
释放 alloc()
返回的内存。如果传递给它的值不是 alloc()
返回的值或 null
/undefined
/0
(所有这些都是无操作),则结果未定义。
alloc()
不命名为 malloc()
的原因相同。realloc()
pointer realloc(ptr,size)
pointer realloc.impl(ptr,size)
(不抛出异常)
在语义上等效于 realloc(3)
或 sqlite3_realloc()
,此例程重新分配通过此例程或 alloc()
分配的内存。其第一个参数为 0 或此例程或 alloc()
返回的指针。其第二个参数是要(重新)分配的字节数,或 0 以释放第一个参数中指定的内存。在分配错误时,realloc()
会抛出 WasmAllocError
,而 realloc.impl()
会在分配错误时返回 0。
请注意,重新分配 realloc.impl()
的返回值是不好的做法,会导致堆内存泄漏
let m = wasm.realloc(0, 10); // allocate 10 bytes
m = wasm.realloc.impl(m, 20); // grow m to 20 bytes
如果重新分配失败,它将返回 0,覆盖 m
并有效地泄漏第一次分配。
sizeofIR()
int sizeofIR(string)
对于给定的集合中的类似 IR 的字符串('i8'
、'i16'
、'i32'
、'f32'
、'float'
、'i64'
、'f64'
、'double'
、'*'
)或以 '*'
结尾的任何字符串值,返回该值的 sizeof(在后一种情况下为 wasm.ptrSizeof
)。对于任何其他值,它返回 undefined
值。
一些分配例程使用它来使调用方能够传递 IR 值而不是整数。
“作用域”分配管理
通常方便地以这样一种方式管理分配,即在特定块中进行的所有分配在该块退出时“自动”清理。此 API 提供以这种方式工作的“作用域”分配例程。
以下列出它们在典型使用顺序中的……
scopedAllocPush()
opaque scopedAllocPush()
打开一个新的分配“作用域”。通过 scopedAllocXyz()
API 进行的所有分配都将其结果存储到当前(最近推送的)分配作用域中,以便稍后清理。必须保留返回值以传递给 scopedAllocPop()
。
可以同时激活任意数量的作用域,但必须以与其创建相反的顺序弹出它们。即,它们必须以与 C 样式作用域等效的方式嵌套。
警告
- 所有其他
scopedAllocXyz()
例程在没有活动作用域时都会抛出异常。 - 将作用域分配的结果传递给
dealloc()
绝不合法,这样做会导致在使用scopedAllocPop()
关闭作用域时发生双重释放。
此函数及其相关函数只有一个预期的用法模式
const scope = wasm.scopedAllocPush();
try {
... use scopedAllocXyz() routines ...
// It is perfectly legal to use non-scoped allocations here,
// they just won't be cleaned up when...
}finally{
wasm.scopedAllocPop(scope);
}
scopedAlloc()
pointer scopedAlloc(n)
工作方式与 alloc(n)
完全相同,但将分配的结果存储在当前作用域中。
此函数的只读 level
属性解析为当前分配作用域深度。
scopedAllocMainArgv()
pointer scopedAllocMainArgv(array)
此函数的功能与allocMainArgv()
完全相同,但它属于当前分配作用域,其内容将在弹出当前分配作用域时释放。
scopedAllocCall()
any scopedAllocCall(callback)
调用 scopedAllocPush()
,调用给定的回调,然后调用 scopedAllocPop()
,传播回调中的任何异常或返回其结果。这本质上是以下内容的便捷形式:
const scope = wasm.scopedAllocPush();
try { return callback() }
finally{ wasm.scopedAllocPop(scope) }
scopedAllocCString()
指针 scopedAllocCString(jsString, returnWithLength=false)
工作方式与 allocCString()
完全相同,但将分配结果存储在当前作用域中。
scopedAllocMainArgv()
指针 scopedAllocMainArgv(列表)
工作方式与 allocMainArgv()
完全相同,但将各种分配存储在当前作用域中。
scopedAllocPtr()
指针 scopedAllocPtr(howMany=1, safePtrSize=true)
工作方式与 allocPtr()
完全相同,但将分配结果存储在当前作用域中。
scopedAllocPop()
void scopedAllocPush(不透明)
给定从 scopedAllocPush()
返回的值,此函数“弹出”该分配作用域并释放 scopedAllocXyz()
函数族在该作用域中分配的所有内存。
从技术上讲,可以在没有参数的情况下调用此函数,但传递参数可以让分配器执行完整性检查,以确保作用域按正确的顺序推入和弹出(如果不正确,则会抛出错误)。不传递参数不是非法的,但会使该完整性检查无法进行。
琐事:在美国的一些地区,此函数可能更广为人知的名字是
scopedAllocSoda()
或scopedAllocCola()
。
“PStack”分配
"pstack"(伪栈)API 是一个特殊用途的分配器,仅用于分配少量内存,例如 输出指针 所需的内存。它比 作用域分配 API 更高效,并且涵盖了该 API 的许多用例,但它具有很小的静态内存限制(总大小不小于 4kb,且未指定)。
pstack API 通常使用方法如下:
const pstack = sqlite3.wasm.pstack;
const stackPtr = pstack.pointer;
try {
const ptr = pstack.alloc(8);
// ==> pstack.pointer === ptr
const otherPtr = pstack.alloc(8);
// ==> pstack.pointer === otherPtr
...
}finally{
pstack.restore(stackPtr);
// ==> pstack.pointer === stackPtr
}
pstack 方法和属性按字母顺序列出如下。
alloc()
pointer alloc(n)
尝试从 pstack 分配给定数量的字节。成功后,它会将给定大小的内存块清零,调整 pstack 指针,并返回指向该内存的指针。如果出错,则会抛出 WasmAllocError
。最终必须使用 pstack.restore()
释放该内存。
n
可以是 wasm.sizeofIR()
接受的字符串,任何不被该函数接受的字符串值都会触发 WasmAllocError
异常。
此方法始终将给定值调整为 8 字节的倍数,因为不这样做会导致从/向 WASM 堆读取和写入 64 位值时出现错误的结果。类似地,返回的地址始终是 8 字节对齐的。
allocChunks()
数组 allocChunks(n, sz)
将 alloc()
的 n
个块(每个块 sz
字节)作为单个内存块,并返回地址作为包含 n
个元素的数组,每个元素都保存一个块的地址。
sz
参数可以是 wasm.sizeofIR()
接受的字符串值,任何不被该函数接受的字符串值都会触发 WasmAllocError
异常。
如果分配失败,则抛出 WasmAllocError
。
示例
const [p1, p2, p3] = pstack.allocChunks(3,4);
allocPtr()
混合 allocPtr(n=1,safePtrSize=true)
allocChunks()
的一个便捷包装器,它将每个块的大小设置为 8 字节(safePtrSize
为真值)或 wasm.ptrSizeof
(如果 safePtrSize
为假值)。
它返回结果的方式取决于它的第一个参数:如果为 1,则返回单个指针值。如果大于 1,则返回与 allocChunks()
相同的结果。
当任何返回的指针将引用 64 位值(例如 double 或 int64),并且必须写入或获取该值(例如,使用 wasm.poke()
或 wasm.peek()
)时,重要的是所讨论的指针必须与 8 字节边界对齐,否则它将无法正确获取或写入,并且会损坏或读取相邻的内存。
但是,当所有涉及的指针都指向“小”数据时,传递假值以节省少量内存是安全的。
指针
此属性解析为当前 pstack 位置指针。此值仅用于保存以传递给 restore()
。在通过 pstack.alloc()
(或等效项)预留内存之前,写入此内存会导致未定义的结果。
配额
此属性解析为 pstack 中可用的总字节数,包括当前分配的任何空间。此值是编译时常量。
剩余
此属性解析为 pstack 中剩余的空间量。
restore()
void restore(pstackPtr)
将当前 pstack 位置设置为给定指针。如果传入的值不是来自 pstack.pointer
,或者如果在此调用之后使用了给定指针之前空间中分配的内存,则结果未定义。
获取/设置内存值
WASM 内存堆作为内存的字节数组公开给 JS,使其看起来是连续的(尽管实际上它是分块分配的)。给定堆的字节方向视图,可以像在 C 中一样读取和写入堆的单个字节。
const X = wasm.heap8u(); // a uint8-oriented view of the heap
X[someAddress] = 0x2a;
console.log( X[someAddress] ); // ==> 42
显然,写入任意地址会像在 C 中一样损坏 WASM 堆,因此必须小心处理使用的内存地址(就像在 C 中一样!)。
提示:重要的是永远不要长期保存从诸如
heap8u()
之类的方法返回的对象,因为如果堆增长,它们可能会失效。可以短暂地保存该引用以进行一系列调用,只要这些调用保证不会在 WASM 堆上分配内存,但这永远不应该缓存以供以后使用。
在描述操作堆的例程之前,我们首先需要查看数据类型描述符,有时也称为“IR”(内部表示)。这些是识别 WASM 和/或 JS/WASM 粘合代码支持的特定数据类型的短字符串。
i8
:8 位有符号整数i16
:16 位有符号整数i32
:32 位有符号整数。别名:int
、*
、**
(注意*
和**
可以在 WASM 环境获得 64 位指针功能时动态重新映射到i64
)。i64
:64 位有符号整数。使用此 API 需要应用程序已 使用 BigInt 支持构建,否则会抛出错误。f32
:32 位浮点数。别名:float
f64
:64 位浮点数。别名:double
这些被内存访问器 API 大量使用,需要提交到内存中。
TODO:解释堆中值的对齐方式如何影响对其的访问方式。在实践中,这通常不是问题,除非/直到人们将内存分块并将其划分为子块本身。简而言之:当读取或写入给定大小的值时,它通常必须在恰好是该大小的偶数倍数的堆地址处执行。
以下例程可用于以各种方式访问内存地址……
peek()
及其变体
数字 peek(地址 [,representation='i8'])
数组 peek(地址数组 [,representation='i8'])
第一种形式从内存中获取单个值。第二种形式从给定数组中的每个指针获取值,并返回值数组。用于读取内存的堆视图由第二个参数指定,默认为字节方向视图。
如果第二个参数以 "*"
结尾,则始终使用指针大小的表示形式(当前始终为 32 位)。
示例
let i32 = wasm.peek(myPtr, 'i32');
提供了 peek()
的几种便捷形式,它们只是简单地转发到 peek()
并使用特定的第二个参数。
peekPtr()
(已弃用的别名:getPtrValue()
):等效于peek(X,'*')
。最常用于 获取输出指针值。peek8()
:等效于peek(X,'i8')
peek16()
:等效于peek(X,'i16')
peek32()
:等效于peek(X,'i32')
peek64()
:等效于peek(X,'i64')
。如果环境未 配置为支持 BigInt,则会抛出错误。peek32f()
:等效于peek(X,'f32')
peek64f()
:等效于peek(X,'f64')
heapForSize()
及其朋友
TypedArray heapForSize(n [,unsigned=true])
要求 n 为以下之一:
- 整数 8、16 或 32。
- 整数类型 TypedArray 构造函数:Int8Array、Int16Array、Int32Array 或它们的 Uint 对应项。
如果启用了 BigInt 支持,它还接受值 64 或 BigInt64Array/BigUint64Array,否则如果传递 64 或其中一个构造函数,则会抛出错误。
返回与给定块大小关联的 WASM 堆内存缓冲区的基于整数的 TypedArray 视图。如果将整数作为第一个参数传递且 unsigned 为真值,则返回该视图的“U”(无符号)变体,否则返回有符号变体。如果传递了 TypedArray 值,则忽略第二个参数。请注意,此函数不支持 Float32Array 和 Float64Array 视图。
请注意,堆的增长可能会使对该堆的任何引用无效,因此不要长时间保存引用,并且不要在任何可能分配的运算后使用引用。而是通过再次调用此函数重新获取引用,如果需要,此函数会自动刷新视图。
如果传递了无效的 n
,则会抛出错误。
客户端代码中很少使用此函数。在实践中,使用以下(更快的)便捷形式之一。
heap8()
→ Int8Arrayheap8u()
→ Uint8Arrayheap16()
→ Int16Arrayheap16u()
→ Uint16Arrayheap32()
→ Int32Arrayheap32u()
→ UInt32Array
poke()
对象 poke(地址, 数字 [,representation='i8'])
对象 poke(地址数组, 数字 [,representation='i8']
获取给定表示形式的 heapForSize()
,然后将给定的数值写入其中。只有数字可以通过这种方式写入,传递非数字可能会触发异常。如果传递了指针数组,则会将给定值写入所有指针。
返回 this
。
存在 poke()
的几种便捷形式,它们只是简单地转发到该方法并使用特定的第三个参数。
pokePtr()
(已弃用的别名:setPtrValue()
):等效于poke(X,Y,'*')
。最常用于清除 输出指针值。poke8()
:等效于poke(X,Y,'i8')
poke16()
:等效于poke(X,Y,'i16')
poke32()
:等效于poke(X,Y,'i32')
poke64()
:等效于poke(X,Y,'i64')
。如果此环境未 配置为支持 BigInt,则会抛出错误。poke32f()
:等效于poke(X,Y,'f32')
poke64f()
:等效于poke(X,Y,'f64')
字符串转换和实用程序
经常需要在 WASM 中传递字符串,但 JS 和 C 代码表示字符串的方式差异很大。以下例程可用于字符串转换和相关算法。
按字母顺序列出如下……
cArgvToJs()
数组 cArgvToJs(int argc, 指向指针的指针 pArgv)
期望接收 C 风格字符串数组及其长度。它返回一个包含字符串和/或 null
值的 JS 数组:pArgv
数组中任何为 NULL 的条目都会在结果数组中产生一个 null
条目。如果 argc
为 0,则返回空数组。
如果 pArgv
的前 argc
个条目中任何一个既不为 0(NULL)也不是合法的 UTF 格式 C 字符串,则结果未定义。
需要明确的是,此函数期望传递的 C 风格参数为 (int, char **)
(可选地进行 const 限定)。
cstrToJs()
字符串 cstrToJs(ptr)
期望其参数为 WASM 堆内存中的一个指针,该指针引用一个以 UTF-8 编码的以 NUL 结尾的 C 风格字符串。此函数使用 cstrlen()
计算其字节长度,然后返回一个表示其内容的 JS 格式字符串。作为特殊情况,如果参数为假值,则返回 null
。
cstrlen()
int cstrlen(ptr)
预期其参数是指向WASM堆内存的指针,该指针引用一个以UTF-8编码的、以NUL结尾的C风格字符串。返回字符串的长度(以字节为单位),与strlen(3)
相同。作为特殊情况,如果参数为假值,则返回null
。如果参数超出wasm.heap8u()
的范围,则抛出异常。
cstrncpy()
int cstrncpy(tgtPtr, srcPtr, n)
工作方式类似于C语言的strncpy(3)
,最多将n
个字节(而不是字符)从srcPtr
复制到tgtPtr
。它一直复制直到复制了n
个字节或在src中遇到0字节。与strncpy()
不同,它返回在tgtPtr
中赋值的字节数,包括NUL字节(如果有)。如果在srcPtr
中的NUL字节之前达到n
,则tgtPtr
将不会以NUL结尾。如果在复制n
个字节之前遇到NUL字节,则tgtPtr
将以NUL结尾。
如果n
为负数,则使用cstrlen(srcPtr)+1
来计算它,其中+1表示NUL字节。
如果tgtPtr
或srcPtr
为假值,则抛出异常。如果以下情况发生,则结果未定义
- 两者都不是指向WASM堆的指针,或者
srcPtr
没有以NUL结尾,并且n
小于srcPtr
的逻辑长度。
注意:当传入非负的n
值时,可以通过这种方式复制部分多字节字符,并且将此类字符串转换回JS字符串将导致结果未定义。
jstrcpy()
int jstrcpy(jsString, TypedArray tgt, offset = 0, maxBytes = -1, addNul = true)
预警:此API有些复杂,实际上客户端代码永远不需要使用它。
将给定的JS字符串以UTF-8编码到给定的TypedArray tgt
(必须是Int8Array或Uint8Array)中,从给定的偏移量开始,最多写入maxBytes字节(如果addNul
为真,则包括NUL终止符,否则不添加NUL)。如果它写入任何字节并且addNul
为真,它始终以NUL终止输出,即使这样做意味着NUL字节是它写入的全部内容。
如果maxBytes
为负数(默认值),则将其视为tgt
的剩余长度,从给定的偏移量开始。
如果写入最后一个字符会因为字符是多字节字符而超过maxBytes
计数,则不会写入该字符(而不是写入截断的多字节字符)。这可能导致它写入的字节数比maxBytes
指定的少3个。
返回写入目标的字节数,包括NUL终止符(如果有)。如果它返回0,则它什么也没写入,这可能发生在以下情况:
jsString
为空并且addNul
为假。offset
< 0.maxBytes
=== 0.maxBytes
小于多字节jsString[0]
的字节长度。
如果tgt
不是Int8Array或Uint8Array,则抛出异常。
在C语言的
strcpy()
中,目标指针是第一个参数。这里不是这种情况,主要是因为第3个及以后的参数都引用了目标,因此将它们分组在一起似乎更有意义。Emscripten的此函数的对应函数
stringToUTF8Array()
返回写入的字节数,不包括NUL终止符。但是,这是模棱两可的:str.length===0或maxBytes===(0或1)都会导致返回0。
jstrlen()
int jstrlen(jsString)
给定一个JS字符串,此函数返回其UTF-8长度(以字节为单位)。如果其参数不是字符串,则返回null
。这是一个相对昂贵的计算,在不需要时应避免。
jstrToUintArray()
Uint8Array jstrToUintArray(jsString, addNul=false)
对于给定的JS字符串,返回一个包含其内容的Uint8Array
,以UTF-8编码。如果addNul
为真,则返回的数组将有一个尾随的0条目,否则不会。
琐事:这是在JS的
TextEncoder
被此代码的作者知晓之前编写的。相同的功能,不包括尾随NUL选项,可以通过new TextEncoder().encode(str)
实现。
其他分配例程
allocFromByteArray()
pointer allocFromByteArray(srcTypedArray)
wasm.alloc()
分配srcTypedArray.byteLength
个字节,用源TypedArray中的值填充它们,并返回该内存的指针。返回的指针最终必须传递给wasm.dealloc()
以清理它。
参数可以是Uint8Array、Int8Array或ArrayBuffer,如果传递任何其他类型,则会抛出异常。
作为特殊情况,为了避免在此例程使用的地方出现更多特殊情况,如果srcTypedArray.byteLength
为0,则它会分配一个字节并将其设置为值0。即使在这种情况下,调用也必须表现得好像分配的内存恰好有srcTypedArray.byteLength
个可用字节。
桥接 JS/WASM 函数
本节介绍了与弥合JavaScript和WebAssembly函数之间差距相关的辅助API。
WASM模块将所有导出的函数公开给用户,但它们处于“原始”形式。也就是说,它们不执行任何参数或结果类型转换,并且只支持WASM支持的数据类型(即仅数字类型)。对于仅接受和返回数字的函数来说,这很好,但对于接受或返回字符串或具有输出指针的函数来说,通常不太有用。出于可用性原因,希望通过自动执行诸如为在JS和WASM之间转换字符串而分配和释放内存等普通任务来减少JS/C之间的摩擦。
此外,通常需要从JS向WASM运行时添加新函数,这需要动态编译二进制WASM代码。一个常见的例子是创建用户定义的SQL函数。在大多数情况下,sqlite3 API的JS绑定会为用户处理此类转换,但有些情况下客户端代码需要或希望自行执行此类转换。
WASM 函数表
WASM导出的函数以及已绑定到运行时WASM的JavaScript函数通过WebAssembly.Table实例公开给客户端。以下API可用于处理此问题。
functionEntry()
mixed functionEntry(ptr)
给定一个函数指针,如果找到则返回WASM函数表条目,否则返回假值。
functionTable()
WebAssembly.Table functionTable()
返回WASM模块的间接函数表。
调用和包装函数
xCall()
any xCall(functionName, ...args)
any xCall(functionName, [args...])
通过名称调用WASM导出的函数,传递所有提供的参数(可以选择作为数组提供)。如果函数未导出或参数计数不匹配,则抛出异常。此例程不执行任何类型转换,并且本质上等同于
const rc = wasm.exports.some_func(...args)
除了xCall()
如果参数计数与WASM导出的函数的参数计数不匹配,则会抛出异常。
xCallWrapped()
any xCallWrapped(functionName, resultType, argTypes, ...args)
any xCallWrapped(functionName, resultType, argTypes, [args array...])
函数类似于xCall()
,但执行参数和结果类型转换,如xWrap()
所示。
第一个参数是要调用的导出函数的名称。第二个是其结果类型的名称,如xWrap()
中所述。第三个是参数类型名称的数组,如xWrap()
中所述。第四个及以后的参数是调用的参数,特殊情况是,如果第四个参数是数组,则将其用作调用的参数。
返回调用的转换结果。
这只是xWrap()
的一个薄包装器。如果要多次调用给定的函数,则使用xWrap()
创建包装器然后根据需要多次调用该包装器效率更高。但是,对于一次性调用,此变体可能效率更高,因为它假设会快速释放包装器函数。
xGet()
Function xGet(functionName)
通过名称返回WASM导出的函数,如果找不到函数则抛出异常。
xWrap()
Function xWrap(functionName, resultType=undefined, ...argTypes)
Function xWrap(functionName, resultType=undefined, [argTypes...])
xWrap()
创建一个JS函数,该函数调用WASM导出的函数,如xCall()
中所述。
为WASM导出函数fname创建包装器。它使用xGet()
获取导出函数(在出错时会抛出异常),并返回该函数本身或该函数的包装器,该包装器将JS端参数类型转换为WASM端类型并转换结果类型。如果函数不带参数并且resultType为null
,则按原样返回函数,否则为其创建包装器以调整其参数和结果值,如下所述。
此函数的参数为
functionName
:导出函数的名称。xGet()
用于获取此名称,因此如果找不到具有该名称的导出函数,则会抛出异常。resultType
:结果类型的名称。文字null
表示按原样返回原始函数的值(助记符:没有进行“空”转换)。文字undefined
或字符串"void"
表示忽略函数的结果并返回undefined
。除了这两个特殊情况外,它可以是下面描述的值之一,也可以是客户端使用xWrap.resultAdapter()
安装的任何映射。
如果传递3个参数且最后一个参数是数组,则该数组必须包含一个类型名称列表(见下文),用于将参数从JS适配到WASM。如果传递2个参数、超过3个参数或第3个参数不是数组,则所有第2个参数之后的参数(如果有)都被视为类型名称。换句话说,以下用法是等价的
xWrap('funcname', 'i32', 'string', 'f64');
xWrap('funcname', 'i32', ['string', 'f64']);
与以下用法等价
xWrap('funcname', 'i32'); // no arguments
xWrap('funcname', 'i32', []);
类型名称是符号名称,它将函数的结果和参数映射到适配器函数,以根据需要在将值传递给WASM之前转换值,或者转换WASM的返回值。内置名称列表。以下列表描述了每个名称,请注意,有些名称仅适用于参数或返回值,这两者通常具有不同的语义
i8
、i16
、i32
(参数和结果):所有整数转换,将参数转换为整数并将其截断为给定的位长度。N*
(参数):形式为N*
的类型名称,其中N是数字类型名称,与WASM指针的处理方式相同。*
和pointer
(参数):假定为不透明的WASM指针,并像当前WASM指针数字类型一样处理。非数字将强制转换为值0,超出范围的数字将导致结果未定义(与任何指针误用一样)。*
和pointer
(结果):是当前WASM指针数字类型的别名。**
(参数):只是'*'
的描述性别名。它主要用于标记输出指针参数。i64
(参数和结果):将值传递给BigInt()
以将其转换为int64。仅在启用BigInt支持时可用。f32
(float
)、f64
(double
)(参数和结果):将参数传递给Number()
。即适配器当前不区分这两种类型的浮点数。number
(结果):使用Number(theValue).valueOf()
将结果转换为JS Number。请注意,这仅用于结果转换,因为无法通用地知道将参数转换为哪种类型的数字。
非数字转换包括
string
或utf8
(参数):为了适应某些C API的各种用途,具有两种不同的语义……- 如果arg是JS字符串,则会创建一个临时的C字符串(UTF-8编码)以传递给导出函数,该字符串在包装器返回之前会被清理。如果需要长期存在的C字符串指针,则客户端代码需要创建字符串,然后将其指针传递给函数。
- 否则,假定arg是指向客户端已分配的字符串的指针,并将其作为WASM指针传递。
string
或utf8
(结果):将结果值视为一个常量C字符串(以UTF-8编码),将其复制到JS字符串,并返回该JS字符串。string:dealloc
或utf8:dealloc
(结果):将结果值视为一个非 const 的 C 字符串,编码为 UTF-8,其所有权刚刚转移到调用方。它将 C 字符串复制到一个 JS 字符串,使用dealloc()
释放 C 字符串,并返回 JS 字符串。如果这样的结果值为 NULL,则 JS 结果为null
。
注意:当使用返回来自特定分配器的结果的 API 时,此转换不合法。相反,需要使用适当的释放器的等效转换。下一节提供了一个这样的示例。string:flexible
(参数):是 C 样式 API 文档 中描述的string
的扩展版本。这些广泛用于库中的 SQL 字符串输入。string:static
(参数):如果传递了一个指针,则按原样返回。其他任何内容:强制转换为 JS 字符串以用作映射键。如果找到匹配的条目(如下所述),则返回它,否则使用wasm.allocCString()
创建一个新字符串,将其指针映射到(''+v
)以供应用程序生命周期的其余部分使用,并返回此调用以及传递字符串等效参数的所有未来调用的指针值。此转换适用于需要静态/长期存在的字符串参数的情况,例如sqlite3_bind_pointer()
和sqlite3_result_pointer()
。json
(结果):将结果视为 const C 字符串,并返回将转换为 JS 字符串的结果传递给JSON.parse()
的结果。如果 C 字符串是 NULL 指针,则返回null
。传播JSON.parse()
中的任何异常。json:dealloc
(结果):工作方式与string:dealloc
完全相同,但返回与json
适配器相同的内容。请注意string:dealloc
中关于分配器和释放器的警告。
当调用 xWrap()
时,会验证结果和参数的类型名称,任何未知的名称都会触发异常。
客户端可以使用 xWrap.resultAdapter()
和 xWrap.argAdaptor()
映射他们自己的结果和参数适配器,注意并非所有类型转换都对参数和结果类型都有效,因为它们通常具有不同的内存所有权要求。下一节将介绍该主题……
参数和结果值类型转换
另请参阅:api-c-style.md#type-conversion
当调用 xWrap()
并评估函数调用签名时,它会查找参数和结果类型适配器以进行匹配。可以使用下面列出的方法为参数和结果值安装自定义适配器。
xWrap()
具有两个签名相同的函数
xWrap.argAdapter(string, function)
xWrap.resultAdapter(string, function)
每个函数都期望一个类型名称字符串,例如为 xWrap()
描述的字符串,以及一个传递单个值的函数,该函数必须返回该值、该值的转换或抛出异常。这些函数中的每一个都返回自身,以便可以链接调用。
例如,假设我们有一个 C 绑定函数,它返回使用非默认分配器 my_str_alloc()
分配的 C 样式字符串。返回的内存由调用方拥有,必须释放,但需要使用分配器的释放对应部分 my_str_free()
来释放。我们可以使用以下方法创建这样的结果值适配器:
wasm.xWrap.resultAdaptor('my_str_alloc*', (v)=>{
try { return v ? target.cstrToJs(v) : null }
finally{ wasm.exports.my_str_free(v) }
};
有了这个,我们可以进行如下调用:
const f = wasm.xWrap('my_function', 'my_str_alloc*', ['i32', 'string']);
const str = f(17, "hello, world");
// ^^^ the memory allocated for the result using my_str_alloc()
// is freed using my_str_free() before f() returns.
类似地,假设我们有一个自定义的 JS 类,它有一个名为 pointer
的成员属性,该属性引用表示此 JS 类的结构体的 C 端内存2。然后,我们可以使其合法地将此类对象传递给 C API,方法如下:
const argPointer = wasm.xWrap.argAdapter('*'); // default pointer-type adapter
wasm.xWrap.argAdaptor('MyType',(v)=>{
if(v instanceof MyType) v = v.pointer;
if(wasm.isPtr(v)) return argPointer(v);
throw new Error("Invalid value for MyType argument.");
});
有了这个,我们可以包装我们的一个函数,如下所示:
const f = wasm.xWrap('MyType_method', undefined, ['MyType', 'i32']);
const my = new MyType(...);
// ^^^ assume this allocates WASM memory referenced via my.pointer.
f( my /* will use my.pointer */, 17 );
可以对结果值进行类似的转换,尽管如何对结果值进行转换完全取决于客户端内存管理的语义。
(取消)安装 WASM 函数
当使用采用回调函数指针的 C API 时,不能简单地将 JS 函数传递给它们。相反,JS 函数必须代理到 WASM 环境中,并且必须将该代理传递给 C。这是通过即时编译少量描述函数签名(以 WASM 术语)的二进制 WASM 代码来完成的,将它的参数转发给提供的 JS 函数,并返回该 JS 函数的结果。细节很丑陋,但用法很简单……
installFunction()
pointer installFunction(funcSignature, function)
pointer installFunction(function, funcSignature)
期望一个 JS 函数和签名,与 wasm.jsFuncToWasm()
完全相同。它使用该函数创建一个 WASM 导出函数,将该函数安装到 wasm.functionTable()
的下一个可用插槽中,并返回该函数在该表中的索引(充当指向该函数的指针)。返回的指针可以传递给 wasm.uninstallFunction()
以卸载它并释放表插槽以供重用。
作为特殊情况,如果传入的函数是 WASM 导出函数,则会忽略签名参数,并且会按原样安装 func,无需重新编译/重新包装。
如果 WebAssembly.Table.grow()
或 wasm.jsFuncToWasm()
抛出异常,则此函数将传播异常。在不使用 Emscripten 的 -sALLOW_TABLE_GROWTH
标志的情况下构建 Emscripten 编译环境中,可能会发生前一种情况。
jsFuncToWasm()
function jsFuncToWasm(function, signature)
function jsFuncToWasm(signature, function)
创建一个包装给定 JS 函数的 WASM 函数,并返回该 WASM 函数的 JS 绑定。函数签名字符串必须采用 jaccwabyt 或 Emscripten 的 addFunction()
使用的格式。简而言之:它可能具有以下格式之一
Emscripten:
"x..."
,其中第一个 x 是表示结果类型的字母,后续字母表示参数类型。见下文。没有参数的函数只有一个字母。Jaccwabyt:
"x(...)"
,其中x
是表示结果类型的字母,括号中的字母(如果有)表示参数类型。没有参数的函数使用x()
。见下文。
支持的字母
i
= int32p
= int32(“指针”)j
= int64f
= float32d
= float64v
= void,仅在用作结果类型时才合法
如果使用无效的签名字母,则会抛出异常。
Jaccwabyt 格式签名3支持一些此处没有特殊含义的其他字母,但在(此上下文中)充当其他字母的别名
s
、P
:与p
相同
scopedInstallFunction()
pointer scopedInstallFunction(funcSignature, function)
pointer scopedInstallFunction(function, funcSignature)
此函数的工作方式与 installFunction()
完全相同,只是安装范围限于当前的 分配范围,并在当前分配范围弹出时卸载。如果当前没有活动分配范围,则会抛出异常。
uninstallFunction()
Function uninstallFunction(pointer)
需要之前从 wasm.installFunction()
返回的指针值。从 WASM 函数表中删除该函数,将其表槽标记为可供重用,并返回该函数。在调用 installFunction()
之前调用此函数是非法的,如果参数不是由该函数返回的,则结果未定义。返回的函数可以传递回 installFunction()
以重新安装它。
通用实用程序函数
isPtr()
boolean isPtr(value)
如果其值为 WASM 指针类型,则返回 true。也就是说,它是一个大于或等于零的 32 位整数。
关于混合 JS 和 C 代码的 WASM 特定特性
另请参阅:Gotchas
从 WASM 到 C 的转换是一个相对透明的过程。使用少量粘合代码,从 C 到 JS 的转换在大多数情况下也相对透明。本章涵盖了并非完全透明的方面。
从 JS 使用输出指针参数
输出指针参数在 C 中很常见。相反,它们在 JavaScript 中根本不存在。在 sqlite3 API 中,一个例子是
int sqlite3_open_v2(const char *zDbFile, sqlite3** pDb, int flags, const char *zVfs);
第二个参数上的两个指针限定符表示它是一个所谓的输出参数:函数可以通过为该指针分配一个新值来向调用方报告一个值。
在 JavaScript 中使用输出指针需要几个步骤
- 分配 WASM 内存以保存指针值。
- 使用
wasm.pokePtr()
或等效方法为其分配初始值(通常为 0)。 - 调用 WASM 函数并将指针传递给它。
- 使用
wasm.peekPtr()
或等效方法获取输出指针的新值。这在语义上等效于在 C 中取消引用指针。 - 释放步骤 (1) 中分配的指针。
以其最简单的形式,它看起来像这样
const wasm = sqlite3.wasm;
const ppOut = wasm.alloc(wasm.ptrSizeof); // allocate space for a pointer
wasm.pokePtr(ppOut, 0); // zero out the memory
const rc = some_c_function( ..., ppOut ); // pass ppOut to a C function
const pOut = wasm.peekPtr(ppOut); // fetch the pointed-to value
wasm.dealloc(ppOut); // free space for the pointed-to value
// pOut now holds the output result value.
if(0===rc) { ... success ... }
else { ... error ... }
显然,这是一个相当多的代码,并且存在一些弱点,例如如果 some_c_function()
抛出 JS 异常,则会泄漏 ppOut
内存。使用 sqlite3.wasm
和 try
/finally
块可以显着清理此问题
const scope = wasm.scopedAllocPush();
try {
const ppOut = wasm.scopedAllocPtr(); // alloc and zero pointer
const rc = some_c_function( ..., ppOut );
const pOut = wasm.peekPtr(ppOut);
if(0===rc) { ... success ... }
else { ... error ... }
}finally{
// free all "scoped allocs" made in the context of `scope`,
// in our case ppOut.
wasm.scopedAllocPop(scope);
}
或者不使用“作用域分配”机制
let pOut, ppOut;
try {
ppOut = wasm.allocPtr(); // alloc and zero pointer
const rc = some_c_function( ..., ppOut );
pOut = wasm.peekPtr(ppOut);
if(0===rc) { ... success ... }
else { ... error ... }
}finally{
wasm.dealloc(ppOut);
}
或者使用“pstack”分配器,它是为了精确地解决此类情况而添加的一个更高效(更快)的选项
const stack = wasm.pstack.pointer;
try {
const ppOut = wasm.pstack.allocPtr(); // "alloc" and zero memory
const rc = some_c_function( ..., ppOut );
const pOut = wasm.peekPtr(ppOut);
if(0===rc) { ... success ... }
else { ... error ... }
}finally{
wasm.pstack.restore(stack);
}
请注意,pstack
具有一个小的静态内存缓冲区,因此不能用于通用分配。尽管它是静态内存,但它被访问的方式就好像它是 WASM 堆的一部分一样,因此可以通过与堆内存相同的内存访问例程来访问它。
使用 try
/finally
块是 sqlite3 JS 代码中的一种常见习惯用法,广泛用于管理内存和对象生命周期。无论 try
块如何退出,finally
块都将执行:通过 return
、continue
、break
、throw
或运行到完成。当它执行时,在 try
块中进行的任何“作用域”分配都将被释放。在此示例中,我们只有一个这样的分配,但多个分配并不少见。作用域分配 API 在许多常见用例中简化了内存释放,而不是使用低级 alloc()
和 dealloc()
例程(它们是 C 级 sqlite3_malloc()
和 sqlite3_free()
的对应部分)。peekPtr()
和 pokePtr()
帮助程序是 peek()
和 poke()
的简化包装器,消除了记住为后者的最终参数传递除默认值以外的一些值的需要(默认值是上面演示的此类情况所需的错误值)。
- ^ 不要与
sqlite3.wasm
文件混淆,sqlite3.wasm
命名空间有效地包装了该文件。 - ^ 如 ./c-structs.md 中所述
- ^ 此代码与 jaccwabyt 共同开发,因此支持其签名格式