On this page

Functions

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.

Parameters & Results

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.

Calling Functions

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.

Export to JavaScript

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).

Import from JavaScript

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.

Parameter Types

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.

i32

  • Uses the standard number object in JavaScript
  • Any of the decimal parts are removed
  • Does not round up or down
  • An undefined parameter is converted into a zero value
  • Numbers can be negative

i64

  • You need to use the BigInt object in JavaScript
  • You cannot use an undefined object
  • Numbers can be negative

f32/f64

  • Uses the standard number object in JavaScript
  • You will lose some data precision when using f32
  • Numbers can be negative

Take a look at the following example, where we explore what happens when we pass different values to the parameters.

Log 101

(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.

Log All

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.

Log Random

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.

Log Extra

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.

Test Parameters

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.

Export Function & This

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.