建置 JS/WASM

本頁介紹 WASM/JS 程式碼的各種建置方式以及如何建置客製化版本。

sqlite3.jssqlite3.wasm 是「標準建置」。它們是為了廣泛部署在多種瀏覽器上而建置的。

原始碼樹狀目錄中 ext/wasm 目錄下的 GNUmakefile 會建立這些檔案。本文檔的其餘部分將說明建置它們的過程,以便客戶可以建立自訂建置,並將建置調整成適用於 Emscripten 以外的工具鏈。

這些說明假設您使用的是:

前言:目標平台

此專案的主要交付成果是「最大公約數」JavaScript,它可以在各種環境中使用,優先考慮網頁瀏覽器而非伺服器端 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 值,但除非使用 JavaScript 的 BigInt 類型支援建構 sqlite3.js,否則所有此類 API 都將被停用。這是一個建構時選項。

標準建構預設啟用 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`。它被維護為 一個獨立的 side project,但 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 建置不支援動態載入擴充套件,因為基於網頁的 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 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

可以使用 wasi-sdk 建置 sqlite3 以供「伺服器端」使用,但此類建置無法與提供的 JavaScript 程式碼搭配使用,因為 WASI 建置需要我們無法提供(但 Emscripten 可以提供)的 JS 導入。

以下是在 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 建置類似,可以使用 vanilla 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 命名空間中,包含自動的字串類型參數轉換。

注意事項

將新的 API 匯出至 JS

將新的 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 公開給公開介面並新增任何必要的類型轉換,我們必須在內部綁定列表中新增一個項目。

⚠ **注意:** 同樣,以下內容是內部實作細節,隨時可能變更。請勿在用戶端層級程式碼中依賴這些說明。

WASM 匯出函式與其 JS 公開對應項之間的映射儲存在 ext/wasm/api/sqlite3-api-glue.js 中的三個位置之一,具體取決於函式的角色以及函式是否在其參數或結果值中使用 64 位元整數。綁定屬於以下陣列之一:

這些陣列中的每一個都包含子陣列,用於描述函式的參數和返回類型,如同 sqlite3.wasm.xWrap() 的文件中所述。在函式庫初始化期間,會將每個列出函式的類型轉換代理注入到上述命名空間之一。如果 WASM 匯出中缺少任何列出的函式,將會觸發例外狀況,並且函式庫初始化將會失敗。

當向其中一個函式列表陣列新增新項目時,務必確保所有結果和參數類型都參考 xWrap() 的映射,並注意其中一些是於同一個檔案中定義的擴充,且並未在 xWrap() 的文件中記載(因為它們是擴充,而非核心定義的轉換)。任何拼寫錯誤或未知的類型名稱都會在函式庫初始化期間導致例外狀況。