本页面介绍了 WASM/JS 代码的各种构建以及如何构建自定义版本。
sqlite3.js
和 sqlite3.wasm
是“标准构建”。它们是为在众多浏览器上广泛部署而构建的。
源代码树中 ext/wasm
目录中的 GNUmakefile 创建了这些文件。本文档的其余部分描述了构建它们的步骤,以便客户端可以创建自定义构建并为除 Emscripten 之外的其他工具链调整构建。
这些说明假设
- 一个支持... 的类 Linux 系统
- 最新版本的 Emscripten SDK。该项目可以使用其他 WASM 工具链构建,但目前在没有 Emscripten 提供的文件 I/O 模拟层的情况下构建时无法完成太多操作。改善对其他构建环境的支持是持续的项目目标。
前言:目标平台
该项目的主要交付成果是“最低公分母”JavaScript,它可以在各种环境中使用,其中 Web 浏览器优先于服务器端 JS 引擎。我们不直接支持各种子平台,例如各种基于 node.js 的工具,因为
- 该项目的开发人员都没有使用与 node.js 相关的工具,除了通过 Emscripten “幕后”使用之外。
- JavaScript 生态系统比大多数生态系统都更加灵活,而且该项目缺乏开发人员带宽来获得并维护对不断变化的 JS 环境的专业知识。
如果“原始”JS 在给定的第三方环境中不起作用,我们很乐意帮助用户集成它,但我们要求这些环境的用户积极配合才能做到这一点。这种合作不可避免地需要与开发人员进行来回互动,因为会发现和解决任何给定环境的怪癖。
总的来说,我们将环境特定构建(例如 npm)的开发留给那些对其有积极兴趣和需求的人。我们提供了一个 社区维护的 npm 构建,但不能保证它适用于该生态系统中工具的每种组合。我们还提供了核心 JS 文件的单独构建版本,这些构建版本旨在与某些工具兼容,这些工具通常在基于 node.js 的构建环境中出现,例如“捆绑器”。有关详细信息,请参阅 API 索引。
该项目的目标不是成为唯一的 SQLite WASM 发行版,我们欢迎其他发行版和完全不同的实现,特别是那些涵盖我们缺乏带宽和主题专业知识以充分发挥作用的用例的发行版。
快速入门:规范构建
规范构建文件(即存在于 sqlite3 自身源代码树中的文件)支持几种不同的构建目标,这些目标可能对除开发人员之外的人员也有用。它们需要完整的 sqlite3 源代码树检出(而不是合并发行版)、GNU Make 和 Emscripten SDK。强烈建议使用 wabt 工具。
最简单的方法是从 sqlite3 源代码树的检出副本的顶部 执行以下操作
$ ./configure --enable-all
$ make sqlite3.c
$ cd ext/wasm
$ make
提示:并行构建(例如使用
make -j4
)应该可以正常工作,但要可靠地做到这一点,需要事先运行make sqlite3.c
步骤。
有用的目标包括
- 默认目标(如果未提供目标)使用
-O0
优化级别构建,因为它比任何其他级别编译得快得多。 o0
使用-O0
优化级别构建,并且可能会添加默认构建中没有的优化标志。o1
、o2
、o3
、os
和oz
使用其第二个字符指示的-OX
级别构建。release
等效于oz
。snapshot
创建一个“预发行快照”zip 文件。dist
创建一个适合在 规范下载页面 中使用的 zip 文件。这与“快照”构建的区别仅在于生成的 zip 文件和嵌入目录的名称。
大量实验表明,-O2
提供最快的二进制文件,但 -Oz
提供最小的二进制文件(注意,需要 wasm-strip 来减小其大小)。-O2
和 -Oz
构建之间的速度差异通常只有大约 10%。
每个构建都会产生许多交付成果,包括
jswasm/sqlite3.{js,mjs,wasm}
是库和 其 API 的核心版本。还有许多其他 JS 和 WASM 文件构建到此目录。sqlite3.mjs
与sqlite3.js
相同,除了将其作为 ES6 模块加载所需的非常小的差异之外。jswasm/*-bundler-friendly.*
是 旨在与 JS “捆绑器”工具一起使用 的变体构建。fiddle/fiddle-module.{js,wasm}
是 fiddle 应用程序的核心。fiddle/
目录可以按原样复制并通过 Web 服务器提供服务,以托管 sqlite3 fiddle 应用程序。
顶级目录中剩余的交付成果是各种演示和测试,这些演示和测试既不必要也不(很可能)对大多数人来说很有趣。
64位整数
JavaScript 的核心 Number
类型不支持 64 位整数,这会导致 WASM 和 JS 在 64 位整数方面出现一些摩擦(而两者都支持 64 位浮点数)。
许多 sqlite3 API 接受或返回 int64 值,但所有这些 API 都是禁用的,除非 sqlite3.js
使用对 JavaScript 的 BigInt 类型的支持进行构建。这是一个构建时选项。
规范构建默认情况下启用 BigInt 支持。自定义构建可能会禁用它,但必须注意,所有接受 int64 参数或返回 int64 参数的 sqlite3 C API 都将不可用,因为在 JS 绑定中不可用。
WASM 堆内存
WASM 的设计特性之一是 WASM 模块如何获得用于工作的 RAM。它们必须具有以下两种情况之一
- 起始内存限制已编译到 WASM 文件中。
- 内存由客户端从 JavaScript 分配并提供给 WASM 模块加载器。不幸的是,由于 WASM 模块加载过程的工作方式,在加载 sqlite3 模块时,无法从任意客户端级代码提供此功能。要解决此限制,需要对上游工具进行更改,要么提供一个挂钩来 Emscripten,要么将最关键的 Emscripten 提供的功能带到其他 WASM 运行时。
根据构建配置,WASM 模块可能能够也可能不能在运行时增加其内存量。如果不能,并且内存不足,则分配尝试将失败。这是否对 WASM 模块完全致命取决于构建选项。如果它不是致命的,则应用程序代码必须自行检查内存不足情况,就像它在 C 中一样。如果它被配置为致命,则该模块将在任何分配失败时发出响亮的警告(在浏览器的开发控制台中,不一定是客户可见的地方),并停止工作。
堆内存大小是客户可能希望或需要针对给定部署进行自定义的众多构建选项之一。分配给模块的默认堆内存是根据 sqlite3 模块的测试和开发选择的。随着通过第三方客户端应用程序获得经验,模块的未来版本可能会增加 WASM 模块接收的初始内存量。
预处理 JS 文件
为了能够在同一个源文件中同时支持传统的 JavaScript 和 ES6 模块(又名 ESM),我们必须“预处理”某些源文件以过滤特定的代码部分,这些部分根据它们是为传统的 JS 还是 ESM 构建而进行过滤。由于 C 预处理器在用于预处理 JS 代码时会弄乱事情,因此专门为该项目创建了一个自定义预处理器,俗称C-Minus 预处理器,或 c-pp
。它作为 一个独立的辅助项目 进行维护,但 sqlite3 源代码树包含 它自己的副本。
特定 JS 文件不能按原样使用,而必须通过 c-pp
运行。虽然这在一定程度上使创建自定义构建变得复杂,但它似乎是“较小的错误”,因为它是一种方法,可以通过这种方法维护 JS 代码,使其可被两种 JS 版本使用。
提示:需要预处理的文件的扩展名为
.c-pp.X
,其中X
是该文件类型的典型扩展名,例如html
或js
。
预处理器的指令不会直接干扰源代码,但会导致以下结构,如果在 JS 中运行这些结构而不对它们进行预处理,将导致(充其量)语法错误,或者(最坏的情况)可能导致静默错误。以下是一个预处理块的示例片段
const W =
//#if target=es6-module
new Worker(new URL(options.proxyUri, import.meta.url));
//#else
new Worker(options.proxyUri);
//#endif
JS 关键字 import
和 export
会在非 ESM 代码中引发语法错误,因此可能无法通过 JS 的内省功能在运行时将其过滤掉。它们提供的功能对于 ESM 模块的客户端使用至关重要,因此不能简单地将其视为“很好”而忽略。即我们必须以某种方式支持 ESM,而预处理为我们提供了一种相对无痛的方法来实现这一点。
预处理器支持其关键字的可配置前缀,此树中的 JS 代码使用前缀 //#
,因此此类结构不会直接干扰 JS 感知文本编辑器中的语法高亮和代码缩进。预处理器的关键字不能缩进:它们必须从第 0 列开始。
预处理后,上面的块将简化为以下两种情况之一
const W =
new Worker(new URL(options.proxyUri, import.meta.url));
或者
const W =
new Worker(options.proxyUri);
预处理器的使用量保持在最低限度。截至撰写本文时,它仅用于阻止需要 import
ESM 关键字的代码。所有使用预处理器的代码都可以通过在 JS 文件中搜索 '^//#'
来找到。
此类预处理的完整技术细节在 GNUMakefile
中维护。
SQLite 加密扩展(SEE)
构建过程支持使用 SQLite 加密扩展(SEE)版本的 SQLite 合并源文件进行构建。有关详细信息,请参阅 see.md。
添加客户端自定义初始化代码
WASM 构建不支持动态加载扩展,因为没有可用于基于 Web 的 WASM 的兼容 dlopen()
等效项,但客户可以在编译时使用本节中描述的方法添加扩展。
如果构建在构建目录 (ext/wasm
) 中看到名为 sqlite_wasm_extra_init.c
的文件,则适用以下内容
- 构建定义
SQLITE_EXTRA_INIT=sqlite3_wasm_extra_init
- 该文件将添加到编译过程。
- 该文件必须定义一个具有此签名的函数
int sqlite3_wasm_extra_init(const char *)
它将在库初始化阶段被调用,并传递NULL
。如果函数返回非 0,则库的初始化将失败。
该函数可以安装客户关心的任何扩展,前提是它们可以由 WASM 构建过程编译。请注意,构建不会检测和编译其他 C 文件,因此扩展应该完全复制到 sqlite3_wasm_extra_init.c
中,或 该文件应该使用
#include "/path/to/your/extension.c"
(注意 .c
扩展名) 用于每个包含的扩展。
客户可以通过将 sqlite3_wasm_extra_init.c=/path/to/its/replacement.c
传递给 make
来覆盖文件名 sqlite3_wasm_extra_init.c
。
源代码树包含一个用于引导目的的示例/模板文件,名为 example_extra_init.c
。只需重命名它,编辑它以适合,然后重建。
C 文件
构建 sqlite3.wasm
需要两个本地 C 文件和一个本地 H 文件,以及它需要的任何系统级头文件
sqlite3.c
和sqlite3.h
包含规范的合并构建。sqlite3-wasm.c
扩展了合并的一小部分,以添加需要 C 端支持的少量 WASM 特定 API。绝大多数 JS/WASM API 完全在 JavaScript 中实现,只有少量基础设施级支持代码在 C 中实现。WASM 特定的 C 例程仅供 JS 层使用,除非明确记录为公开 API,否则不被视为公开 API。也就是说,它们是“不受支持的”,并且可能会随时更改。如果它们没有在这些页面中记录,那么它们就不是公开 API。
在编译时,只有 sqlite3-wasm.c
被编译。它 #include
's sqlite3.c
,因为它需要访问后一个文件的一些内部使用状态。它还为 sqlite3.c
定义了许多与 WASM 相关的配置标志。因为它使用 sqlite3.c
内部状态,sqlite3-wasm.c
仅用于与从同一版本项目源代码树生成的 sqlite3.c
一起构建。
JS 文件 (sqlite3-api.js
)
经常提到的 sqlite3.js
是由构建过程从多个其他文件生成的。本节概述了这些文件,作为希望创建自定义构建的人员的指南。
使用 ext/wasm
目录 作为引用文件的基目录,以下文件用于创建 JS 合并,按组装顺序列出
api/sqlite3-api-prologue.js
包含 sqlite3 API 对象的初始引导设置。它被公开为函数,而不是对象,以便下一步可以传入一个配置对象,该对象抽象化了 WASM 环境的部分内容,以方便将其插入到任意 WASM 工具链中。common/whwasmutil.js
一个半第三方 JS/WASM 实用程序代码集合,旨在替换大部分 Emscripten 胶水,并为我们提供更多对可用 JS/WASM 桥接功能的控制。sqlite3 API 在内部使用这些 API 而不是它们的 Emscripten 对应物。原则上,此 API 可配置,用于与任意 WASM 工具链一起使用。它是“半第三方”的,因为它是为了支持此树而创建的,但它是独立的,并且与......一起维护。jaccwabyt/jaccwabyt.js
另一个半第三方 API,jaccwabyt 在 JS 和 C 结构之间创建绑定,这样 JS 或 C 对结构状态的更改将对连接的另一端可见。这也是一个独立的分支项目,专为 sqlite3 项目而构思,但单独维护。例如,它用于创建 OPFS sqlite3_vfs 在 JS 中。api/sqlite3-api-glue.js
调用前两个文件公开的功能,以充实sqlite3-api-prologue.js
的低级部分。大多数这些部分都与填充sqlite3.wasm
对象并使用它创建 WASM 导出函数的更 JS 友好包装器相关。此文件还删除了上述文件创建的大多数全局范围符号,有效地将它们移入用于初始化 API 的范围。<build>/sqlite3-api-build-version.js
由构建过程创建,并填充sqlite3.version
对象。api/sqlite3-api-oo1.js
为更低级的 C API 提供高级面向对象包装器,俗称 OO API #1。它的 API 与其他高级 sqlite3 JS 包装器类似,对于熟悉此类 API 的任何人来说应该感觉比较熟悉。也就是说,它不是“必需组件”,可以从不需要它的构建中省略。api/sqlite3-api-worker1.js
基于工作线程的 API,它使用 OO API #1 提供对数据库的接口,该数据库可以通过工作线程消息传递接口从主窗口线程驱动。与 OO API #1 一样,这是一个可选组件,它为这种 API 提供了众多潜在实现中的一种。api/sqlite3-worker1.js
不是合并源的一部分,旨在由客户端工作线程加载。它加载 sqlite3 模块并运行 工作线程 #1 API,该 API 在sqlite3-api-worker1.js
中实现。api/sqlite3-worker1-promiser.js
同样不是合并源的一部分,并提供对工作线程 #1 API 的基于 Promise 的接口。这是与在单独的工作线程中运行的数据库交互的更友好的方式。
api/sqlite3-v-helper.js
此仅供内部使用的文件安装用于sqlite3-*.js
其他文件的实用程序,这些文件创建自定义sqlite3_vfs
或 虚拟表 实现。api/sqlite3-vfs-opfs.c-pp.js
一个 sqlite3 VFS 实现,支持 Origin-Private FileSystem (OPFS) 作为存储层,为浏览器中的数据库文件提供持久存储。如果激活,它需要...api/sqlite3-opfs-async-proxy.js
是 OPFS 代理的异步后端部分。它直接与 (异步) OPFS API 交谈,并将这些结果传递回其同步对应物。此文件,因为它必须在自己的工作线程中启动,所以不是合并的一部分。
api/sqlite3-api-cleanup.js
前面的文件不会立即扩展库,因为它们最初被评估。相反,它们添加了回调函数,这些函数将在库的引导阶段被调用。有些还会暂时创建全局对象,以便将它们的状态传递给后面的文件。此文件清理任何悬空的全局变量并运行 API 引导过程,该过程最终执行由前面的文件安装的初始化代码。此代码确保前面的文件最多只安装一个全局符号。在将 API 调整为非 Emscripten 工具链时,这“应该”是唯一需要更改的文件。
扩展名为 .c-pp.js
的文件旨在 使用 c-pp
处理,请注意,这种预处理可以在所有相关文件连接后应用。该扩展名主要用于提醒代码维护人员这些文件包含在 JavaScript 中无法直接运行的结构。
将这些文件连接起来的结果(除了那些被标记为外部的文件)通常命名为 sqlite3-api.js
。该文件包含完整的 JS API,但它需要在引导之前加载 sqlite3.wasm
,此过程主要用于配置 JS 部分以使用当前 WASM 环境。引导需要每个 WASM 构建环境特有的部分,并在 sqlite3-api-cleanup.js
中发生。使用自定义构建,可以通过替换 sqlite3-api-cleanup.js
的部分内容来进一步延迟引导,也许是为了让最终客户在引导之前进一步扩展库,或为引导过程提供更自定义的配置。
sqlite3-api.js
可能会根据构建环境进一步复合到其他 JS 文件中。
例如,在 Emscripten 驱动的构建中,该文件被夹在其他文件中,这些文件以一种方式将它包装起来,以便 Emscripten 模块初始化过程会激活它。然后,该捆绑包被包含到生成的 Emscripten 生成的 sqlite3.js
中,其中不仅包含 sqlite3 API,还包含加载 sqlite3.wasm
和 Emscripten 提供的各种实用程序代码所需的所有基础设施。
以下文件是构建过程的一部分,但与 sqlite3-api.js
一起注入到构建生成的 sqlite3.js
中。
extern-pre-js.js
Emscripten 特定的标头,用于 Emscripten 的--extern-pre-js
标志。截至撰写本文时,该文件仅用于实验目的,不包含与生产交付相关的任何代码。pre-js.c-pp.js
Emscripten 特定的标头,用于 Emscripten 的--pre-js
标志。此文件提供了一个地方来覆盖 Emscripten 启动之前的某些 Emscripten 行为。post-js-header.js
Emscripten 特定的标头,用于--post-js
输入。它通过为 Emscripten 启动后运行处理程序来打开词法范围。post-js-footer.js
Emscripten 特定的页脚,用于--post-js
输入。这关闭了post-js-header.js
打开的词法范围。extern-post-js.c-pp.js
Emscripten 特定的标头,用于 Emscripten 的--extern-post-js
标志。此文件用一个函数覆盖 Emscripten 安装的sqlite3InitModule()
函数,该函数在模块加载后还会初始化 sqlite3 模块的异步部分。例如,OPFS VFS 支持。
构建 JS/WASM 文件
除了下面提到的工具外,wabt 工具 也强烈推荐,主要用于 wasm-strip
。
Emscripten
Emscripten 为中心的构建过程在 Emscripten 页面 上有介绍。
wasi-sdk
sqlite3 可以使用 wasi-sdk 构建,用于“服务器端”使用,但此类构建无法与提供的 JavaScript 代码一起使用,因为 WASI 构建需要我们无法提供的 JS 导入(但 Emscripten 可以提供)。
以下是在 Ubuntu 风格的 Linux 系统上设置 wasi-sdk 的简要说明
$ git clone --recursive https://github.com/WebAssembly/wasi-sdk.git
$ cd wasi-sdk
$ sudo apt install ninja-build cmake clang
$ NINJA_FLAGS=-v make package
# ^^^^ go order a pizza, wait for it to arrive, eat it, and
# check back. Maybe it'll be done by then. Maybe. As of this writing,
# it has to compile more than 3000 C++ files.
$ sudo ln -s $PWD/build/wasi-sdk-* /opt/wasi-sdk
# ^^^^^^^ /opt/wasi-sdk is a magic path name for these tools
或者,更快,从该 git 页面获取预构建的 SDK 二进制文件,将其解压缩到某个位置,然后将 /opt/wasi-sdk
符号链接到它。
安装完毕后,可以告诉规范源代码树使用它
$ ./configure --with-wasi-sdk=/opt/wasi-sdk
$ make
纯 clang
与 wasi-sdk 构建一样,可以使用普通 clang 编译 sqlite3.wasm
,但生成的二进制文件不能直接与该项目的 JavaScript 代码一起使用,因为缺少 JS/WASM 导入/导出。
为 SQLite 加密扩展 (SEE) 构建
自 2023-03-08 以来,规范的 WASM 构建支持针对 SQLite 加密扩展 (SEE) 构建,方法是:
- 在构建 WASM 组件之前,将
sqlite3-see.c
添加到树的最顶层目录,或者 - 在
ext/wasm
目录中运行make
或gmake
时传递sqlite3.c=/path/to/sqlite3-see.c
。(路径中不能包含任何空格。)
生成的 jswasm/sqlite3.wasm
将包含与 SEE 相关的 API,而 jswasm/sqlite3.[m]js
将将这些 API 添加到 sqlite3.capi
命名空间中,包括自动字符串类型参数转换。
注意事项
- SEE 是一款商业产品,WASM 构建与使用 SEE 源代码创建的二进制文件具有相同的发布限制。本项目的立场是,在许可证持有者的私有内网中使用 SEE WASM 构建符合 SEE 许可条款,但在更广泛的网络上发布则不符合。
- 这里只支持不需要第三方库的加密方法。具体而言:RC4、AES128-OFB 和 AES258-OFB。
- 目前还没有已知的方法可以完全将加密密钥隐藏在 JavaScript 客户端中。即使这些密钥被编译到 WASM 二进制文件中,它们也容易被反汇编成明文形式。
向 JS 导出新 API
将新的 API 导出到 WASM 需要做的不仅仅是将它们包含在 sqlite3.c
聚合文件中并启用任何必需的功能标志。简而言之,它需要
- 将它们添加到当前构建环境的导出函数列表中。对于基于 Emscripten 的构建,这意味着将它们添加到
ext/wasm/api/EXPORTED_FUNCTIONS.sqlite3-api
中,在每个函数前面加上下划线字符(因为 Emscripten 需要它)。 - 如果需要(通常是需要的),将绑定描述添加到 JS 代码中,这将确保它被安装到
sqlite3.capi
命名空间中。
这些步骤将在下面描述,但要注意 ⚠ 这些是内部细节,随时可能发生变化 ⚠!C 函数如何导出到 WASM 和 JS API 的具体细节不属于库的公共接口的一部分,至少部分取决于用于创建 WASM 文件的构建平台(即,其中一部分依赖于 Emscripten)。
也就是说...
扩展导出函数列表
ext/wasm/api/EXPORTED_FUNCTIONS.sqlite3-api
是一个纯文本文件,列出了所有必须导出到生成的 sqlite3.wasm
中的 C 函数。每个函数名称前面都有一个下划线,因为 Emscripten 需要它。为了维护方便,该列表应按字母顺序排列。可以将导出列在这里,但不要暴露给 JS。
如果/当其他构建平台可用时,它们可能会有自己的函数导出列表。
添加 sqlite3.capi
绑定
一旦从 WASM 导出函数,我们就可以通过 sqlite3.wasm.exports
命名空间访问它,但这种绑定是“原始”形式,这意味着对它们的实参或结果值不执行任何类型转换,例如对于字符串实参。为了将 API 暴露给公共接口并添加任何必要的类型转换,我们必须在内部绑定列表中添加一个条目。
⚠ ACHTUNG: 同样,以下内容是一个内部实现细节,随时可能发生变化。不要在客户端代码中依赖这些说明。
WASM 导出函数与其 JS 暴露对应项之间的映射存储在 ext/wasm/api/sqlite3-api-glue.js
中,具体位置取决于函数的作用以及函数是否在其实参或结果值中使用 64 位整数。绑定属于以下数组之一
sqlite3.wasm.bindingSignatures
包含大多数函数。这些绑定被映射到sqlite3.capi
命名空间中。sqlite3.wasm.bindingSignatures.int64
包含所有使用 64 位整数的函数。这些绑定在没有 BigInt 支持 的情况下构建时将被替换为抛出异常的占位符。sqlite3.wasm.bindingSignatures.wasm
包含 C 端函数,这些函数仅供项目自己的 JS API 内部使用,不属于公共接口的一部分。这些绑定被映射到sqlite3.wasm
命名空间中。那些暴露给公共 API 的例程将被手动重新导出到sqlite3.capi
命名空间,或者为它们提供等效的包装器。
每个数组都包含子数组,这些子数组描述函数实参和返回值类型,如 sqlite3.wasm.xWrap()
中所记录的那样。在库初始化期间,每个列出函数的类型转换代理都会被注入到上面描述的命名空间之一中。如果列出的任何函数在 WASM 导出中丢失,将触发异常,库初始化将失败。
在向函数列表数组之一添加新条目时,重要的是所有结果和实参类型都引用 xWrap()
映射,并注意其中一些是该文件中定义的扩展,并且未在 xWrap()
文档中记录(因为它们是扩展,而不是核心定义的转换)。任何拼写错误或未知类型名称都将在库初始化期间导致异常。