在 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:

除了 sqlite3_index_info 的內部類別(它們都是 sqlite3_index_info 的屬性)之外,所有這些在 JS 中都以名為 sqlite3.capi.TheStructName 的建構函數提供1

實例的(釋放)分配和包裝現有實例

每個 JS 包裝的結構體在此程式庫中有兩種不同的用途:

這兩種用法都相當普遍,它們之間的區別僅在於它們如何管理(或不管理)結構體的記憶體。

只要結構體實例處於活動狀態,它的 pointer 屬性就會解析為其 WASM 堆積區記憶體位址。該值可以傳遞給任何接受該類型指標的 C 函數。例如:

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

當用戶端程式碼使用完一個實例,且沒有 C 層級程式碼正在使用其記憶體時,必須透過呼叫 theStruct.dispose() 來清理該結構實例。多次呼叫 dispose() 並無害處 - 第一次呼叫之後的呼叫都是無操作。對於*包裝*的實例,呼叫 dispose() 並非*嚴格*必要,因為它們的 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 添加到 struct 框架的功能。

安裝 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 個參數呼叫,它沒有任何副作用,但會返回一個與上述描述具有相同簽章的函數。

⚠**注意:**⚠ 由於我們無法通用地知道如何將 JS 例外轉換為結果代碼,因此已安裝的函數不會自動攔截例外。為了避免 C 層中的未定義行為,透過此函數對應的方法*不得*拋出例外,這一點至關重要。這個規則的例外是……

如果 applyArgcCheck 為 true,則每個 JS 函數(與函數指標相對)都會被包裝在一個代理中,該代理會斷言傳遞給它的參數數量與預期相符,如果參數數量不符則拋出例外。*這僅用於開發時的健全性檢查*,因為例外傳遞通過此類方法會使 C 環境處於未定義狀態。

installMethods()

structBinderInstance installMethods(methods, applyArgcCheck = false)

將方法安裝到此 StructType 類型的實例中。指定 methods 物件中的每個項目都必須對應到指定 StructType 的已知成員,否則會觸發例外。有關詳細資訊,包括第二個參數的語義,請參閱 installMethod()

作為上述的例外,如果 methods 物件中的任何兩個或多個方法是完全相同的函數,則*不會*為第二個和後續實例呼叫 installMethod(),而是將這些實例分配給為第一個實例建立的相同方法指標。此優化主要是為了適應 sqlite3_module::xConnectxCreate 方法的特殊處理。

成功時,返回此物件。出錯時拋出例外。


  1. ^ 在 C 語言中,這些結構是在 sqlite3_index_info 中內聯定義的,因此將它們作為該 JS 類別的成員屬性似乎是合適的。
  2. ^ 注意,如果需要,*可以*使用ondispose 機制有效地將記憶體傳輸到 JS,但在實務中,這從來沒有必要。
  3. ^ 須注意,用戶端程式碼可以自由新增帶有 `$` 前綴的純 JavaScript 屬性,但這樣做可能會造成日後程式碼維護上的混淆。