雖然從一開始談 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