JavaScript 中的 C 结构体

SQLite3 在其接口中使用了一些 C 结构体,其中大多数是“不透明的”。也就是说,它们的内容对客户端代码不可见,并且它们可以在库的任何给定版本中更改而不会违反兼容性约束。但是,某些结构体并不透明,并且由 sqlite3 API 用于实现与客户端代码的双向通信。例如,sqlite3_modulesqlite3_vfs

不透明类型,例如 sqlite3sqlite3_stmt,在本 API 中仅由其 WASM 指针值(整数)表示。非不透明类型在本 API 中可能具有两种不同的表示形式

此库中如何实现后者在 Jaccwabyt 的文档中进行了详细介绍,这是一个为了支持该库而创建的该库的分支子项目,但它不依赖于该项目(它可以被任意 WASM/JS 客户端使用)。本文档提供了对该支持的高级概述,并描述了此库中如何使用此类绑定。它不涵盖整个 Jaccwabyt API,只涵盖与 sqlite3 客户端相关的部分。

简而言之,此功能通过在 C 中创建 C 结构体的 JSON 格式描述、将它们导入到 JS 中,然后生成使用 JS 属性拦截器和 JS 的 DataView API 来代理访问/从 WASM 堆中的 C 级内存进行访问来实现。

以下非不透明 sqlite3 结构体映射到 JS

所有这些都可以在 JS 中作为名为 sqlite3.capi.TheStructName 的构造函数使用,除了 sqlite3_index_info 的内部类,它们都是 sqlite3_index_info 的属性1

(反)分配实例和包装现有实例

每个 JS 包装的结构体在本库中都有两种不同的用途

两种用途都很常见,它们的区别仅在于它们如何管理(或不管理)结构体的内存。

只要结构体实例处于活动状态,它的 pointer 属性就会解析为其 WASM 堆内存地址。该值可以传递给任何接受该类型指针的 C 例程。例如

const m = new MyStruct();
functionTakingMyStructPointers( m.pointer );

当客户端代码完成对某个实例的操作时,并且没有 C 级别的代码使用其内存,则必须通过调用 theStruct.dispose() 来清理该结构体实例。多次调用 dispose() 是无害的——第一次之后的调用是无操作的。调用 dipose() 对于包装的实例不是严格要求的,因为它们的 WASM 堆内存由其他地方拥有,但调用它是好的做法,因为每个实例都可能拥有除了结构体内存之外的内存,如下一节所述。

自定义清理:ondispose

如果给定的 JS 端结构体实例有一个名为 ondispose 的属性,则在调用 dispose() 时将使用该属性来释放可能与该结构体关联的任何其他资源(例如 C 分配的字符串或其他结构体实例)。

ondispose 不是默认设置的,但可以由客户端设置为以下内容之一

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 风格字符串的成员有几个其他成员没有的选择

以下约束适用于这些方法

sqlite3 特定的 API 扩展

上面列出的 API 和功能都是 Jaccwabyt 框架的一部分。本节涵盖专门为 sqlite3 API 添加的结构体框架功能。

安装 JS 函数作为方法指针

所有 StructType 实例都继承以下方法,以帮助安装引用 JavaScript 函数的 C 端成员函数指针。

installMethod()

用法

  1. function installMethod(name, func, applyArgcCheck = false)
  2. 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::xConnectxCreate 方法的特殊处理。

成功后,返回此对象。发生错误时抛出异常。


  1. ^ 在 C 中,这些结构体是在 sqlite3_index_info 中内联定义的,因此将它们作为该 JS 类的成员属性似乎很合适。
  2. ^ 需要注意的是,如果需要,ondispose 机制 *可以* 用于将内存有效地转移到 JS,但在实践中从未有必要。
  3. ^ 需要注意的是,客户端代码可以自由地添加以 $ 为前缀的 JS 专属属性,但这样做可能会导致将来代码维护混乱。