匯入與匯出


WebAssembly 最小可行版本(Minimum Viable Product, MVP)就只有四個對象可以匯入、匯出,也就是到目前為止,看過的函式、全域變數、表格與記憶體。

在匯入的部份,之前就看過了,import 都會包含兩個名稱,分別對應至匯入物件的第一層與第二層特性名稱,不過,import 包含的這兩個名稱,規格上是稱為模組名稱(module name)與匯入名稱(import name)。

這或許意謂著在安排匯入物件時,第一層特性應該是模組名稱空間,像是 Instance 實例上 exports 特性的角色,第二層特性就是模組被匯出的各個對象,像是函式、全域變數、表格與記憶體。

例如,若有個 foo 模組撰寫為:

(module
    (func $foo1 (export "foo1") (result i32)
        i32.const 1
    )
    (func $foo2 (export "foo2") (result i32)
        i32.const 2
    )    
)

在這邊看到 export 的另一種風格,可以直接寫在要匯出的函式上,實際上這個模組沒有呼叫任何函式,因此 $foo1$foo2 是可以省略的。

若有另一個模組,會需要使用到 foo 模組匯出的函式:

(module
    (import "env" "log" (func $log (param i32)))
    (import "foo" "foo1" (func $foo1 (result i32)))
    (import "foo" "foo2" (func $foo2 (result i32)))
    (func $main
        call $foo1
        call $log
        call $foo2
        call $log
    )
    (start $main)
)

目前來說,瀏覽器尚未整合模組的載入、初始化等,因此這並不會下載 envfoo 模組,目前得自己動手來做:

(async () => {
    const foo = await WebAssembly.instantiateStreaming(fetch('foo.wasm'));
    const importObj = {
        env : {
            log(n) {
                console.log(n);
            }
        },
        foo : foo.instance.exports
    };
    WebAssembly.instantiateStreaming(fetch('program.wasm'), importObj);
})();

當然,可以這麼寫的原因在於,事先知道 program 模組,需要從哪個模組 import 函式之類的,問題在於,撰寫 JavaScript 時,怎麼在拿到一個模組時,事先知道它當中要匯入哪些模組的東西呢?

WebAssembly.Module 定義了個 imports 函式,可以取得模組宣告的 import 相關資訊,給它一個 WebAssembly.Module 實例,它會傳回一個陣列,當中的各元素是個物件,擁有 kindmodule、與 name 三個特性,分別表示匯入的是哪種對象(例如 'function')、模組名稱、匯入名稱。

因此,上例也可以改寫為:

function moduleNames(mod, importObj) {
    return Array.from(
        new Set(
            WebAssembly.Module.imports(mod)
                              .map(impt => impt.module)
                              .filter(name => !(name in importObj))
        )
    );
}

(async () => {
    const importObj = {
        env : {
            log(n) {
                console.log(n);
            }
        }
    };

    const progModule = await WebAssembly.compileStreaming(fetch('program.wasm'));
    const names = moduleNames(progModule, importObj);
    const results = await Promise.all(
        names.map(name => WebAssembly.instantiateStreaming(fetch(`${name}.wasm`)))
    );

    for(let i = 0; i < names.length; i++) {
        importObj[names[i]] = results[i].instance.exports;
    }

    WebAssembly.instantiate(progModule, importObj);
})();

如果在建立 WebAssembly.Instance 實例之前,需要進一步知道模組匯出的東西,WebAssembly.Module 也有定義 exports 函式來處理這件事。

當然,上面的範例很單純,複雜的情況下,會有模組匯入的順序等問題,不過仔細想想,這應該是未來瀏覽器要處理的事情,或許是在 WebAssembly 可以整合 ES6 模組之後的事情,如果沒有,也應該會有個模組管理程式庫來做這類的事。

在匯入、匯出函式、全域變數、表格與記憶體時,其實有一些要注意的小細節,這些記載在〈Imports〉中,如果你可以跟著文件一路來到這邊,自行參閱應該是不成問題。