构建 JS/WASM

本页面介绍了 WASM/JS 代码的各种构建以及如何构建自定义版本。

sqlite3.jssqlite3.wasm 是“标准构建”。它们是为在众多浏览器上广泛部署而构建的。

源代码树中 ext/wasm 目录中的 GNUmakefile 创建了这些文件。本文档的其余部分描述了构建它们的步骤,以便客户端可以创建自定义构建并为除 Emscripten 之外的其他工具链调整构建。

这些说明假设

前言:目标平台

该项目的主要交付成果是“最低公分母”JavaScript,它可以在各种环境中使用,其中 Web 浏览器优先于服务器端 JS 引擎。我们不直接支持各种子平台,例如各种基于 node.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 步骤。

有用的目标包括

大量实验表明,-O2 提供最快的二进制文件,但 -Oz 提供最小的二进制文件(注意,需要 wasm-strip 来减小其大小)。-O2-Oz 构建之间的速度差异通常只有大约 10%。

每个构建都会产生许多交付成果,包括

顶级目录中剩余的交付成果是各种演示和测试,这些演示和测试既不必要也不(很可能)对大多数人来说很有趣。

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 模块可能能够也可能不能在运行时增加其内存量。如果不能,并且内存不足,则分配尝试将失败。这是否对 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 是该文件类型的典型扩展名,例如 htmljs

预处理器的指令不会直接干扰源代码,但会导致以下结构,如果在 JS 中运行这些结构而不对它们进行预处理,将导致(充其量)语法错误,或者(最坏的情况)可能导致静默错误。以下是一个预处理块的示例片段

const W =
//#if target=es6-module
  new Worker(new URL(options.proxyUri, import.meta.url));
//#else
  new Worker(options.proxyUri);
//#endif

JS 关键字 importexport 会在非 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 的文件,则适用以下内容

该函数可以安装客户关心的任何扩展,前提是它们可以由 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-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 合并,按组装顺序列出

扩展名为 .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 中。

构建 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) 构建,方法是:

生成的 jswasm/sqlite3.wasm 将包含与 SEE 相关的 API,而 jswasm/sqlite3.[m]js 将将这些 API 添加到 sqlite3.capi 命名空间中,包括自动字符串类型参数转换。

注意事项

向 JS 导出新 API

将新的 API 导出到 WASM 需要做的不仅仅是将它们包含在 sqlite3.c 聚合文件中并启用任何必需的功能标志。简而言之,它需要

  1. 将它们添加到当前构建环境的导出函数列表中。对于基于 Emscripten 的构建,这意味着将它们添加到 ext/wasm/api/EXPORTED_FUNCTIONS.sqlite3-api 中,在每个函数前面加上下划线字符(因为 Emscripten 需要它)。
  2. 如果需要(通常是需要的),将绑定描述添加到 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.xWrap() 中所记录的那样。在库初始化期间,每个列出函数的类型转换代理都会被注入到上面描述的命名空间之一中。如果列出的任何函数在 WASM 导出中丢失,将触发异常,库初始化将失败。

在向函数列表数组之一添加新条目时,重要的是所有结果和实参类型都引用 xWrap() 映射,并注意其中一些是该文件中定义的扩展,并且未在 xWrap() 文档中记录(因为它们是扩展,而不是核心定义的转换)。任何拼写错误或未知类型名称都将在库初始化期间导致异常。