All applications are made up of many different functions. Creating them in WAT is just like any other programming language, containing parameters for inputs and return values if required. Here we are going to explore what it takes to create and use functions within our WAT code, and learn how to import and export them into the JavaScript world.
Let's take a look at a simple function and see how it is defined.
(func $add (param $first i32) (param $second i32) (result i32) ;; Add first and second parameters together and return result local.get $first local.get $second i32.add )
This is an S-Expression, so it starts with a (
parentheses character followed by the func
keyword,
which is declaring that everything inside is part of the function.
This is followed by the name of the function, its identifier, which is $add
in this case.
The function name must be unique, you cannot have two functions with the same name.
Following this is the list of parameters. You can have any number of parameters or none at all. To add a parameter you use the following S-Expression.
(param <name> <type>)
It starts with the param
keyword followed by the name of the parameter and then its type.
The name of the parameter must be unique within the function, but another function can have a parameter with the same name.
You do not need to have parameter names. Instead you can use the parameter's index, which starts at 0 and increases by one for each parameter you have.
(func $add2 (param i32) (param i32) (result i32) ;; Add first and second parameters together and return result local.get 0 local.get 1 i32.add )
Here we have no parameter names.
Instead we use the 0
and 1
index values to link to the first and second parameters.
We can even go one step further by including all the types within one parameter S-Expression.
(func $add3 (param i32 i32 i32) (result i32) ;; Add all 3 parameters together and return result local.get 0 local.get 1 i32.add local.get 2 i32.add )
This time we have only one param
object followed by 3 parameter types.
This is stating that 3 parameters are required, all of type i32
, which have the indexes 0
, 1
and 2
.
(func $add3b (param $first) (param i32 i32 i32) (param $last) (result i32) ;; ... )
If you wanted to you could even mix parameters up so that some have parameter names and others do not.
Here the $first
parameter would have index 0
, then next three would be 1
, 2
, 3
and
the $last
parameter would be index 4
.
The final part is the return value, which is known as the result. The function does not need to return something, so you can leave it out if it is not needed. To add a result you use the following S-Expression.
(result <type>)
It begins with the keyword result
followed by the return type.
There is no name or index for the result.
The last value pushed onto the stack will be the returning result.
You can return more than one result.
(func $example (param $integer i32) (param $float f64) (result f64 i32) ;; Put parameters on to stack to return them local.get $float local.get $integer )
Here the result contains two values, one an integer and the other a float.
You need to push the result values onto the stack to return them, and they need to be in the correct order.
Here we will end up with the i32
value at the top of the stack with the f64
below it.
This matches the order of the results, with the f64
coming second, and the i32
being first.
You need to be careful to make sure the order of the returning results match the order you add the returning values (and types) onto the stack.
When calling a function, from within another function, you need to make sure you push the parameters for it onto the stack first.
Then you run the function by using the call
instruction and the name of the function.
(func $addTwo (param $value i32) (result i32) ;; Add 2 to $value and return result local.get $value i32.const 2 i32.add ) (func $callFunctionTest (result i32) ;; Push parameter onto the stack i32.const 42 ;; Call the function call $addTwo )
Here we have the $addTwo
function that takes a single parameter, adds two to it, and returns it as the result.
The other $callFunctionTest
function calls it.
Let's take a look at what is happening with the stack as each part happens.
When we run the $callFunctionTest
function the stack is initially empty.
The first line of code pushes the constant i32
integer value 42 onto the stack.
42 | Top of Stack |
The next part calls the $addTwo
function.
When the function is called it pops off the parameter $value
off the stack ready to be used.
Inside the $addTwo
function the stack has become empty again.
We then push the $value
amount back onto the stack, followed by the i32
integer value 2.
2 | Top of Stack |
42 | Bottom of Stack |
The i32.add
instruction pops the last two items off the stack, making it empty again, adds them together (2 + 42 = 44),
and pushing the result onto the stack.
44 | Top of Stack |
This last item on the stack becomes our returning result.
The $addTwo
function is now finished with and control is moved back to the calling function, with the stack containing the result.
Let's look at another example, but this time with more parameters and return results.
(func $addThree (param $integer i32) (param $float f64) (result i32 f64) ;; Add 3 to $integer local.get $integer i32.const 3 i32.add ;; Add 3 to $float local.get $float f64.const 3.0 f64.add ) (func $callFunctionTest (result i32 f64) ;; Push first parameter i32.const 101 ;; Push second parameter f64.const 3.14 ;; Call the function call $addThree )
Within this example we have the $addThree
function that takes 2 parameters, one is an i32
integer and the other is a f64
floating point number.
It adds 3 to each value then returns them both.
We need to make sure the order of the parameters are pushed to the stack in the right way, so the function can pop them off and use them correctly.
Let's run through the process again and see what is happening to the stack.
Like last time, when the $callFunctionTest
function starts, the stack is initially empty.
The $addThree
function has 2 parameters, so we need to push those parameters onto the stack in a way that matches up.
The parameters are listed as an integer first then a float.
We match this by pushing an integer constant, followed by a floating point constant.
3.14 | Top of Stack |
101 | Bottom of Stack |
We then call the $addThree
function.
The function's two parameters are popped off the stack and are ready to be used.
At this point the stack is now empty again.
The steps performed inside the function are performed so that the stack will contain the results we need at the end.
First we push the $integer
value onto the stack, followed by the i32
integer constant value 3.
3 | Top of Stack |
101 | Bottom of Stack |
The i32.add
instruction pops the last two items off the stack, again making it empty, adds them together, and pushes the result onto the stack.
104 | Top of Stack |
The next step is to push the $float
value onto the stack, followed by the f64
floating point constant value 3.0.
3.0 | Top of Stack |
3.14 | |
104 | Bottom of Stack |
You will notice that the last i32.add
result is still on the stack at the bottom. More on this later.
The f64.add
instruction pops the last two items off the stack, leaving the single 104 item,
adds them together and then pushes the result onto the top of the stack.
6.14 | Top of Stack |
104 | Bottom of Stack |
These two adding steps have left us with a stack containing two items. These are the results we wanted to return. We have performed the steps in an order that leaves the results on the stack in the way that matches. We have used the stack like a temporary store, saving us from storing the results in local variables.
The last two items on the stack become our returning result.
The $addThree
function has finished and control moves back to the calling function.
After calling the function, you would pop the results off the stack and use them in some other way.
The stack is incredibly useful for setting parameter values, returning results, and storing temporary values. Working out how to use it well can improve the overall efficiency of your code.
To use your WAT functions in JavaScript they first need to be exported. This marks the function as exportable and gives it a name. Below is the first example of how this is done.
(func $add (param $first i32) (param $second i32) (result i32) ;; Add first and second parameters together and return result local.get $first local.get $second i32.add ) (export "add" (func $add))
The add
function is just the same as before.
We have added an extra line at the bottom.
This states that we want to export the function $add
using the name “add”.
This is an S-Expression which is formatted as follows.
(export <name> (func <point to function to use>))
We have the keyword export
followed by the name of the export function.
This will be used later within JavaScript to point to the function we want to call.
The second S-Expression is a way of pointing to the function that it links to.
This must point to an existing internal function you have created somewhere else within the WAT file.
(func (export "add") (param $first i32) (param $second i32) (result i32) ;; Add first and second parameters together and return result local.get $first local.get $second i32.add )
In this second example we are embedding the export statement inside the function object.
You will also notice that we have left out the name of the function.
This is because we are only calling the function from outside (with JavaScript) so we can remove the internal name of the function, which was $add
.
Both methods will set up the WASM file so that JavaScript will be able to see and call those functions. Now let's see how we configure and use the function within JavaScript.
// Load in and create instance of add.wasm file const wasm = await WebAssembly.instantiateStreaming(fetch('add.wasm')); // Call the exported WASM add function const result = wasm.instance.exports.add(42, 101);
The first part is to load the WASM file and create an instance of it.
Inside the instance there is an object called exports
which contains everything we have within the WASM file that has been marked as exportable.
This includes our add
function, which we can call using any number values we want.
The returned result is a single number object. You can use it just like any other JavaScript number.
If the function returns more than one result item, then we need to handle things a little differently. Take a look at the following example WAT function.
(func (export "addThree") (param $integer i32) (param $float f64) (result i32 f64) ;; Add 3 to $integer local.get $integer i32.const 3 i32.add ;; Add 3 to $float local.get $float f64.const 3.0 f64.add )
In this example we are exporting the addThree
function.
It takes in two parameters, adds 3 to them and then returns the result for them both.
It is returning two results, one an integer and the other a floating point number.
When calling this function in JavaScript, instead of returning a single number object, it returns an array of result values.
// Call the exported WASM add function const result = wasm.instance.exports.addThree(101, 3.14); // Log result console.log(result[0]); // 104 = i32 console.log(result[1]); // 6.14 = f64
The array items match the order of the return results listed in the function.
So the first item matches up with the i32
type (the first result listed), and the second item matches up with the f64
type (the second result listed).
Not only can you call functions inside WASM from outside in JavaScript, but you can also call outside functions within your WAT code.
You can import and use a JavaScript function from within WASM, just like any other function, using the call
instruction with parameters.
The first step in doing this is to create the function in JavaScript that you want to import and when loading the WASM data to create the instance, pointing to the imported function.
const logInteger = function (integer32) { console.log(integer32); }; const options = { import: { logInteger: logInteger, } }; const wasm = await WebAssembly.instantiateStreaming( fetch('functions.wasm'), options);
Here we have the logInteger
function we are going to import.
It has a normal JavaScript parameter that could be anything, and we just log it.
We have called it an integer but on the JavaScript side we cannot dictate what the parameter or return types are, that is controlled within your WAT code.
We now need to create an options
object that will contain all the extra information we want to pass on to the WASM instance when it is created.
In this case we have created a property called "import", which contains the function link to our logInteger
function.
You can call these properties whatever you want, you do not need to use the word “import”.
We now create the WASM instance like we have done before, but this time you need to pass the options
object as a parameter.
This makes sure all the import functions you have configured match those listed inside the WASM file.
The next step is to set up the WAT code. We need to state which functions we want importing and what their parameters and return results are (with types).
(import "import" "logInteger" (func $logInteger (param i32)))
To define an imported function you use the S-Expression with the keyword import. It is formatted as follows.
(import <options.property> <function name> (func <internal function identifier> <parameters> <result>) )
Let's break this down a little.
The first part links to the properties inside the options
object we passed to the instantiateStreaming
function, which in our example we called import
.
It doesn't need to be called import, you can use any word.
This is followed by the name of the function we are importing, which we have as logInteger
.
The final part is another S-Expression detailing the function prototype, which uses the same format as declaring a new function.
You must give it an identifier name and any parameters (with types) and a return result (if one is used).
This function prototype dictates what parameters and return result (with their types) the function will use in JavaScript. You need to set up your JavaScript function to match the same list of parameters and return result.
You can only use the types i32
, i64
, f32
and f64
with imported and exported parameters and return results.
These need to get translated between JavaScript and when doing so you need to take some extra steps.
f32
Take a look at the following example, where we explore what happens when we pass different values to the parameters.
(import "import" "logInteger" (func $logInteger (param i32))) (func (export "log101") ;; Add the value 101 to the stack i32.const 101 ;; The last item on the stack is the parameter send to the ;; logInteger function call $logInteger )
The first example is to log an integer value by passing an i32
value to the imported $logInteger
function.
The function takes a single i32 parameter, which means we can only send values of that type.
If we try to send something else then the wat2wasm
tool will give us an error when we compile the WAT file.
Here we push the constant value 101 onto the stack and then call the $logInteger
function.
const options = { import: { logInteger: logInteger, logAll: logAll, getRandom: getRandom, getExtra: getExtra } }; const wasm = await WebAssembly.instantiateStreaming( fetch('functions.wasm'), options); wasm.instance.exports.log101();
There are some more functions that get imported.
For now though we are only looking at the logInteger
function.
In JavaScript we call the export function log101
, which will then call the WASM function $logInteger
, which then calls the JavaScript logInteger
function,
which finally logs the integer parameter value 101.
It's a round trip, but it explores what is going on.
The next example does the same sort of thing but this time it does it with 4 different types.
(import "import" "logAll" (func $logAll (param i32 i64 f32 f64))) (func (export "logAll") ;; Push the values onto the stack i32.const 101 i64.const 42 f32.const 12.34 f64.const 3.14159265359 ;; The last items on the stack are the parameters send to ;; the logAll function call $logAll )
The imported logAll
function takes 4 parameters of different types.
We use this to see what happens to the data, of the different types, when it gets translated moving between WASM and JavaScript.
At this point we just push the parameter values onto the stack.
Again, the types must match those used when declaring the function and be pushed onto the stack in the correct order.
wasm.instance.exports.logAll();
Calling the function from JavaScript is straight forward.
The next example explores how an imported function can be used to return a value. Instead of just sending information from WASM to JavaScript, we can ask it to return some data. The import function can return a value and it will be pushed onto the stack inside WASM for it to use in whatever way it wants.
const getRandom = function () { // Return random value back into WASM return Math.floor(Math.random() * 100); };
The function getRandom
simply returns a random integer number between 0 and 99.
(import "import" "getRandom" (func $getRandom (result i32))) (func (export "logRandomNumber") ;; Call function to get random number call $getRandom ;; The value returned from the function is on the stack. It is ;; now the parameter we are sending to the logInteger function call $logInteger )
The import statement does not take any parameters but it does have a single i32
return result.
This means after calling the function there should be a returned result on the stack waiting to be processed.
The logRandomNumber
function calls the $getRandom
function which pushes the returned random number onto the stack,
which is then used as the parameter to the $logInteger
function.
wasm.instance.exports.logRandomNumber();
Calling the function from JavaScript is again simple to do.
We can extend this idea to returning more than just one value. In the next example we are returning 4 different data types.
(import "import" "getExtra" (func $getExtra (result i32 i64 f32 f64))) (func (export "logExtra") ;; Call function to get random number call $getExtra ;; The values returned from the function are on the stack. It is ;; now the parameter we are sending to the logAll function call $logAll )
The getExtra
function is set up to have 4 different return result types.
We call this function from within logExtra
and then send all the returned results, which are on the stack,
onto the logAll
function, which ends up being logged as before.
const getExtra = function () { // Return random values back into WASM return [ Math.floor(Math.random() * 100), BigInt(Math.floor(Math.random() * 1000)), Math.random() * 10000, Math.random() * 100000 ]; };
In JavaScript we are returning an array of values that match the order given in the WAT function prototype.
The first array item lines up with type i32
, then i64
, f32
and finally f64
.
When using i64
data, you cannot use normal numbers in JavaScript, you have to use the BigInt
object.
wasm.instance.exports.logExtra();
Calling the JavaScript function again is simple to do.
Lets see what happens when we send parameters to an exported function. We want to see what the data looks like after it passes through from JavaScript into WASM.
(func (export "logParameters") (param i32 i64 f32 f64) ;; Add parameters and call function local.get 0 local.get 1 local.get 2 local.get 3 call $logAll )
We are using the logParameters
function to pass the parameter data onto the import function $logAll
.
We are going to send different values to see how they are translated into WASM.
wasm.instance.exports.logParameters(909, BigInt(1334), 56.78, 4.22); output: logAll 909, 1334, 56.779998779296875, 4.22
The first example shows what should happen if all the parameters are given as expected.
You will notice that the f64
value looks longer, but this is just how 56.78 exists as a 64 bit floating point number.
Also, remember, i64
data can only work with BigInt
objects.
wasm.instance.exports.logParameters( undefined, BigInt(0), undefined, undefined); output: logAll 0, 0, NaN, NaN
The second example looks to see what happens if you send undefined
for each parameter.
When using the i64
type you need to use the BigInt
object, and as a result it will not allow you to use an undefined
value.
If you do then it will throw an exception.
You can see that the i32
parameter is automatically converted into a zero value.
However, both the f32
and f64
parameters become NaN
(not a number).
wasm.instance.exports.logParameters( 3.14159265359, BigInt(0), 3.14159265359, 3.14159265359); output: logAll 3, 0, 3.1415927410125732, 3.14159265359
The third example is looking at float point precision.
Apart from the i64
parameter, we are using the same Pi value for each parameter.
You will notice that the i32
value has its decimal points removed.
You will also notice that both the floating point numbers are not the same.
The f64
value matches the value we used in the parameter, but the f32
has lost some of its precision, though not by much.
This is because of the precision lost when converting the JavaScript number value (normally stored as a 64 bit floating point number)
into the f32
value (a 32 bit floating point number).
wasm.instance.exports.logParameters(3.75, BigInt(1), 0, 0); wasm.instance.exports.logParameters(-3.75, BigInt(-1), 0, 0); output: logAll 3, 1, 0, 0 logAll -3, -1, 0, 0
In the fourth example we want to see if the i32
number is rounded or truncated.
If the value was rounded, either up or down, then the result would have been different to the one shown.
What is happening here, is that the decimal parts of the number are removed when it is converted into an integer.
wasm.instance.exports.logParameters(4294967295, BigInt(0), 0, 0); output: logAll -1, 0, 0, 0
In the fifth example we are showing the limits involved with numbers when converting them.
In WAT the i32
integer can be seen as either signed or unsigned.
When the number is going to be converted from JavaScript it first removes any decimal points, and converts it into a 32 bit number,
so the value 4294967295 becomes 0xFFFFFFFF (all bits are 1), and then this gets converted into an i32 type.
However, when logging the number, when transferring it from an i32
number into a JavaScript number, it sees the i32
integer as being signed,
and therefore logs -1 instead of 4294967295.
Calling an export function from within WASM can be very useful, but there is one limitation you need to know about. Let's look at an example first.
(import "import" "logThis" (func $logThis)) (func (export "callLogThis") ;; Call function call $logThis )
Here we are importing a logThis
function that exists inside JavaScript.
We also have a function we export that calls the function.
It is a simple way of getting WASM to call an outside function.
const logThis = function () { // Log the this value window.customLogEvent('lotThis ' + this); }; wasm.instance.exports.callLogThis();
This is the JavaScript side of the example.
The logThis
function will be called by a WASM function, which will log the this
object and anything it contains.
When you run the example you will see that the this
object does not exist.
Unlike normal JavaScript functions and events, exported WASM functions does not have a this
object that we can use.
This can be a problem if you have more than one instance of a WASM object.
You will never know which WASM instance has called the export function.