Dive into Wasm: Functions
Functions are one of the main building blocks of Wasm. In the following sections I discover how to use them and how they are built.
The first function
WAT code is represented as functions in this syntax:
( func <signature> <locals> <body> )
- The signature declares the input parameters and the return values.
- The locals are variables.
- The body is a list of low-level instructions.
Here is the syntax definition.
This function takes two 32-bit params and results one 64-bit float:
(func (param i32) (param i32) (result f64))
You can also name the params with a $ before their type:
(func (param $x i32) (param $y i32) (result i32))
This function takes two parameters and returns the second one:
(module
(func (param $x i32) (param $y i32) (result i32)
local.get $y
)
)
You see that you can access the params with local.get followed by the name.
A function that makes more sense
Why not use this knowledge and create a function that makes more sense?
The following one adds two input numbers together and returns the result:
(module
(func $add (param $x i32) (param $y i32) (result i32)
local.get $x
local.get $y
i32.add)
)
You can see that the function here got the name $add. If you don’t give it a name, then the function will be numbered from top to down, beginning at 0. This also applies to other building blocks like types.
One important thing is, that the execution of Wasm code follows the principle of a stack machine. So the local.get instructions pushes the value it read onto the stack.
The i32.add instruction pops two i32 values from the stack, calculates their sum and pushes the result back onto the stack. The return value of a function is the value which is left on the stack.
But how do we access it?
Accessing the function from the outside
You can export functions and make them accessible to the outside.
The following code exports the $add function and makes it possible to invoke it by calling add.
(module
(func $add (param $x i32) (param $y i32) (result i32)
local.get $x
local.get $y
i32.add)
(export "add" (func $add))
)
Compile the code and invoke the function by using wasmtime:
wasmtime sample.wasm --invoke add 1 2
Another syntax for the export is the usage of the export in the signature:
(module
(func $add (export "add") (param $x i32) (param $y i32) (result i32) ;; export in signature
local.get $x
local.get $y
i32.add)
)
Defining types
You can also define a type for a function and use it then in the signature. The following example shows the $add function that has the $addType signature:
(module
(type $addType (func (param i32 i32) (result i32)))
(func $add (type $addType)
local.get 0
local.get 1
i32.add)
)
You see that this changes the access to the locals, but it also makes it a little bit cleaner.
Calling other functions
You can also define more than one function and call another function within the Wasm module.
Check the following example which makes use of the call instruction:
(module
(func $getOneValue (result i32)
i32.const 19)
(func $add (param $x i32) (result i32)
call $getOneValue
local.get $x
i32.add)
(export "add" (func $add))
)
You see that you call other functions with the call instruction. This executes the function and pushes the result onto the stack. When you compile the code, you can invoke the add function:
wasmtime write.wasm --invoke add 1
This calls the $getOneValue function, which returns 19 (and pushes this value to the stack). Afterwards this value gets added to the input.
Calling with parameters
Sure, you can also call functions with parameters.
In the following you can see how this is done.
(module
(func $sumWithOneValue (param $x i32) (result i32)
i32.const 19
local.get $x
i32.add
)
(func $add (param $x i32) (result i32)
local.get $x
call $sumWithOneValue
local.get $x
i32.add)
(export "add" (func $add))
)
In short the example represents the following calculation: x + 19 + x.
The $sumWithOneValue function takes one parameter. In this case this will be the input value, which gets pushed to the stack before calling it. The $sumWithOneValue function itself adds the input with 19 and returns the result.
Then the result is on the stack and the input value is again pushed onto it. Afterwards the i32.add function gets called again.
Importing functions from the outside
You can also import functions from the outside. A simple sample can look like the following one:
(module
(type $fd_write_type (func (param i32 i32 i32 i32) (result i32)))
(import "wasi_snapshot_preview1" "fd_write" (func $write (type $fd_write_type)))
(func $_start (param $x i32) (result i32)
;; load params here ...
call $write
)
(export "start" (func $_start))
)
First, the $fd_write_type is defined which is used as a signature in the imported function one line below.
The import says that a function fd_write from the module wasi_snapshot_preview1 is needed. This is imported as the $write function into our module and can be called afterwards.
You can see a full example in the post about writing an own WASI function.
Explicit returns and drops
Until this point we returned implicitly the result. As per default the return of a function is the last value on the stack.
But you can also return values explicitly with the return statement. See the following example:
(module
(func (export "main") (result i32)
(return (i32.const 99))
)
)
If you don’t need the value on the stack anymore, you can call drop. Check the following example:
(module
(func (export "main") (result i32)
i32.const 99
i32.const 100
i32.const 100
i32.add
drop
)
)
This will return 99 as you drop the result from the stack.
Start function (“main” method)
If you want to invoke a function after the module was initialized, you can use the start function. This is comparable to a main method (indeed, it’s not a method, it’s a function!).
The function which is invoked must not return anything! Check the following example:
(module
(func $main
i32.const 100
i32.const 100
i32.add
drop
)
(start $main)
)
Conclusion
This post showed the different parts of a function, how you can export and name it and invoke it with parameters.
For modules that need to be invoked automatically after the initialization, you can use the start function.