Importing Host Functions for Wasm
In the last post I explained how compiled WebAssembly code could export functions which can then be invoked from the host, essentially using WebAssembly as a safe version of a shared object. But that’s only part of the story, at some point our WebAssembly code is going to want to call the host. Either to obtain a resource like a fopen
function call, or as some sort of notification.
Importing Functions
WebAssembly functions are imported into a module from other modules. This means we need to tell the compiler the name of the module and the name of the function to invoke. Again, there is this badly named c intrinsic we can use:
__attribute__((__import_module__(("module name")), __import_module__(("function name"))))
So, for instance if I wanted to import a function which too the answer to a calculation I could do it like this:
__attribute__((__import_module__(("host")),__import_module__("addanswer"))) void addanswer(int result);
As you can see in the c code I’m defining the module the function came from host
the string name of the function addansewer
and providing the c function signature.
Just like with the last example typing __attribute__((__import_module__(("module name")), __import_module__(("function name"))))
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_IMPORT(A, B) __attribute__((__import_module__((A)), __import_name__((B))))
Now our addanswer
function definition becomes:
WASM_IMPORT("host", "addanswer") void addanswer(int result);
Now, that looks a lot better. Let’s update our add function so that instead of returning the result it calls our new function prototype and gives the result to it. Doing this our (add.c
) code looks like this:
#define WASM_EXPORT(A) __attribute__((export_name(A)))
#define WASM_IMPORT(A, B) __attribute__((__import_module__((A)), __import_name__((B))))
WASM_IMPORT("host", "addanswer") void addanswer(int result);
WASM_EXPORT("add") void add(int a, int b) {
addanswer( a + b );
}
Compiling our Wasm
We can now compile our Wasm using a similar command as before (I added -O3
to optimize the output):
/opt/wasi-sdk/bin/clang -O3 -nostdlib -Wl,--no-entry -o add.wasm ./add.c
This is going to produce a add.wasm
file which is about 233 bytes in size, again 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 that the Wasm module is now importing a function (addanswer
) and invoking it.
(module
(type (;0;) (func (param i32)))
(type (;1;) (func (param i32 i32)))
(import "host" "addanswer" (func $addanswer (type 0)))
(func $add (type 1) (param i32 i32)
local.get 1
local.get 0
i32.add
call $addanswer)
(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 update our WebAssembly host environment to provide the native version of that function.
Invoking a Wasm Function from the Host
Again, 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], I’ll point out the interesting bits below.
Write your callback function
First we’ve got to write our callback function. The function signature is going to be almost identical to the one we defined in Wasm, with just one extra parameter, a wasm_exec_env_t
this is needed if you would like to interact with the WAMR runtime, but we can ignore it for now. So our code will look like this:
void addanswer(wasm_exec_env_t exec_env, int result) {
printf("Hey! - I got the result [%d]\n", result);
}
We’ll get the answer and print it to the screen.
Export your callback function to the module
Now we need to tell the WAMR runtime we have the function and we want to export it. We do this by supplying three new parameters to the WAMR VM:
- The name of the host module, we’ll call this
host
- The number of functions we are exporting, in this case 1
- The function name and signatures of the functions we’re exporting, in this case a function called
addanswer
which take one 32bit int, and returns nothing.
Setting the host name is the easy part:
#define MODULE_NAME "host"
//....
args.native_module_name = MODULE_NAME;
We need to describe the function we are exposing to WAMR in a NativeSymbol
table this is done as follows:
static NativeSymbol native_symbols[] = {
{ "addanswer",
addanswer,
"(i)",
NULL }
};
As you can see the first parameter is the Wasm string name we are using to identify this function ({ "addanswer",
), for now I’ve used the same name as the function itself, but it doesn’t have to be. Then I give the address of the function to invoke (addanswer,
). I provide a string which explains to Wamr what the function signature should be, "(i)",
this uses a set of letters to denote the function signature - you can find a list of these letters here : wasm-micro-runtime/doc/export_native_api.md at main · bytecodealliance/wasm-micro-runtime
Determining the number of functions we’re exposing to the running Wasm code is easy, it’s 1, but being lazy we can do it as follows:
const int numberOfFunctions = sizeof(native_symbols) / sizeof(NativeSymbol);
Now we just need to stick this into our RuntimeInitArgs
structure as follows:
args.n_native_symbols = numberOfFunctions;
args.native_symbols = &native_symbols[0];
If you compile and run this you get the result
Hey! - I got the result [42]
This is how you import and export functions from the host environment to and from Wasm.