Embedding WebAssembly in Software
In most of the WebAssembly examples I’ve posted so far I’ve written simple, but complete applications, complete with a main
function. But WebAssembly really comes into its own when you use it to embed third party code inside your application.
Exporting Functions
With the WAMR VM (in fairness all Wasm engines) you can load a WebAssembly module and invoke functions on that module. It’s a bit like loading a dll
or a shared object (.so
) but the shared object / dll
is sandboxed. To do this when we compile our WebAssembly application we need to tell the compiler which functions we want to export. This can be done with this clang intrinsic:
__attribute__((export_name("name of function")))
So, for instance if I wanted to export a function which added two numbers together I could do it like this:
__attribute__((export_name("add"))) int add(int a, int b) {
return a + b;
}
Now, one of the really cool things about the export name is it can be any valid string so we could export the function like this:
__attribute__((export_name("the answer to life the universe and everything"))) int add(int a, int b) {
return a + b;
}
Typing __attribute__((export_name("blah")))
in front of every function is a pain in the behind. So instead we can use a macro to make life a bit easier :
#define WASM_EXPORT(A) __attribute__((export_name(A)))
Now our add function becomes:
WASM_EXPORT("add") int add(int a, int b) {
return a + b;
}
Now, that looks a lot better. Our entire shared object now looks a little like this (add.c
) :
#define WASM_EXPORT(A) __attribute__((export_name(A)))
WASM_EXPORT("add") int add(int a, int b) {
return a + b;
}
Compiling our Wasm
Now let’s assume we want to compile our Wasm version of a shared object, when we do we need to tell our compiler that it’s not a full application. On a normal Linux system we’d indicate that the code was shared. For WebAssembly it’s a little different, we compile and tell the compiler to not expect a main function. This can be done with this argument -Wl,--no-entry
. So we compile it like this:
/opt/wasi-sdk/bin/clang -Wl,--no-entry -o add.wasm ./add.c
This is going to produce a add.wasm
file which is about 9.8k in size. That’s huge for such a simple function, and that’s because it is not optimized, and it’s also including the c standard library (printf
, malloc
etc.). We don’t need that library, so we can even remove that with the following -nostdlib
and we can shrink the wasm file to 193 bytes. Note, of course if you don’t want to include some of the standard library functionality, like a printf
you will not be able to use --nostdlib
.
We can check to see what this actually produces by running it through the wasm2wat
tool:
wasm2wat -o add.wat add.wasm
If you peak into the add.wat
you’ll see something which pretty much is exactly what we wrote - a wasm module which exports a function called add, which takes two 32 bit integers adds them together and returns the result. It will look like this:
(module
(type (;0;) (func (param i32 i32) (result i32)))
(func $add (type 0) (param i32 i32) (result i32)
local.get 1
local.get 0
i32.add)
(table (;0;) 1 1 funcref)
(memory (;0;) 2)
(global $__stack_pointer (mut i32) (i32.const 66560))
(export "memory" (memory 0))
(export "add" (func $add)))
Now we just need to invoke that WebAssembly application from our host environment
Invoking a Wasm Function from the Host
Setting up the host environment can be rather lengthy, to save the time the code to do this is available in the wasm_example project I created on github, but the interesting bit is the following function:
bool invoke_add(wasm_module_inst_t module_instance, wasm_exec_env_t exec_env, int a, int b, int* answer) {
const char* WASM_FUNCTION = "add";
printf("look up a function [%s]\n", WASM_FUNCTION);
wasm_function_inst_t func = wasm_runtime_lookup_function(module_instance, WASM_FUNCTION);
if (!func) {
printf("couldn't find function [%s]\n", WASM_FUNCTION);
return false;
}
printf(">function found!\n");
wasm_val_t parameters[] = {
{.kind = WASM_I32
,.of.i32 = a},
{.kind = WASM_I32
,.of.i32 = b}
};
wasm_val_t result[] = {
{.kind = WASM_I32
,.of.i32 = 0}
};
printf(">calling function with parameters(%p)\n", parameters[0].of.ref);
bool executed_ok = wasm_runtime_call_wasm_a(exec_env, func, 1, &result[0], 2, ¶meters[0]);
if (!executed_ok) {
printf("call failed!\n");
return false;
}
(*answer) = result[0].of.i32;
return true;
}
In this function you can see that the name of function is being looked up, then the parameters are packaged up in a wasm_val_t
array of structures, a second array of structures has also been created, and it took has space for a response value, these are then passed to the function, and the function is executed.
bool executed_ok = wasm_runtime_call_wasm_a(exec_env, func, 1, &result[0], 2, ¶meters[0]);
This translates to, call the function pointer func
it will return 1 return value (note that wasm functions could return more than one return value) this should be stored in the &result[0]
address, the function requires 2 arguments and these are located in the array pointed to by ¶meters[0]
. The function will return a boolean value indicating success. If false
then the function failed to execute.