定義與呼叫函式


雖然從一開始談 WebAssembly,就已經在使用函式了,不過還沒有正式談過它的定義與呼叫方式,在〈Hello 模組〉中看到,函式是 WebAssembly 的區段之一,一個最簡單的函式可以是:

(module
    (func $main)
)

$main 名稱只是撰寫時增加可讀性,每個函式實際上會有個索引,不命名函式也是可以的:

(module
    (func)
)

當然,就撰寫的可讀性來說,為函式取個名稱會比較好。

可以為函式加上參數、結果型態,例如底下的 $add 函式:

(module
    (import "env" "log" (func $log (param i32)))
    (func $add (param $lhs i32) (param $rhs i32) (result i32)
        (i32.add (get_local $lhs) (get_local $rhs))
    )   
    (func $main
        (call $add (i32.const 1) (i32.const 2))
        call $log
    )
    (start $main)
)

在定義函式時,使用 param 定義參數,可以為其取個名稱,實際上最後會是使用索引,參數被視為區域變數,因此使用 get_local 來取得值,函式是一種區塊,可以定義結果型態,若沒有定義結果型態,執行完函式後堆疊必須為空。

start 指定的函式,不可定義參數與結果型態,start 呼叫的函式,會在模組載入、初始化之後執行,start 之後也可以接上索引。

call 用來呼叫函式,可以指定名稱(如果有定義的話)或索引,如果呼叫函式時需要引數,必須先依序置入堆疊,call 會依照呼叫的函式之參數定義,依序從堆疊中取出數值並設定給參數,如果函式有定義結果型態,執行完後可以在堆疊中留下數值,這個數值在離開函式之後,會置入先前堆疊,可以繼續其他指令操作。

函式中如果有區域變數,必須定義在參數、結果型態之後(如果有的話),例如,來定義一個求第 n 個費式數的 $fib 函式:

(module
    (import "env" "log" (func $log (param i32)))
    (func $fib (param $n i32) (result i32)                       ;; 求第 n 個費式數        
        (local $a i32) (local $b i32)
        (local $i i32) (local $tmp i32)

        (i32.or                                                  ;; n == 0 || n == 1
            (i32.eqz (get_local $n))
            (i32.eq (get_local $n) (i32.const 1))
        )

        if (result i32)
            get_local $n
        else
            (set_local $b (i32.const 1))                         ;; b = 1
            (set_local $i (i32.const 2))                         ;; i = 2
            loop (result i32)
                (i32.le_s (get_local $i) (get_local $n))         ;; i <= n
                if
                    (set_local $tmp (get_local $b))              ;; tmp = b
                    (set_local $b                                ;; b = a + b
                        (i32.add (get_local $a) (get_local $b)))
                    (set_local $a (get_local $tmp))              ;; a = tmp
                    (set_local $i                                ;; i = i + 1
                        (i32.add (get_local $i) (i32.const 1)))
                    br 1
                end
                get_local $b
            end
        end
    )
    (func $main
        (call $fib (i32.const 10))
        call $log
    )
    (start $main)
)

上頭是在堆疊中留下一個數值,然後等待函式執行完成,你也可以使用 return,這麼一來,return 之後的流程就不會執行,語義上也比較明確:

(module
    (import "env" "log" (func $log (param i32)))
    (func $fib (param $n i32) (result i32)                       ;; 求第 n 個費式數        
        (local $a i32) (local $b i32)
        (local $i i32) (local $tmp i32)

        (i32.or                                                  ;; n == 0 || n == 1
            (i32.eqz (get_local $n))
            (i32.eq (get_local $n) (i32.const 1))
        )

        if (result i32)
            (return (get_local $n))
        else
            (set_local $b (i32.const 1))                         ;; b = 1
            (set_local $i (i32.const 2))                         ;; i = 2
            loop (result i32)
                (i32.le_s (get_local $i) (get_local $n))         ;; i <= n
                if
                    (set_local $tmp (get_local $b))              ;; tmp = b
                    (set_local $b                                ;; b = a + b
                        (i32.add (get_local $a) (get_local $b)))
                    (set_local $a (get_local $tmp))              ;; a = tmp
                    (set_local $i                                ;; i = i + 1
                        (i32.add (get_local $i) (i32.const 1)))
                    br 1
                end
                (return (get_local $b))
            end
        end
    )
    (func $main
        (call $fib (i32.const 10))
        call $log
    )
    (start $main)
)

就撰寫這篇文件的時間點,函式只能有一個結果值,未來可能支援多個結果值。

函式可以遞迴呼叫,例如將上面的 $fib 函式改為遞迴版本:

(module
    (func $fib (param $n i32) (result i32)             ;; 求第 n 個費式數        
        (i32.or                                        ;; n == 0 || n == 1
            (i32.eqz (get_local $n))
            (i32.eq (get_local $n) (i32.const 1))
        )

        if (result i32)
            (return (get_local $n))
        else
            ;; return fib(n - 1) + fib(n - 2)
            (return (i32.add                                              
                (call $fib (i32.sub (get_local $n) (i32.const 1))) 
                (call $fib (i32.sub (get_local $n) (i32.const 2)))
            ))
        end
    )
    (export "fib" (func $fib))
)

這個程式也示範了如何匯出函式,被匯出的名稱,會成為 WebAssembly.Instance 實例上 exports 的特性,來個無聊的評比好了:

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

    function fibJS(n) {
        if(n == 0 || n == 1) {
            return n;
        }
        return fibJS(n - 1) + fibJS(n - 2);
    }

    WebAssembly.instantiateStreaming(fetch('program.wasm'))
               .then(prog => {
                   const n = 40;
                   const fibWasm = prog.instance.exports.fib;

                   let start = new Date().getTime();
                   fibWasm(n);
                   console.log(new Date().getTime() - start);

                   start = new Date().getTime();
                   fibJS(n);
                   console.log(new Date().getTime() - start);
               });
    </script>
  </body>
</html>

一個 WebAssembly 求 40 個費式數,一個用 JavaScript 求 40 個費式數,試試看,哪個會比較快呢?…XD