SQLite3 在其接口中使用了一些 C 结构体,其中大多数是“不透明的”。也就是说,它们的内容对客户端代码不可见,并且它们可以在库的任何给定版本中更改而不会违反兼容性约束。但是,某些结构体并不透明,并且由 sqlite3 API 用于实现与客户端代码的双向通信。例如,sqlite3_module 和 sqlite3_vfs。
不透明类型,例如 sqlite3
和 sqlite3_stmt
,在本 API 中仅由其 WASM 指针值(整数)表示。非不透明类型在本 API 中可能具有两种不同的表示形式
- 它们的 WASM 指针。
- 一个 JavaScript 级别的包装器,它反映了 C 级别的对应结构,并允许检查和操作单个结构实例的内容。
此库中如何实现后者在 Jaccwabyt 的文档中进行了详细介绍,这是一个为了支持该库而创建的该库的分支子项目,但它不依赖于该项目(它可以被任意 WASM/JS 客户端使用)。本文档提供了对该支持的高级概述,并描述了此库中如何使用此类绑定。它不涵盖整个 Jaccwabyt API,只涵盖与 sqlite3 客户端相关的部分。
简而言之,此功能通过在 C 中创建 C 结构体的 JSON 格式描述、将它们导入到 JS 中,然后生成使用 JS 属性拦截器和 JS 的 DataView
API 来代理访问/从 WASM 堆中的 C 级内存进行访问来实现。
以下非不透明 sqlite3 结构体映射到 JS
- VFS 相关
- 虚拟表 相关
sqlite3_module
sqlite3_vtab_cursor
sqlite3_vtab
sqlite3_index_info
及其内部类型sqlite3_index_constraint
sqlite3_index_orderby
sqlite3_index_constraint_usage
所有这些都可以在 JS 中作为名为 sqlite3.capi.TheStructName
的构造函数使用,除了 sqlite3_index_info
的内部类,它们都是 sqlite3_index_info
的属性1。
(反)分配实例和包装现有实例
每个 JS 包装的结构体在本库中都有两种不同的用途
- 使用不带参数的构造函数创建一个新的 WASM 堆实例,以便将其连接到 C。在这种情况下,客户端 JavaScript 代码拥有该实例的内存,除非某些 API 明确接管它。
- 将 WASM 指针传递给构造函数将为结构体的现有实例(无论它来自 C 还是 JS)创建一个 JS 级别的包装器,而不会接管该内存的所有权。这允许 JS 在不接管其内存的情况下操作在 C 中创建的实例2。
两种用途都很常见,它们的区别仅在于它们如何管理(或不管理)结构体的内存。
只要结构体实例处于活动状态,它的 pointer
属性就会解析为其 WASM 堆内存地址。该值可以传递给任何接受该类型指针的 C 例程。例如
const m = new MyStruct();
functionTakingMyStructPointers( m.pointer );
当客户端代码完成对某个实例的操作时,并且没有 C 级别的代码使用其内存,则必须通过调用 theStruct.dispose()
来清理该结构体实例。多次调用 dispose()
是无害的——第一次之后的调用是无操作的。调用 dipose()
对于包装的实例不是严格要求的,因为它们的 WASM 堆内存由其他地方拥有,但调用它是好的做法,因为每个实例都可能拥有除了结构体内存之外的内存,如下一节所述。
自定义清理:ondispose
如果给定的 JS 端结构体实例有一个名为 ondispose
的属性,则在调用 dispose()
时将使用该属性来释放可能与该结构体关联的任何其他资源(例如 C 分配的字符串或其他结构体实例)。
ondispose
不是默认设置的,但可以由客户端设置为以下内容之一
- 如果它是一个函数,则不带参数调用它,并将被处置的对象作为其
this
。它可以执行任意清理。 - 如果它是一个数组,则数组的每个条目都可以是以下内容之一
- 一个函数,如上所述。
- 任何 JS 绑定的结构体实例都会调用其
dispose()
方法。 - 一个数字被假定为一个 WASM 指针,它将使用
sqlite3.wasm.dealloc()
释放。 - 任何其他值类型将被忽略。有时使用字符串条目注释数组有助于理解代码。例如
x.ondispose = ["Wrapper for this.$next:", y]
ondispose
回调抛出的任何异常都会被忽略,但可能会在控制台中引发警告。
客户端代码可以调用 aStructInstance.addOnDispose()
以将一个或多个参数推送到处置列表中。该函数将在需要时创建一个 ondispose
数组,或者将非数组 ondispose
值移动到新创建的 ondispose
数组中。它将返回其 this
。
访问结构体成员
使用传统的 JS 属性访问运算符从 JS 访问 C 结构体成员。
C 结构体与其 JS 对应项之间的一个明显区别是,C 级别的结构体成员在 JS 中都带有 $
名字前缀。因此,C 中的 myVfs->xOpen
在 JS 中为 myVfs.$xOpen
。此前缀的存在是为了让 JS 代码的作者和读者能够轻松地区分 C 级成员和 JS 级成员,以及避免 C 和传统 JS 级成员之间的任何命名冲突3。
当从 JS 访问结构体级别的成员时,属性拦截器将获取或分配底层的 C 内存并执行适当的类型和字节序转换(如果分配了它无法合理转换的值,则抛出异常)。
访问 C 字符串值
专门标记为 C 风格字符串的成员有几个其他成员没有的选择
structInstance.setMemberCString(memberName, jsString)
覆盖(不释放)该成员中的任何现有值,用新分配的 C 字符串替换它,并将该 C 字符串存储在实例的ondispose
状态中,以便在调用dispose()
时清理。该结构体无法知道在覆盖这些字符串时是否可以安全地释放它们,因此而是将以这种方式设置的每个字符串都添加到ondispose
列表中。structInstance.memberToJsString(memberName)
获取成员的值。如果它是 NULL,则返回null
,否则假定它是一个有效的 C 风格字符串,并返回其副本作为 JS 字符串。structInstance.memberIsString(memberName)
如果给定成员名称被明确标记为字符串,则返回 true。
以下约束适用于这些方法
memberName
参数必须是 JS/C 绑定的结构体成员的名称。它可以选择包含$
前缀字符。- 任何未知的结构体成员名称都会触发异常。
- 在低级结构体描述中没有明确标记为字符串的成员将在
setMemberCString()
和memberToJsString()
中触发异常,但在memberIsString()
中不会触发异常。
sqlite3 特定的 API 扩展
上面列出的 API 和功能都是 Jaccwabyt 框架的一部分。本节涵盖专门为 sqlite3 API 添加的结构体框架功能。
安装 JS 函数作为方法指针
所有 StructType 实例都继承以下方法,以帮助安装引用 JavaScript 函数的 C 端成员函数指针。
installMethod()
用法
function installMethod(name, func, applyArgcCheck = false)
structTypeInstance installMethod(methodsObject, applyArgcCheck = false)
第二种形式的行为与 installMethods()
完全相同。
在此对象中安装给定名称和函数的 StructBinder 绑定函数指针成员。
它为给定函数创建一个 WASM 代理,并安排在调用 this.dispose()
时清理该代理。在任何错误的暗示下都会抛出异常,例如,给定名称不映射到结构体绑定成员。
作为特例,如果给定函数是一个指针,则 wasm.functionEntry()
用于验证它是否是一个已知函数。如果是,则直接使用它,而无需额外的代理或清理级别,否则会抛出异常。传递 0 的值是合法的,表示 NULL 指针,但需要注意的是,0 是 WASM 中合法的函数指针,但它在此处不会被接受为函数指针。(理由:地址为零的函数必须是最初来自 WASM 模块的函数,而不是我们要绑定到客户端级扩展代码的方法。)
此函数返回一个绑定到 this
的代理,并接受 2 个参数 (name,func)。该函数返回与该函数相同的值,允许调用进行链接。
如果只调用 1 个参数,则它没有副作用,但会返回一个与上面描述的签名相同的 func。
⚠注意:⚠ 因为我们无法泛化地知道如何将 JS 异常转换为结果代码,所以安装的函数不会自动捕获异常。为了避免 C 层的未定义行为,至关重要的是,通过该函数映射的方法不要抛出异常。规则的例外是...
如果 applyArgcCheck
为 true,则每个 JS 函数(与函数指针相反)都会被包装在一个代理中,该代理断言它被传递了预期的参数数量,如果参数数量与预期不符,则会抛出异常。这仅用于开发时用于健全性检查,因为通过此类方法传递的异常会使 C 环境处于未定义状态。
installMethods()
structBinderInstance installMethods(methods, applyArgcCheck = false)
将方法安装到此 StructType 类型实例中。给定方法对象中的每个条目都必须映射到给定 StructType 的已知成员,否则会触发异常。有关更多详细信息,请参见 installMethod()
,包括第二个参数的语义。
作为上述情况的例外,如果方法对象中的任何两个或多个方法是完全相同的函数,则不会为第二个和后续实例调用 installMethod()
,而是将这些实例分配给为第一个实例创建的相同方法指针。此优化主要用于适应对 sqlite3_module::xConnect
和 xCreate
方法的特殊处理。
成功后,返回此对象。发生错误时抛出异常。
- ^ 在 C 中,这些结构体是在
sqlite3_index_info
中内联定义的,因此将它们作为该 JS 类的成员属性似乎很合适。 - ^ 需要注意的是,如果需要,
ondispose
机制 *可以* 用于将内存有效地转移到 JS,但在实践中从未有必要。 - ^ 需要注意的是,客户端代码可以自由地添加以
$
为前缀的 JS 专属属性,但这样做可能会导致将来代码维护混乱。