Defining and calling a function


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