You've been using functions for a while, but I haven't talked about functions detaily. In Hello Module, you've known that functions are defined in sections of a module. The simplest function can be:
(module
(func $main)
)
The function name is for readability. Functions are assigned monotonically-increasing indices based on the order of definition in the module so a function without a name is allowed.
(module
(func)
)
A function may declare parameters and a return type, such as the $add
function below.
(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)
)
You use param
to declare parameters and their names are optional. After compiling, all are indices. Parameters are viewed as local variables so use get_local
to retrieve their values. A function creates a block. If it doesn't define the return type, you should empty the stack before leaving the function.
The start
function must not take any arguments or return anything. After initializing the module instance, the function start
refers would be called.
The call
instruction calls a single function, given its index or name. Push enough numbers onto the stack before calling a function. If your function declares a result type, the left number on the stack can be used after leaving the function.
Local variables should be declared after the result type (if any). For example, let's define a $fib
function which returns the nth Fibonacci number.
(module
(import "env" "log" (func $log (param i32)))
(func $fib (param $n i32) (result i32) ;; nth
(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)
)
You can use return
in the function. The instructions after return
would not be executed.
(module
(import "env" "log" (func $log (param i32)))
(func $fib (param $n i32) (result i32) ;; nth
(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)
)
In the MVP, a function can only return one result value. It might return more than one value in the future.
WebAssembly functions support recursion. For example, the following is a recursive version of the above program.
(module
(func $fib (param $n i32) (result i32) ;; nth
(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))
)
The exported function will be a property of exports
defined in the instance of WebAssembly.Instance
. Let's run a simple benchmark.
<!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>
Guess what will be faster in your browser? :p