在〈建立記憶體〉中談過,WebAssembly 預設的資料型態有 i32
、i64
、f32
、f64
,對於高階的資料結構,例如陣列,必須自行實作,接下來,就以實作陣列作為例子,首先,來決定一下陣列在記憶體中的結構…
為了簡化,只考慮 i32
整數陣列,記憶體中第一個 i32
打算用來記錄可用空間的位元組偏移量,至於每個陣列,會使用一個 i32
來記錄陣列長度,之後是陣列的元素。
也就是若有兩個陣列,一個長度為 2,一個長度為 3,那麼記憶體中的資料會是:
可以定義一個 $arr
函式來建立陣列,它接受一個長度引數,傳回陣列首元素的位元組偏移量:
;; 建立新陣列
(func $arr (param $len i32) (result i32)
(local $offset i32) ;; 記錄陣列偏移量
(set_local $offset (i32.load (i32.const 0))) ;; 取得偏移量
(i32.store (get_local $offset) ;; 首個 i32 儲存陣列長度
(get_local $len)
)
(i32.store (i32.const 0) ;; 在記憶體開頭記錄可用空間的偏移量
(i32.add
(i32.add
(get_local $offset)
(i32.mul
(get_local $len)
(i32.const 4)
)
)
(i32.const 4) ;; 別忘了每個陣列首個 i32 是記錄長度
)
)
(get_local $offset) ;; 建立的陣列偏移量
)
建立陣列時,會從記憶體第一個 i32
取得可用空間的位元組偏移量,在該位置儲存陣列長度,這個位置最後會作為函式呼叫的結果值,接著在記憶體第一個 i32
記錄可用空間的偏移量。
要取得陣列長度的話,可以定義一個 $len
函式:
;; 取得陣列長度
(func $len (param $arr i32) (result i32)
(i32.load (get_local $arr))
)
為了取得陣列長度,必須傳入陣列在記憶體裡的偏移量,也就是 $arr
建立陣列後的傳回值,$len
會以 i32
取出數值,這個數值就是陣列長度。
接著來實作陣列索引存取,為了方便,先建立一個 $offset
,在指定陣列索引時,協助計算出每個元素在記憶體中的偏移量:
;; 在指定陣列索引時,計算出每個元素在記憶體中的偏移量
(func $offset (param $arr i32) (param $i i32) (result i32)
;; 陣列偏移量 + 根據索引及型態計算而得的偏移量
(i32.add
(i32.add (get_local $arr) (i32.const 4)) ;; 別忘了每個陣列首個 i32 是記錄長度
(i32.mul (i32.const 4) (get_local $i)) ;; 一個 i32 元素是四個位元組
)
)
接著是 $set
與 $get
,接受索引來設值與取值:
;; 使用索引設定元素值
(func $set (param $arr i32) (param $i i32) (param $value i32)
(i32.store
(call $offset (get_local $arr) (get_local $i))
(get_local $value)
)
)
;; 使用索引取得元素值
(func $get (param $arr i32) (param $i i32) (result i32)
(i32.load
(call $offset (get_local $arr) (get_local $i))
)
)
來實際建立一個陣列:
(func $main
(local $a1 i32)
;; 因為記憶體首個 i32 記錄可用空間偏移量
;; 第一個可用空間偏移量應為 4(位元組)
(i32.store (i32.const 0) (i32.const 4))
(set_local $a1 (call $arr (i32.const 5))) ;; 建立長度為 5 的陣列,指定給 $a1
(call $len (get_local $a1))
call $log ;; 顯示長度為 5
;; 在 $a1 索引 1 存入 10
(call $set (get_local $a1) (i32.const 1) (i32.const 10))
;; 取得 $a1 索引 1 的值
(call $get (get_local $a1) (i32.const 1))
call $log ;; 顯示元素值為 10
)
程式在一開始時,就會將記憶體首元素設定為 4,這是因為第一個 i32
用來儲存可用空間偏移量,因而可用空間要從 4 開始。
底下是完整的程式實作:
(module
(import "env" "log" (func $log (param i32)))
(memory 1)
;; 建立新陣列
(func $arr (param $len i32) (result i32)
(local $offset i32) ;; 記錄陣列偏移量
(set_local $offset (i32.load (i32.const 0))) ;; 取得偏移量
(i32.store (get_local $offset) ;; 首個 i32 儲存陣列長度
(get_local $len)
)
(i32.store (i32.const 0) ;; 記憶體開頭記錄可用空間的偏移量
(i32.add
(i32.add
(get_local $offset)
(i32.mul
(get_local $len)
(i32.const 4)
)
)
(i32.const 4) ;; 別忘了每個陣列首個 i32 是記錄長度
)
)
(get_local $offset) ;; 建立的陣列偏移量
)
;; 取得陣列長度
(func $len (param $arr i32) (result i32)
(i32.load (get_local $arr))
)
;; 在指定陣列索引時,計算出每個元素在記憶體中的偏移量
(func $offset (param $arr i32) (param $i i32) (result i32)
;; 陣列偏移量 + 根據索引及型態計算而得的偏移量
(i32.add
(i32.add (get_local $arr) (i32.const 4)) ;; 別忘了每個陣列首個 i32 是記錄長度
(i32.mul (i32.const 4) (get_local $i)) ;; 一個 i32 元素是四個位元組
)
)
;; 使用索引設定元素值
(func $set (param $arr i32) (param $i i32) (param $value i32)
(i32.store
(call $offset (get_local $arr) (get_local $i))
(get_local $value)
)
)
;; 使用索引取得元素值
(func $get (param $arr i32) (param $i i32) (result i32)
(i32.load
(call $offset (get_local $arr) (get_local $i))
)
)
(func $main
(local $a1 i32)
;; 因為記憶體首個 i32 記錄可用空間偏移量
;; 第一個可用空間偏移量應為 4(位元組)
(i32.store (i32.const 0) (i32.const 4))
(set_local $a1 (call $arr (i32.const 5))) ;; 建立長度為 5 的陣列,指定給 $a1
(call $len (get_local $a1))
call $log
;; 在 $a1 索引 1 存入 10
(call $set (get_local $a1) (i32.const 1) (i32.const 10))
;; 取得 $a1 索引 1 的值
(call $get (get_local $a1) (i32.const 1))
call $log
)
(start $main)
)
現在來思考一個問題,如果要匯出陣列呢?別忘了,記憶體匯出至 JavaScript,其實是個 ArrayBuffer
,因此,必須有個中間的轉換函式,依陣列在記憶體中的結構,從 ArrayBuffer
各個元素,然後指定給 JavaScript 的陣列。
那麼將 JavaScript 陣列匯入 WebAssembly 呢?一樣地,要有個中間的轉換函式,從 JavaScript 陣列收集元素,然而依結構存入 ArrayBuffer
,無論這個 ArrayBuffer
是來自於匯出的 WebAssembly.Memory
,或者是自行建立 WebAssembly.Memory
後匯入模組。
不同的語言會有不同的高階資料結構,這些語言在能支持編譯為 WebAssembly 時,應該也都會提供轉換程式庫,來支持各自高階資料結構的匯出、匯入、轉換等,像是 Go 1.11 中 wasm_exec.js 這類的東西。