sqlite3.wasm
命名空間1(在本頁其餘部分簡稱為 wasm
)包含許多用於處理 WASM 端構造的例程。它們包含用於以下任務的 API:
- 記憶體管理。
- 配置和釋放記憶體。
- 用於處理 WASM 堆積記憶體的輔助工具,例如從 WASM 堆積取得和設定原始值。
- WASM 匯出函式的可配置結果值和參數類型轉換。
- JS/C 字串轉換。
- 將 JS 函式綁定到 WASM 執行階段,以便可以從 WASM 程式碼(即從 C)呼叫它們。
簡而言之,如果在開發 sqlite3 JS API 期間需要特定 WASM 功能,則已將其添加到此命名空間。在大多數情況下,高階用戶端程式碼很少需要使用其中多個功能,而使用 C 風格 API 的用戶端可能會大量使用它們。
sqlite3.wasm.exports
命名空間
sqlite3.wasm.exports
命名空間物件是 WASM 模組檔案的 WASM 標準部分,包含所有內建於 WASM 模組的「匯出」C 函式,以及屬於 WASM 模組的某些非函式值。位於此物件中的函式就 JS/C 綁定而言,層級最低。它們不會對其參數或結果值執行任何自動類型轉換,而且由於這個原因,許多(甚至可能是大多數)函式從 JS 使用起來很麻煩。一般不建議用戶端使用此 API 層級,但可供想要使用它的用戶端使用。此物件中供用戶端使用的函式會重新匯出到 sqlite3.capi
命名空間,並對其應用自動類型轉換(如果適用)。少數函式會重新匯出到 sqlite3.wasm
命名空間。
exports
中屬於此專案 API 的唯一符號是
- 名為
sqlite3_...()
的函式,但以下例外:sqlite3
後面有兩個底線的所有函式,例如sqlite3__wasm_...()
,都是內部使用 API,隨時可能更改或移除。- 名為
sqlite3_wasm_...()
的函式不是用戶端 API 的一部分,除非它們重新匯出到sqlite3.wasm
命名空間。其餘部分供 JS 綁定內部使用,並且沒有穩定的 API。同樣地… - 名為
sqlite3_wasm_test_...()
的函式僅供此專案自己的測試使用,並且可以從任何給定版本中省略。
- 記憶體配置函式:語義上是
free()
和malloc()
,但標準版本使用sqlite3_free()
和sqlite3_malloc()
。這些函式會以如下所述的方式公開給用戶端,但如果用戶端需要在記憶體不足的情況下不擲回例外的變體,則 C 層級形式的sqlite3_malloc()
很有用。 - WASM 記憶體物件(可能是可選的,取決於建置選項):
memory
__indirect_function_table
是匯出函式表的事實上標準名稱。此 API 通過sqlite3.wasm.functionTable()
公開它。
建置過程會在 exports
命名空間中包含其他函式和物件,這些函式和物件並非此專案公開介面的一部分,用戶端程式碼不應使用它們。它們在 WASM 檔案的任何給定建置中都可能不同,並且在不同建置環境中肯定會有所不同。
記憶體管理
就像在 C 語言中一樣,WASM 提供了一個記憶體「堆積」,在 JS 和 WASM 之間傳輸值通常需要操作該記憶體,包括低階的記憶體配置和釋放。以下各小節描述各種記憶體管理 API。
低階管理
最低階的記憶體管理類似於 C 語言標準的 malloc()
、realloc()
和 free()
,唯一的區別是使用例外狀況來報告記憶體不足的情況。為了避免因混合使用不同的配置器而導致某些 API 誤用,標準的 sqlite3.js
建置封裝了 sqlite3_malloc()
、sqlite3_realloc()
和 sqlite3_free()
,而不是 malloc()
、realloc()
和 free()
,但這兩對函式的語義實際上是相同的。
按字母順序列出...
alloc()
指標 alloc(n)
指標 alloc.impl(n)
(不拋出例外)
從 WASM 堆積配置 n
個位元組的記憶體,並返回區塊中第一個位元組的位址。如果配置失敗,alloc()
會拋出 WasmAllocError
例外。如果需要不拋出例外的配置,請使用 alloc.impl(n)
,如果配置失敗,它會返回一個 WASM 空指標(整數 0)。
請注意,以這種方式配置的記憶體不會自動歸零。在實務中,這並未被證明是一個問題(至少在 JS 中是如此),因為只有在有特定用途且將由配置它的程式碼填充時,才會明確配置記憶體。
allocCString()
指標 allocCString(jsString, returnWithLength=false)
使用 alloc()
配置足夠的記憶體來儲存給定 JS 字串的位元組長度,再加上 1(用於 NUL 終止符),使用 jstrcpy()
將給定的 JS 字串複製到該記憶體,以 NUL 終止它,並返回指向該 C 字串的指標。指標的所有權會轉移給呼叫者,呼叫者最終必須將指標傳遞給 dealloc()
以釋放它。
如果傳遞一個真值作為第二個參數,則其返回語義會改變:它返回 [ptr,n]
,其中 ptr
是 C 字串的指標,n
是其 cstrlen()
。
allocMainArgv()
指標 allocMainArgv(list)
使用 alloc()
建立一個 C 樣式的陣列,適合傳遞給 C 層級的 main()
常式。輸入是一個具有 length
屬性和 forEach()
方法的集合。會配置一個長度為 list.length
個項目的記憶體區塊,並且該記憶體的每個指標大小的區塊都會使用每個元素的 (''+value)
的 allocCString()
轉換來填充。返回指向列表開頭的指標,適合作為 C 樣式 main()
函式的第二個參數傳遞。
如果 list.length
為假值,則會拋出例外。
請注意,傳回值難以釋放,但它旨在與呼叫 C 層級的 main()
函式一起使用,其中字串必須與應用程式一樣長。請參閱 scopedAllocMainArgv()
以了解易於釋放的變體。
allocPtr()
指標 allocPtr(howMany=1, safePtrSize=true)
配置一個或多個指標作為單個記憶體區塊,並將它們歸零。
第一個參數是要配置的指標數量。第二個參數指定是否應使用「安全」指標大小(8 位元組),或者是否可以使用預設指標大小(通常為 4 位元組,但也可能是 8 位元組)。
回傳結果的方式取決於它的第一個引數:如果傳入 1,它會回傳配置的記憶體位址。如果傳入多個引數,則會回傳一個指標位址的陣列,可以選擇性地與「解構賦值」一起使用,如下所示
const [p1, p2, p3] = allocPtr(3);
注意:釋放記憶體時,只能將*第一個*結果值傳遞給 dealloc()
。其他值是同一個記憶體區塊的一部分,不得單獨釋放。
第二個引數的原因是……
當其中一個返回的指標指向 64 位元值(例如 double 或 int64)並且必須寫入或讀取該值(例如使用 poke()
或 peek()
)時,重要的是,相關指標必須對齊到 8 位元組邊界,否則將無法正確讀取或寫入,並且會損壞或讀取相鄰的記憶體。只有當用戶端程式碼確定它只會取得/讀取 4 位元組(或更小)的值時,才能安全地傳遞 false。
dealloc()
void dealloc(pointer)
釋放 alloc()
返回的記憶體。如果傳遞的值不是 alloc()
返回的值或 null
/undefined
/0
(這些都是無效操作),則結果未定義。
alloc()
未命名為 malloc()
的原因相同。realloc()
pointer realloc(ptr,size)
pointer realloc.impl(ptr,size)
(非拋擲)
此函式在語義上等同於 realloc(3)
或 sqlite3_realloc()
,它重新配置透過此函式或 alloc()
配置的記憶體。它的第一個引數是 0 或此函式或 alloc()
返回的指標。它的第二個引數是要(重新)配置的位元組數,或 0 以釋放第一個引數中指定的記憶體。發生配置錯誤時,realloc()
會拋出 WasmAllocError
,而 realloc.impl()
將在配置錯誤時返回 0。
請注意,重新賦值 realloc.impl()
的返回值是不良做法,並且可能導致堆記憶體洩漏
let m = wasm.realloc(0, 10); // allocate 10 bytes
m = wasm.realloc.impl(m, 20); // grow m to 20 bytes
如果重新配置失敗,它將返回 0,覆蓋 m
並有效地洩漏第一個配置。
sizeofIR()
int sizeofIR(string)
對於集合 ('i8'
、'i16'
、'i32'
、'f32'
、'float'
、'i64'
、'f64'
、'double'
、'*'
) 中的給定 IR 類似字串,或任何以 '*'
結尾的字串值,返回該值的 sizeof(在後一種情況下為 wasm.ptrSizeof
)。對於任何其他值,它返回 undefined
值。
一些配置函式使用此功能讓呼叫者傳遞 IR 值而不是整數。
「作用域」配置管理
通常以這樣一種方式管理配置會很方便:在特定區塊退出時「自動」清除在該區塊中進行的所有配置。此 API 提供了以這種方式運作的「作用域」配置函式。
以下列出它們的典型使用順序……
scopedAllocPush()
opaque scopedAllocPush()
開啟一個新的配置「作用域」。透過 scopedAllocXyz()
API 進行的所有配置都會將其結果儲存在目前的(最近推送的)配置作用域中,以便稍後清除。必須保留返回值以傳遞給 scopedAllocPop()
。
可以同時啟用任意數量的作用域,但必須以其建立的相反順序彈出它們。也就是說,它們的嵌套方式必須與 C 風格的作用域等效。
警告
- 如果沒有作用域處於活動狀態,則*所有*其他
scopedAllocXyz()
函式都會拋出錯誤。 - 將作用域配置的結果傳遞給
dealloc()
*永遠*是非法的,這樣做會在使用scopedAllocPop()
關閉作用域時導致重複釋放。
此函式及其相關函式只有一種預期使用模式
const scope = wasm.scopedAllocPush();
try {
... use scopedAllocXyz() routines ...
// It is perfectly legal to use non-scoped allocations here,
// they just won't be cleaned up when...
}finally{
wasm.scopedAllocPop(scope);
}
scopedAlloc()
pointer scopedAlloc(n)
與 alloc(n)
的工作方式相同,但將配置的結果儲存在目前的作用域中。
此函式的唯讀 level
屬性會解析為目前的配置作用域深度。
scopedAllocMainArgv()
指標 scopedAllocMainArgv(陣列)
此函式的功能與 allocMainArgv()
完全相同,但其作用域限於目前的分配作用域,並且其內容將在目前的分配作用域被彈出時釋放。
scopedAllocCall()
任意型別 scopedAllocCall(回呼函式)
呼叫 scopedAllocPush()
,呼叫給定的回呼函式,然後呼叫 scopedAllocPop()
,傳播來自回呼函式的任何例外,或返回其結果。這基本上是以下程式碼的簡便形式:
const scope = wasm.scopedAllocPush();
try { return callback() }
finally{ wasm.scopedAllocPop(scope) }
scopedAllocCString()
指標 scopedAllocCString(js字串, returnWithLength=false)
功能與 allocCString()
相同,但將分配的結果儲存在目前的範圍內。
scopedAllocMainArgv()
指標 scopedAllocMainArgv(列表)
功能與 allocMainArgv()
相同,但將各種分配儲存在目前的範圍內。
scopedAllocPtr()
指標 scopedAllocPtr(數量=1, 安全指標大小=true)
功能與 allocPtr()
相同,但將分配的結果儲存在目前的範圍內。
scopedAllocPop()
void scopedAllocPush(不透明)
給定從 scopedAllocPush()
返回的值,此函式會「彈出」該分配作用域,並釋放該作用域中由 scopedAllocXyz()
系列 API 分配的所有記憶體。
技術上來說,在沒有任何參數的情況下呼叫此函式是合法的,但傳遞參數允許分配器執行完整性檢查,以確保作用域以正確的順序推送和彈出(如果順序不正確,則會拋出例外)。不傳遞參數並非非法,但會使該完整性檢查無法進行。
趣聞:在美國某些地區,此函式可能更廣為人知的名稱為
scopedAllocSoda()
或scopedAllocCola()
。
「PStack」配置
「pstack」(偽堆疊)API 是一種特殊用途的分配器,僅用於分配少量記憶體,例如 輸出指標 所需的記憶體。它比 作用域分配 API 更有效率,並且涵蓋了該 API 的許多使用案例,但它具有微小的靜態記憶體限制(未指定的總大小不少於 4kb)。
pstack API 的典型用法如下:
const pstack = sqlite3.wasm.pstack;
const stackPtr = pstack.pointer;
try {
const ptr = pstack.alloc(8);
// ==> pstack.pointer === ptr
const otherPtr = pstack.alloc(8);
// ==> pstack.pointer === otherPtr
...
}finally{
pstack.restore(stackPtr);
// ==> pstack.pointer === stackPtr
}
pstack 方法和屬性按字母順序列示如下。
alloc()
指標 alloc(n)
嘗試從 pstack 分配給定數量的位元組。成功時,它會將給定大小的記憶體區塊歸零,調整 pstack 指標,並返回指向該記憶體的指標。發生錯誤時,會拋出 WasmAllocError
例外。記憶體最終必須使用 pstack.restore()
釋放。
n
可以是 wasm.sizeofIR()
接受的字串,任何該函式不接受的字串值都會觸發 WasmAllocError
例外。
此方法始終將給定值調整為 8 位元組的倍數,因為不這樣做可能會導致從 WASM 堆讀取和寫入 64 位元值時出現錯誤結果。同樣地,返回的位址始終是 8 位元組對齊的。
allocChunks()
陣列 allocChunks(n, sz)
將 alloc()
的 n
個區塊(每個區塊 sz
位元組)分配為單個記憶體區塊,並將位址作為包含 n
個元素的陣列返回,每個元素都保存一個區塊的位址。
sz
參數可以是 wasm.sizeofIR()
接受的字串值,任何該函式不接受的字串值都會觸發 WasmAllocError
例外。
如果分配失敗,則拋出 WasmAllocError
例外。
範例
const [p1, p2, p3] = pstack.allocChunks(3,4);
allocPtr()
混合型別 allocPtr(n=1, 安全指標大小=true)
allocChunks()
的便利包裝函式,它將每個區塊的大小設定為 8 位元組(安全指標大小
為真值)或 wasm.ptrSizeof
(如果 安全指標大小
為假值)。
其返回結果的方式取決於其第一個參數:如果為 1,則返回單個指標值。如果大於 1,則返回與 allocChunks()
相同的結果。
當任何返回的指標指向 64 位元值(例如 double 或 int64),且該值必須被寫入或讀取(例如使用 wasm.poke()
或 wasm.peek()
)時,重要的是該指標必須對齊到 8 位元組邊界,否則將無法正確讀取或寫入,並會損壞或讀取相鄰的記憶體。
然而,當所有涉及的指標都指向「小型」資料時,傳遞一個假值以節省少量記憶體是安全的。
指標 (pointer)
此屬性解析為目前的 pstack 位置指標。此值*僅*用於儲存以傳遞給 restore()
。在未先透過 pstack.alloc()
(或等效方法)保留記憶體的情況下寫入此記憶體將導致未定義的結果。
配額 (quota)
此屬性解析為 pstack 中可用的總位元組數,包括任何目前已配置的空間。此值是一個編譯時期常數。
剩餘 (remaining)
此屬性解析為 pstack 中剩餘的空間量。
restore()
void restore(pstackPtr)
將目前的 pstack 位置設定為給定的指標。如果傳入的值並非來自 pstack.pointer
,或者在此呼叫後使用了在給定指標之前的空間中配置的記憶體,則結果將未定義。
取得/設定記憶體值
WASM 記憶體堆積以位元組陣列的形式暴露給 JS,使其看起來是連續的(雖然實際上它是以區塊方式配置的)。透過堆積的位元組導向視圖,可以讀取和寫入堆積的個別位元組,就像在 C 語言中一樣。
const X = wasm.heap8u(); // a uint8-oriented view of the heap
X[someAddress] = 0x2a;
console.log( X[someAddress] ); // ==> 42
顯然,寫入任意位址可能會損壞 WASM 堆積,就像在 C 語言中一樣,因此必須小心處理記憶體位址(就像在 C 語言中一樣!)。
提示:切勿長期持有從
heap8u()
等方法返回的物件,因為如果堆積增長,它們可能會失效。在短時間的一系列呼叫中持有參考是可以接受的,只要這些呼叫保證不會在 WASM 堆積上配置記憶體,但不應將其快取以供稍後使用。
在描述操作堆積的例程之前,我們首先需要了解資料類型描述符,有時稱為「IR」(內部表示法)。這些是識別 WASM 和/或 JS/WASM 膠合程式碼所支援的特定資料類型的簡短字串。
i8
:8 位元有號整數i16
:16 位元有號整數i32
:32 位元有號整數。別名:int
、*
、**
(請注意,當 WASM 環境獲得 64 位元指標功能時,*
和**
可能會動態重新對應到i64
)。i64
:64 位元有號整數。使用此 API 需要應用程式已使用 BigInt 支援建置,否則將會拋出錯誤。f32
:32 位元浮點值。別名:float
f64
:64 位元浮點值。別名:double
記憶體存取器 API 廣泛使用這些類型,需要牢記。
待辦事項:說明堆積中值的對齊方式如何影響它們的存取方式。實際上,這通常不是問題,除非自行以區塊方式配置記憶體並將其劃分為子區塊。簡而言之:讀取或寫入給定大小的值時,*通常*必須在堆積位址上進行,該位址恰好是該大小的偶數倍。
以下例程可用於以各種方式存取記憶體位址...
peek()
及其變體
number peek(address [,representation='i8'])
array peek(array-of-addresses [,representation='i8'])
第一種形式從記憶體中讀取單個值。第二種形式從給定陣列中的每個指標讀取值,並返回值陣列。用於讀取記憶體的堆積視圖由第二個參數指定,預設為位元組導向視圖。
如果第二個參數以 "*"
結尾,則一律使用指標大小的表示法(目前一律為 32 位元)。
範例
let i32 = wasm.peek(myPtr, 'i32');
peek()
提供了幾個便捷的形式,它們只是將特定第二個參數轉發給 peek()
。
peekPtr()
(已棄用的別名:getPtrValue()
):等同於peek(X,'*')
。最常用於擷取輸出指標值。peek8()
:等同於peek(X,'i8')
peek16()
:等同於peek(X,'i16')
peek32()
:等同於peek(X,'i32')
peek64()
:等同於peek(X,'i64')
。如果環境未配置 BigInt 支援,則會拋出錯誤。peek32f()
:等同於peek(X,'f32')
peek64f()
:等同於peek(X,'f64')
heapForSize()
及其相關函式
TypedArray heapForSize(n [,unsigned=true])
n 的值必須為以下其中之一:
- 整數 8、16 或 32。
- 整數型 TypedArray 建構函式:Int8Array、Int16Array、Int32Array 或其對應的 Uint 版本。
如果啟用了BigInt 支援,它也接受值 64 或 BigInt64Array/BigUint64Array,否則如果傳入 64 或這些建構函式之一,則會拋出錯誤。
傳回與給定區塊大小相關聯的 WASM 堆積記憶體緩衝區的基於整數的 TypedArray 視圖。如果第一個參數傳入整數且 unsigned 為真值,則傳回該視圖的「U」(無符號)變體,否則傳回帶符號變體。如果傳入 TypedArray 值,則忽略第二個參數。請注意,此函式不支援 Float32Array 和 Float64Array 視圖。
請注意,堆積的增長可能會使對此堆積的任何參考失效,因此請勿長時間持有參考,並且請勿在任何可能配置的作業後使用參考。而是透過再次呼叫此函式重新擷取參考,如果需要,該函式會自動重新整理視圖。
如果傳入無效的 n
,則會拋出錯誤。
用戶端程式碼很少使用此函式。實際上,會使用其中一個(更快的)便捷形式
heap8()
→ Int8Arrayheap8u()
→ Uint8Arrayheap16()
→ Int16Arrayheap16u()
→ Uint16Arrayheap32()
→ Int32Arrayheap32u()
→ UInt32Array
poke()
object poke(address, number [,representation='i8'])
object poke(array-of-addresses, number [,representation='i8']
擷取給定表示法的 heapForSize()
,然後將給定的數值寫入其中。*僅*可以透過這種方式寫入數字,傳入非數字*可能*會觸發例外狀況。如果傳入指標陣列,它會將給定值寫入所有指標。
傳回 this
。
poke()
存在幾個便捷形式,它們只是將特定第三個參數轉發給該方法。
pokePtr()
(已棄用的別名:setPtrValue()
):等同於poke(X,Y,'*')
。最常用於清除輸出指標值。poke8()
:等同於poke(X,Y,'i8')
poke16()
:等同於poke(X,Y,'i16')
poke32()
:等同於poke(X,Y,'i32')
poke64()
:等同於poke(X,Y,'i64')
。如果此環境未配置 BigInt 支援,則會拋出錯誤。poke32f()
:等同於poke(X,Y,'f32')
poke64f()
:等同於poke(X,Y,'f64')
字串轉換和工具
經常需要將字串傳入和傳出 WASM,但 JS 和 C 程式碼表示字串的方式差異很大。以下例程可用於字串轉換和相關演算法。
以下按字母順序列出...
cArgvToJs()
array cArgvToJs(int argc, 指向指標 pArgv)
預期傳入一個 C 語言風格的字串陣列及其長度。它會回傳一個包含字串和/或 null
值的 JS 陣列:pArgv
陣列中任何為 NULL 的項目都會在結果陣列中產生一個 null
項目。如果 argc
為 0,則回傳一個空陣列。
如果 pArgv
的前 argc
個項目中任何一個既不是 0 (NULL) 也不是合法的 UTF 格式 C 字串,則結果未定義。
更明確地說,預期傳遞給此函式的 C 語言風格引數為 (int, char **)
(可選擇加上 const 限定詞)。
cstrToJs()
string cstrToJs(ptr)
預期其引數是指向 WASM 堆積記憶體的指標,該指標指向以 UTF-8 編碼的 NUL 終止 C 語言風格字串。此函式使用 cstrlen()
計算其位元組長度,然後回傳一個代表其內容的 JS 格式字串。作為一種特殊情況,如果引數為假值,則回傳 null
。
cstrlen()
int cstrlen(ptr)
預期其引數是指向 WASM 堆積記憶體的指標,該指標指向以 UTF-8 編碼的 NUL 終止 C 語言風格字串。回傳字串的長度(以位元組為單位),如同 strlen(3)
。作為一種特殊情況,如果引數為假值,則回傳 null
。如果引數超出 wasm.heap8u()
的範圍,則會拋出例外。
cstrncpy()
int cstrncpy(tgtPtr, srcPtr, n)
運作方式類似於 C 語言的 strncpy(3)
,最多從 srcPtr
複製 n
個位元組(不是字元)到 tgtPtr
。它會持續複製,直到已複製 n
個位元組或在 src 中遇到 0 位元組為止。*與* strncpy()
*不同*,它會回傳在 tgtPtr
中指派的位元組數,*包括* NUL 位元組(如果有的話)。如果在 srcPtr
中遇到 NUL 位元組之前達到 n
,則 tgtPtr
*不會* 以 NUL 終止。如果在複製 n
個位元組之前遇到 NUL 位元組,則 tgtPtr
將以 NUL 終止。
如果 n
為負數,則使用 cstrlen(srcPtr)+1
計算它,+1 是用於 NUL 位元組。
如果 tgtPtr
或 srcPtr
為假值,則會拋出例外。如果
- 兩者都不是指向 WASM 堆積的指標,或
srcPtr
不是以 NUL 終止 *且*n
小於srcPtr
的邏輯長度,則結果未定義。
注意:當傳入非負的 n
值時,可能會以這種方式複製部分多位元組字元,並且將此類字串轉換回 JS 字串的結果將未定義。
jstrcpy()
int jstrcpy(jsString, TypedArray tgt, offset = 0, maxBytes = -1, addNul = true)
預先警告:此 API 有點複雜,實際上客戶端程式碼從不需要它。
將給定的 JS 字串編碼為 UTF-8 到給定的 TypedArray tgt
中(必須是 Int8Array 或 Uint8Array),從給定的偏移量開始,最多寫入 maxBytes 個位元組(如果 addNul
為 true,則包括 NUL 終止符;否則不添加 NUL)。如果它寫入了任何位元組且 addNul
為 true,它總是會以 NUL 終止輸出,即使這樣做意味著只寫入 NUL 位元組。
如果 maxBytes
為負數(預設值),則將其視為 tgt
的剩餘長度,從給定的偏移量開始。
如果寫入最後一個字元會因為該字元是多位元組而超過 maxBytes
計數,則不會寫入該字元(而不是寫入截斷的多位元組字元)。這可能導致寫入的位元組數比 maxBytes
指定的少 3 個。
回傳寫入目標的位元組數,*包括* NUL 終止符(如果有的話)。如果回傳 0,則表示完全沒有寫入任何內容,這可能發生在以下情況:
jsString
為空且addNul
為 false。offset(偏移量)
< 0.maxBytes(最大位元組數)
=== 0.- 若 `maxBytes` 小於多位元組字元 `jsString[0]` 的位元組長度。
如果 `tgt` 不是 Int8Array 或 Uint8Array,則會拋出例外。
在 C 語言的 `strcpy()` 中,目標指標是第一個參數。這裡並非如此,主要原因是第三個及後續的參數都參考目標,所以將它們與目標指標放在一起似乎更合理。
Emscripten 中對應此函式的 `stringToUTF8Array()` 會傳回寫入的位元組數,不包含 NUL 終止符。然而,這會造成混淆:當 str.length===0 或 maxBytes===(0 或 1) 時,都會傳回 0。
jstrlen()
int jstrlen(jsString)
給定一個 JS 字串,此函式會傳回其 UTF-8 編碼的位元組長度。如果參數不是字串,則傳回 `null`。這是一個相對耗時的計算,非必要時應避免使用。
jstrToUintArray()
Uint8Array jstrToUintArray(jsString, addNul=false)
對於給定的 JS 字串,傳回一個以 UTF-8 編碼其內容的 `Uint8Array`。如果 `addNul` 為 true,則傳回的陣列將包含一個尾隨的 0 元素,否則不會。
補充:這是在此程式碼作者知道 JS 的 `TextEncoder` 之前編寫的。可以使用 `new TextEncoder().encode(str)` 達成相同的功能,但不包含尾隨 NUL 選項。
其他配置例程
allocFromByteArray()
pointer allocFromByteArray(srcTypedArray)
配置 `wasm.alloc()` 的 `srcTypedArray.byteLength` 個位元組,將來源 TypedArray 的值填入其中,並傳回指向該記憶體的指標。傳回的指標最終必須傳遞給 `wasm.dealloc()` 進行清理。
參數可以是 Uint8Array、Int8Array 或 ArrayBuffer,如果傳遞任何其他類型,則會拋出例外。
作為一種特殊情況,為了避免在此常式使用時出現進一步的特殊情況,如果 `srcTypedArray.byteLength` 為 0,它會配置一個位元組並將其設定為值 0。即使在這種情況下,呼叫也必須表現得如同已配置的記憶體具有 `srcTypedArray.byteLength` 個可用位元組。
橋接 JS/WASM 函式
本節說明與銜接 JavaScript 和 WebAssembly 函式相關的輔助 API。
WASM 模組會將所有匯出的函式公開給使用者,但它們是「原始」形式。也就是說,它們不執行任何參數或結果類型轉換,並且僅支援 WASM 支援的資料類型(即僅數字類型)。對於僅接受和傳回數字的函式來說,這沒問題,但對於接受或傳回字串或具有輸出指標的函式來說,通常幫助不大。基於易用性考量,最好能自動執行一些繁瑣的任務,例如在 JS 和 WASM 之間轉換字串所需的記憶體配置和釋放,以減少 JS/C 之間的摩擦。
此外,通常需要從 JS 向 WASM 執行階段新增新的函式,這需要即時編譯二進位制 WASM 程式碼。一個常見的例子是建立使用者定義的 SQL 函式。在大多數情況下,sqlite3 API 的 JS 繫結會為使用者處理此類轉換,但在某些情況下,用戶端程式碼需要或想要自行執行此類轉換。
WASM 函式表
WASM 匯出的函式,以及在執行階段繫結到 WASM 的 JavaScript 函式,都會透過 WebAssembly.Table 執行個體公開給用戶端。以下 API 可用於處理該執行個體。
functionEntry()
mixed functionEntry(ptr)
給定一個函式指標,如果找到則傳回 WASM 函式表格項目,否則傳回一個假值。
functionTable()
WebAssembly.Table functionTable()
傳回 WASM 模組的間接函式表格。
呼叫和包裝函式
xCall()
any xCall(functionName, ...args)
any xCall(functionName, [args...])
透過名稱呼叫 WASM 匯出的函式,傳遞所有提供的參數(可以選擇以陣列形式提供)。如果未匯出函式或參數計數不符,則會拋出例外。此常式*不*執行類型轉換,基本上等同於
const rc = wasm.exports.some_func(...args)
除了 xCall()
在參數數量與 WASM 匯出函式的參數數量不符時會拋出錯誤之外。
xCallWrapped()
any xCallWrapped(functionName, resultType, argTypes, ...args)
any xCallWrapped(functionName, resultType, argTypes, [args array...])
功能類似 xCall()
,但會像 xWrap()
一樣執行參數和結果類型的轉換。
第一個參數是要呼叫的匯出函式名稱。第二個參數是其結果類型名稱,如同 xWrap()
的說明文件所述。第三個參數是一個參數類型名稱的陣列,如同 xWrap()
的說明文件所述。第四個及之後的參數是呼叫的參數,特例是如果第四個參數是一個陣列,則它將被用作呼叫的參數。
返回轉換後的呼叫結果。
這只是 xWrap()
的一個輕量級包裝器。如果要多次呼叫給定的函式,使用 xWrap()
建立一個包裝器,然後根據需要多次呼叫該包裝器會更有效率。然而,對於一次性呼叫,這個變體可以說更有效率,因為它理論上可以快速釋放包裝器函式。
xGet()
Function xGet(functionName)
透過名稱返回一個 WASM 匯出的函式,如果找不到該函式,則拋出錯誤。
xWrap()
Function xWrap(functionName, resultType=undefined, ...argTypes)
Function xWrap(functionName, resultType=undefined, [argTypes...])
xWrap()
會建立一個 JS 函式,用於呼叫 WASM 匯出的函式,如同 xCall()
的說明。
為 WASM 匯出的函式 fname 建立一個包裝器。它使用 xGet()
來取得匯出的函式(如果發生錯誤則會拋出錯誤),並返回該函式或該函式的包裝器,該包裝器會將 JS 端的參數類型轉換為 WASM 端的類型,並轉換結果類型。如果該函式不接受任何參數且 resultType 為 null
,則按原樣返回該函式,否則會為其建立一個包裝器以調整其參數和結果值,如下所述。
此函式的參數為
functionName
:匯出函式的名稱。使用xGet()
來取得此函式,因此如果找不到具有該名稱的匯出函式,則會拋出錯誤。resultType
:結果類型的名稱。字面值null
表示按原樣返回原始函式的值(記憶法:沒有進行任何「null」轉換)。字面值undefined
或字串"void"
表示忽略函式的結果並返回undefined
。除了這兩個特殊情況外,它可以是以下描述的值之一,或任何由用戶端使用xWrap.resultAdapter()
安裝的映射。
如果傳遞 3 個參數且最後一個參數是陣列,則該陣列必須包含類型名稱列表(如下所示),以便將參數從 JS 調整為 WASM。如果傳遞 2 個參數、超過 3 個參數或第 3 個參數不是陣列,則第 2 個之後的所有參數(如果有的話)都將被視為類型名稱。換句話說,以下用法是等價的
xWrap('funcname', 'i32', 'string', 'f64');
xWrap('funcname', 'i32', ['string', 'f64']);
以下用法也是等價的
xWrap('funcname', 'i32'); // no arguments
xWrap('funcname', 'i32', []);
類型名稱是符號名稱,用於將函式的結果和參數映射到一個適配器函式,以便在需要時在將值傳遞給 WASM 之前或從 WASM 轉換返回結果時進行轉換。內建名稱列表如下。以下列表描述了每個名稱,請注意,有些名稱僅適用於參數*或*返回結果,兩者通常具有不同的語義
i8
、i16
、i32
(參數和結果):所有整數轉換都會將其參數轉換為整數並將其截斷為給定的位元長度。N*
(參數):格式為N*
的類型名稱,其中 N 是數值類型名稱,其處理方式與 WASM 指標相同。*
和pointer
(參數):被視為不透明的 WASM 指標,並像目前的 WASM 指標數值類型一樣處理。非數字值將強制轉換為 0,超出範圍的數字將會產生未定義的結果(如同任何指標誤用)。*
和pointer
(回傳值):是目前 WASM 指標數值類型的別名。**
(參數):僅是'*'
的描述性別名。它主要用於標記輸出指標參數。i64
(參數和回傳值):將值傳遞給BigInt()
以轉換為 int64。僅在啟用 BigInt 支援時可用。f32
(float
)、f64
(double
) (參數和回傳值):將其參數傳遞給Number()
。也就是說,轉接器目前不區分兩種浮點數類型。number
(回傳值):使用Number(theValue).valueOf()
將結果轉換為 JS Number。請注意,這僅適用於結果轉換,因為無法泛型地知道要將參數轉換為哪種類型的數字。
非數值轉換包含:
string
或utf8
(參數):為了適應某些 C API 的各種用法,具有兩種不同的語義...- 如果參數是 JS 字串,則會建立一個 *暫時的*、UTF-8 編碼的 C 字串傳遞給匯出的函式,該字串會在包裝函式返回之前清除。如果需要一個長效的 C 字串指標,則需要用戶端程式碼來建立字串,然後將其指標傳遞給函式。
- 否則,參數會被假定為指向用戶端已分配的字串的指標,並作為 WASM 指標傳遞。
string
或utf8
(回傳值):將回傳值視為以 UTF-8 編碼的 const C 字串,將其複製到 JS 字串,並返回該 JS 字串。string:dealloc
或utf8:dealloc
(回傳值):將回傳值視為以 UTF-8 編碼的非 const C 字串,其所有權已轉移給呼叫者。它會將 C 字串複製到 JS 字串,使用dealloc()
釋放 C 字串,然後返回 JS 字串。如果此類回傳值為 NULL,則 JS 結果為null
。
注意:當使用從特定分配器返回結果的 API 時,此轉換 *無效*。此時需要使用相應的釋放器的等效轉換。下一節將提供此類範例。string:flexible
(參數):是 C 風格 API 文件 中描述的string
的擴展版本。這些廣泛用於程式庫中的 SQL 字串輸入。string:static
(參數):如果傳遞指標,則按原樣返回。其他任何內容:將強制轉換為 JS 字串以用作映射鍵。如果找到匹配的項目(如下所述),則返回它,否則使用wasm.allocCString()
建立一個新字串,將其指標映射到 (''+v
) 以供應用程式在其剩餘生命週期中使用,並返回該指標值以供本次呼叫和所有未來傳遞字串等效參數的呼叫使用。此轉換適用於需要靜態/長效字串參數的情況,例如sqlite3_bind_pointer()
和sqlite3_result_pointer()
。json
(回傳值):將結果視為 const C 字串,並返回將轉換為 JS 字串的結果傳遞給JSON.parse()
的結果。如果 C 字串是 NULL 指標,則返回null
。傳播來自JSON.parse()
的任何例外。json:dealloc
(回傳值):與string:dealloc
的工作方式完全相同,但返回與json
轉接器相同的內容。請注意string:dealloc
中關於分配器和釋放器的警告。
在呼叫 xWrap()
時會驗證結果和參數的類型名稱,任何未知的名稱都會觸發例外。
用戶可以使用 xWrap.resultAdapter()
和 xWrap.argAdaptor()
來映射他們自己的結果和參數適配器,需要注意的是,並非所有類型轉換都對參數 *和* 結果類型有效,因為它們通常具有不同的內存所有權要求。該主題將在下一節中討論...
參數和結果值類型轉換
另請參閱:api-c-style.md#type-conversion
當 xWrap()
被調用並評估函數調用簽章時,它會查找匹配的參數和結果類型適配器。可以使用下面列出的方法為參數和結果值安裝自定義適配器。
xWrap()
有兩個簽章相同的方法
xWrap.argAdapter(字串, 函數)
xWrap.resultAdapter(字串, 函數)
每個方法都需要一個類型名稱字串,例如 xWrap()
中描述的那些,以及一個函數,該函數傳入單個值,並且必須返回該值、該值的轉換值或拋出異常。這些函數中的每一個都返回自身,以便可以鏈式調用。
舉例來說,假設我們有一個 C 語言綁定函數,它返回一個使用非預設分配器 my_str_alloc()
分配的 C 風格字串。返回的內存由調用者擁有,並且必須釋放,但需要使用分配器的釋放對應項 my_str_free()
釋放。我們可以使用以下方法創建這樣的結果值適配器
wasm.xWrap.resultAdaptor('my_str_alloc*', (v)=>{
try { return v ? target.cstrToJs(v) : null }
finally{ wasm.exports.my_str_free(v) }
};
設置完成後,我們可以進行如下調用
const f = wasm.xWrap('my_function', 'my_str_alloc*', ['i32', 'string']);
const str = f(17, "hello, world");
// ^^^ the memory allocated for the result using my_str_alloc()
// is freed using my_str_free() before f() returns.
同樣,假設我們有一個自定義 JS 類,它有一個名為 pointer
的成員屬性,該屬性指向此 JS 類表示的結構體的 C 端內存2。然後,我們可以使用類似以下內容的程式碼,使將此類物件傳遞給 C API 成為合法操作
const argPointer = wasm.xWrap.argAdapter('*'); // default pointer-type adapter
wasm.xWrap.argAdaptor('MyType',(v)=>{
if(v instanceof MyType) v = v.pointer;
if(wasm.isPtr(v)) return argPointer(v);
throw new Error("Invalid value for MyType argument.");
});
設置完成後,我們可以包裝我們的函數之一,例如
const f = wasm.xWrap('MyType_method', undefined, ['MyType', 'i32']);
const my = new MyType(...);
// ^^^ assume this allocates WASM memory referenced via my.pointer.
f( my /* will use my.pointer */, 17 );
可以對結果值進行類似的轉換,但如何對結果值進行轉換完全取決於用戶端內存管理的語義。
(卸載)安裝 WASM 函數
當使用需要回調函數指針的 C API 時,不能簡單地將 JS 函數傳遞給它們。相反,JS 函數必須被代理到 WASM 環境中,並且該代理必須被傳遞給 C。這是通過動態編譯少量二進制 WASM 程式碼來完成的,該程式碼描述了 WASM 術語中的函數簽章,將其參數轉發給提供的 JS 函數,並返回該 JS 函數的結果。細節很複雜,但用法很簡單...
installFunction()
指標 installFunction(函數簽章, 函數)
指標 installFunction(函數, 函數簽章)
需要一個 JS 函數和簽章,與 wasm.jsFuncToWasm()
完全相同。它使用該函數創建一個 WASM 導出的函數,將該函數安裝到 wasm.functionTable()
的下一個可用槽中,並返回該函數在該表中的索引(作為指向該函數的指標)。返回的指標可以傳遞給 wasm.uninstallFunction()
以卸載它並釋放表槽以供重用。
作為一種特殊情況,如果傳入的函數是 WASM 導出的函數,則忽略簽章參數,並且按原樣安裝 func,而無需重新編譯/重新包裝。
如果 WebAssembly.Table.grow()
拋出異常或 wasm.jsFuncToWasm()
拋出異常,則此函數將傳播異常。在前一種情況下,在使用 *不帶* Emscripten 的 -sALLOW_TABLE_GROWTH
標誌構建的 Emscripten 編譯環境中可能會發生這種情況。
jsFuncToWasm()
函數 jsFuncToWasm(函數, 簽章)
函數 jsFuncToWasm(簽章, 函數)
創建一個包裝給定 JS 函數的 WASM 函數,並返回該 WASM 函數的 JS 綁定。函數簽章字串必須採用 jaccwabyt 或 Emscripten 的 addFunction()
使用的形式。簡而言之:它可以具有以下格式之一
Emscripten:
"x..."
,其中第一個 x 是表示結果類型的字母,後續字母表示參數類型。詳見下文。沒有參數的函數只有一個字母。Jaccwabyt:
"x(...)"
,其中x
是表示結果類型的字母,括號中的字母(如果有的話)表示參數類型。沒有參數的函數使用x()
。詳見下文。
支援的字母
i
= int32(32 位元整數)p
= int32(「指標」)j
= int64(64 位元整數)f
= float32(32 位元浮點數)d
= float64(64 位元浮點數)v
= void(無類型),僅可用作結果類型
如果使用了無效的簽章字母,它會拋出錯誤。
Jaccwabyt 格式簽章3 支援一些額外的字母,這些字母在此沒有特殊含義,但(在此上下文中)作為其他字母的別名
s
、P
:與p
相同
scopedInstallFunction()
指標 scopedInstallFunction(funcSignature, function)
指標 scopedInstallFunction(function, funcSignature)
此函數與 installFunction()
的工作方式完全相同,不同之處在於安裝範圍限定於目前的分配範圍,並在目前的分配範圍彈出時解除安裝。如果沒有作用中的分配範圍,它會拋出錯誤。
uninstallFunction()
函數 uninstallFunction(指標)
需要先前從 wasm.installFunction()
返回的指標值。從 WASM 函數表中移除該函數,將其表槽標記為可供重新使用,並返回該函數。在呼叫 installFunction()
之前呼叫此函數是非法的,如果參數不是由該函數返回的,則結果未定義。返回的函數可以傳回給 installFunction()
以重新安裝它。
通用工具函式
isPtr()
布林值 isPtr(值)
如果其值是 WASM 指標類型,則返回 true。也就是說,它是一個大於或等於零的 32 位元整數。
關於混合 JS 和 C 程式碼的 WASM 特有特性
另請參閱:注意事項
從 WASM 到 C 的轉換是*相對*透明的。藉助少量程式碼,從 C 到 JS 的轉換在大多數情況下也*相對*透明。本章涵蓋*不太*透明的方面。
從 JS 使用輸出指標參數
輸出指標參數在 C 語言中很常見。相反,它們在 JavaScript 中根本不存在。在 sqlite3 API 中,一個例子是
int sqlite3_open_v2(const char *zDbFile, sqlite3** pDb, int flags, const char *zVfs);
第二個參數上的兩個指標限定符號表示它是一個所謂的輸出參數:函數可以透過為該指標分配一個新值來向呼叫者回報一個值。
在 JavaScript 中使用輸出指標需要幾個步驟
- 配置 WASM 記憶體以儲存指標值。
- 使用
wasm.pokePtr()
或等效函數為其分配初始值(通常為 0)。 - 呼叫 WASM 函數並將指標傳遞給它。
- 使用
wasm.peekPtr()
或等效函數擷取輸出指標的新值。這在語義上等同於在 C 語言中對指標進行取值。 - 釋放步驟 (1) 中配置的指標。
在其最基本的形式中,它看起來像這樣
const wasm = sqlite3.wasm;
const ppOut = wasm.alloc(wasm.ptrSizeof); // allocate space for a pointer
wasm.pokePtr(ppOut, 0); // zero out the memory
const rc = some_c_function( ..., ppOut ); // pass ppOut to a C function
const pOut = wasm.peekPtr(ppOut); // fetch the pointed-to value
wasm.dealloc(ppOut); // free space for the pointed-to value
// pOut now holds the output result value.
if(0===rc) { ... success ... }
else { ... error ... }
顯然,這需要大量的程式碼,並且存在一些缺點,例如如果 some_c_function()
拋出 JS 異常,則會洩漏 ppOut
記憶體。藉助 sqlite3.wasm
和 try
/finally
區塊的幫助,可以顯著清理它
const scope = wasm.scopedAllocPush();
try {
const ppOut = wasm.scopedAllocPtr(); // alloc and zero pointer
const rc = some_c_function( ..., ppOut );
const pOut = wasm.peekPtr(ppOut);
if(0===rc) { ... success ... }
else { ... error ... }
}finally{
// free all "scoped allocs" made in the context of `scope`,
// in our case ppOut.
wasm.scopedAllocPop(scope);
}
或者不使用「作用域分配」機制
let pOut, ppOut;
try {
ppOut = wasm.allocPtr(); // alloc and zero pointer
const rc = some_c_function( ..., ppOut );
pOut = wasm.peekPtr(ppOut);
if(0===rc) { ... success ... }
else { ... error ... }
}finally{
wasm.dealloc(ppOut);
}
或者使用「pstack」分配器,它是為了這種情況下更高效(更快)的選項而新增的。
const stack = wasm.pstack.pointer;
try {
const ppOut = wasm.pstack.allocPtr(); // "alloc" and zero memory
const rc = some_c_function( ..., ppOut );
const pOut = wasm.peekPtr(ppOut);
if(0===rc) { ... success ... }
else { ... error ... }
}finally{
wasm.pstack.restore(stack);
}
需要注意的是,pstack
有一個小的靜態記憶體緩衝區,因此不能用於一般用途的分配。儘管它是靜態記憶體,但它的訪問方式就像它是 WASM 堆的一部分一樣,因此可以通過與堆記憶體相同的記憶體訪問器例程來訪問它。
在 sqlite3 JS 程式碼中,使用 try
/finally
區塊是一種常見的習慣用法,廣泛用於管理記憶體和物件的生命週期。無論 try
區塊如何退出(通過 return
、continue
、break
、throw
或運行到完成),finally
區塊都會被執行。當它執行時,在 try
區塊中進行的任何「作用域」分配都將被釋放。在此範例中,我們只有一個這樣的分配,但多個分配並不罕見。與使用較低階的 alloc()
和 dealloc()
例程(它們分別對應 C 級的 sqlite3_malloc()
和 sqlite3_free()
)相比,作用域分配 API 簡化了許多常見用例中的記憶體釋放。peekPtr()
和 pokePtr()
輔助函式是 peek()
和 poke()
的精簡包裝器,它們消除了記住為後一個函式的最後一個參數傳遞除預設值以外的某些值的需要(預設值對於像上面演示的這種情況來說是錯誤的值)。
- ^ 不要與
sqlite3.wasm
檔案混淆,sqlite3.wasm
命名空間實際上是對它的包裝。 - ^ 就像 ./c-structs.md 中描述的那樣。
- ^ 此程式碼是與 jaccwabyt 共同開發的,因此支援其簽章格式。