實作陣列


在〈建立記憶體〉中談過,WebAssembly 預設的資料型態有 i32i64f32f64,對於高階的資料結構,例如陣列,必須自行實作,接下來,就以實作陣列作為例子,首先,來決定一下陣列在記憶體中的結構…

為了簡化,只考慮 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 這類的東西。