How to use WebAssembly in MicroStudio
I will explain the steps to get the initial setup for WebAssembly working. I will also show how to set up shared memory between the .wasm program and microstudio. This will allow us to share arrays between C and microscript.
I found a lot of useful information about webAssembly in this article. It also goes into more depth than I will in terms of memory management. You can also check out the related MDN resources.
Simple function in C
Step 1: writing the function in C
There are several languages that can be compiled to WebAssembly. In this article, I will be using C.
The first step to import a function into MicroStudio is to write it. We will start with a simple add() function:
addition.c
int add(int a, int b) {
return a + b;
}
Next, we need to compile our addition.c
file into addition.wasm
.
To do this we will use clang.
clang addition.c \
--target=wasm32-unknown-unknown-wasm \
--optimize=3 \
-nostdlib \
-Wl,--export-all \
-Wl,--no-entry \
-Wl,--allow-undefined \
--output addition.wasm
If you want to know more about the different flags, they are specified in the article I linked at the top.
Step 2: Importing the function
We now need to create our project in MicroStudio. Click on the (+) icon at the bottom of the left pane and select Assets. We can now drag and drop the addition.wasm file we have just compiled into MicroStudio.
Go in the code editor and import the asset with the asset manager:
init = function()
loader = asset_manager.wasmInstance("addition", function(instance)
//microscript_fn = instance.export.C_fn
add = instance.exports.add
end)
end
As you can notice, Microstudio assets do not have file extentions in their name. That means that if we try to import addition.wasm
, Microstudio won't find our file. We need to import addition
instead.
We can check that our asset has loaded successsfully by calling loader
in the console, and from there we can call our C function with add(1, 2)
.
If all went well you should see
> add(1, 2)
3
We have just executed wasm code from the MicroStudio console, you can call this function in your code in the same way.
Shared memory
The method shown above will work if the function uses numbers or characters, but if we want to use strings and arrays we need to use shared memory. After importing our wasm instance, we can see by typing loader.instance.exports
into the console that we have an object called memory. We will use this object to have a buffer that we can access from both wasm and microstudio.
Let's write a function that returns the sum of two arrays
Here is what the C code looks like:
addArrays.c
void addArraysInt32(int *array1, int *array2, int *result, int length) {
for (int i = 0; i < length; ++i) {
result[i] = array1[i] + array2[i];
}
}
Here we do not have access to memory management in C, so we have passed in a third array called result
into which we will store the result.
We need to use javascript to interact with the buffer. We will create a new file with a few functions:
//javascript
//global variables
let offset = 0;
let memory = null;
this.jsStoreMemory = function(mem) {
memory = mem;
}
this.createArray = function(list) {
// Create an array that can be passed to the WebAssembly instance.
let array_length = list.length;
const array = new Int32Array(memory.buffer, offset, array_length);
// We offset by the size of the array
offset += array_length * Int32Array.BYTES_PER_ELEMENT;
for (let i = 0; i < array_length; i++) {
array[i] = list[i];
}
return array;
}
And finally, the microscript code to run all this:
init = function()
loader = asset_manager.wasmInstance("addArrays", function(instance)
//microscript_fn = instance.exports.C_fn
addArraysInt32 = instance.exports.addArraysInt32
memory = instance.exports.memory
// storing the memory object in javascript
jsStoreMemory(memory)
// those arrays can be manipulated from microscript and C
array1 = createArray([3, 1, 21])
array2 = createArray([1, 1, 20])
arrayRes = createArray([0, 0, 0])
//to send a pointer to a C function we can use byteOffset
addArraysInt32(array1.byteOffset,array2.byteOffset, arrayRes.byteOffset, array1.length)
// the result is stored in arrayRes
// We can directly use the array
arrayRes[2] += 1
print(arrayRes)
end)
end
Here is what this code does, step by step:
- Load the wasm instance
- Store the function and the memory
- Send the memory to javascript so that we don't need to pass it everytime we call a funtion
- Create shared arrays on the buffer
- Call a C function and passes it the arrays in the form of pointers
- The C function writes the result in one of these arrays
- Since the memory is shared, the changes also apply in Microscript
Extending the buffer
Lastly, we'll see how to increase the size of the buffer. By default, our memory weights 128kB. We can increase it by blocks of 64kB.
> print(memory.buffer.byteLength/1024+' kB')
128 kB
> memory.grow(2) // 2 * 64kB
2
> print(memory.buffer.byteLength/1024+' kB')
256 kB
Keep in mind that every call to grow will detach any references to the old buffer, even for grow(0), as detailed here!