函式表


在〈Hello 模組〉中看過,Wat 有 type 可以定義 Type 區段,不過 Type 部份通常會自動根據 Wat 內容產生,例如,一個簡單的函式:

(module
    (func $main)
)

使用 wasm2wat 將產生的 .wasm 轉為 .wat,可以看到:

(module
  (type (;0;) (func))
  (func (;0;) (type 0)))

對於有相同簽署的函式,在編譯時會自動產生對應的函式型態,既然函式有型態,那麼它是個值嗎?可以儲存嗎?

堆疊上不能存放函式,變數也不行,若要能儲存函式,必須使用 table 定義表格,就撰寫本文的這個時間點來說,WebAssembly 每個模組只能定義一個表格:

(module
    (table $tb 2 anyfunc)
    ...
)

在這邊,2 表示表格的初始長度,也就是可以儲存兩個函式,可以在這個數字後選擇性地增加另一個數字,表示表格最大長度;anyfunc 表示可以是任何簽署的函式,目前也只支援這個值,未來可能會支援其他值。

想將函式儲存至表格,要使用 elem,例如:

(module
    (import "env" "log" (func $log (param i32)))
    (table $tb 2 anyfunc)
    (func $f1 (param $p i32)
        (i32.add (get_local $p) (i32.const 10))
        call $log
    )
    (func $f2 (param $p i32)
        (i32.add (get_local $p) (i32.const 20))
        call $log
    )
    (elem (i32.const 0) $f1 $f2)
    ...
)

(i32.const 0) 是偏移量,因此在這邊,函式會從索引 0 開始儲存,若必要也可以將表格匯出,例如:

(module
    (import "env" "log" (func $log (param i32)))
    (table $tb 2 anyfunc)
    (func $f1 (param $p i32)
        (i32.add (get_local $p) (i32.const 10))
        call $log
    )
    (func $f2 (param $p i32)
        (i32.add (get_local $p) (i32.const 20))
        call $log
    )
    (elem (i32.const 0) $f1 $f2)
    (export "tb" (table $tb))
)

被匯出的表格,在 JavaScript 中會是個 WebAssembly.Table 實例,可以使用其 get 方法指定索引,取得對應的函式,例如:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
  </head>
  <body>
    <script>
    const importObj = {
        env: {
            log(n) {
                console.log(n);
            }
        }
    };

    WebAssembly.instantiateStreaming(fetch('program.wasm'), importObj)
               .then(prog => {
                   const f1 = prog.instance.exports.tb.get(0);
                   const f2 = prog.instance.exports.tb.get(1);
                   f1(10);
                   f2(10);
               });
    </script>
  </body>
</html>

你也可以在 JavaScript 中建立 WebAssembly.Table 實例,設定函式之後,匯入模組之中:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
  </head>
  <body>
    <script>

    const tb = new WebAssembly.Table({initial:2, element:"anyfunc"});
    const importObj = {
        env: {
            log(n) {
                console.log(n);
            },
            tb : tb
        }
    };

    WebAssembly.instantiateStreaming(fetch('program.wasm'), importObj)
               .then(_ => {
                   let f1 = tb.get(0);
                   let f2 = tb.get(1);
                   f1(10);
                   f2(10);

                   tb.set(0, f2); // 必須是 WebAssembly 中定義的函式才可以使用 set 
                   tb.set(1, f1);

                   f1 = tb.get(0);
                   f2 = tb.get(1);
                   f1(10);
                   f2(10);
               });
    </script>
  </body>
</html>

這個 HTML 中的 JavaScript,建立了 WebAssembly.Table 實例並作為匯入物件的特性,在 WebAssembly 模組裏對表格儲存函式之後,於 JavaScript 呼叫執行。

在這邊也看到,可以透過 WebAssembly.Tableset 方法設定函式,然而函式必須是在 WebAssembly 模組中定義之函式。

在 Wat 的部份,可以如下撰寫匯入表格:

(module
    (import "env" "log" (func $log (param i32)))
    (import "env" "tb" (table $tb 2 anyfunc))
    (func $f1 (param $p i32)
        (i32.add (get_local $p) (i32.const 10))
        call $log
    )
    (func $f2 (param $p i32)
        (i32.add (get_local $p) (i32.const 20))
        call $log
    )
    (elem (i32.const 0) $f1 $f2)
)

既然可以透過 WebAssembly.Tableset 方法設定函式,這表示,若某模組中呼叫的函式來自表格,就可以動態地改變表格的函式內容,從而令該某模組呼叫的函式也隨之不同。

至於該如何於模組中呼叫表格中儲存之函式,這會在下一篇文件中討論。