I'm trying to run a hello world program made in Rust with WebAssembly, but I'm getting an error message when I try to load the program.
I was able to get it running when following some tutorials that I found, the problem is that they use Emscripten to create JavaScript and HTML for loading the code, but this JavaScript and HTML includes tons and tons of boilerplate and other stuff. I got kinda lost and instead wanted to try to get a really simple example that I'm loading myself.
I ran the following to compile hello.wasm
echo 'fn main() { println!("Hello, Emscripten!"); }' > hello.rs
rustc --target=wasm32-unknown-emscripten hello.rs
To load hello.wasm, I took the example from the Mozilla WebAssembly docs and tried to run that.
var importObject = {
imports: {
imported_func: function(arg) {
console.log(arg);
}
}
};
fetch('hello.wasm').then(response =>
response.arrayBuffer()
).then(bytes =>
WebAssembly.instantiate(bytes, importObject)
).then(results => {
// Do something with the compiled results!
});
WebAssembly.instantiate
crashes:
LinkError: Import #0 module="env" error: module is not an object or function
I found something about this error having to do with something missing, that the boilerplate code is supposed to load, but looking through the autogenerated HTML and JavaScript I couldn't figure out exactly what it could be.
Summary
You have to define a bunch of functions and values which are imported by the WASM module. When the WASM module imports something you don't properly define, you get this linker error. Emscripten generates a whole bunch of JS-code which defines all imports the WASM module needs (which is "easy" in this case, because Emscripten also generates the WASM module itself).
Right now, you either use the Emscripten runtime (the JS file) or you have to do a lot of stuff yourself.
I'll try to explain in more detail, please bear with me:
Assembly and WASM
Assembly is the human-readable form of machine code (but both terms are often used interchangeably, so we won't care in this post either and just call it assembly). Assembly is designed for the machine/the CPU to execute and therefore it's very simple. Assembly is basically a list of instructions where each instruction does a specific, tiny thing. For example, there is an instruction to add two numbers, to execute instructions at a different address, and so on.
Notably missing is a
print
instruction. Something of the functionality ofprint
is a completely different abstraction level and does a lot more than a single instruction. Additionally, what do we mean by "printing"? We expect the our program has access to some kind of console. To repeat the important part: WASM doesn't have aprint
instruction or anything similar!Things like printing need to be provided by an environment. For most programs and for the majority of computer science, this environment is simply the operating system. It manages the "console" and it let's you print. The immediate environment of your WASM program is, however, the browser! So the browser has to offer you a way to print.
Linking
Linking is the process of connecting ("resolving") imports and exports from different modules/compilation units with one another. For example, linking is necessary when you use
extern crate
s in Rust and when you compile multiple.cpp
files in C++.It's also necessary when you instantiate a WASM module, because the module might have imports. And these imports need to be resolved before we can execute the module.
So does your module have imports? Let's have a look! You can use the tool
wasm-dis
(disassembler) to turn the binarywasm
code into a more or less readable assembly code:$ wasm-dis hello.wasm > hello.wast
. Looking at this file, we can see the following:Even without knowing how to read this
wast
format, we can make a reasonable guess and assume that your module does indeed import stuff. We should have known, since the we want to print and there is noprint
instruction!(You might be wondering why there isn't a
(import "env" "print" ...)
. I can't fully explain this, but the reason is basically: it's more complex than that. Emscripten only uses a small set of important imports and uses those imports to access other functions from the environment.)Linking with WASM (and Emscripten)
Linking in WASM is done by the
WebAssembly.instantiate()
method. As you can see in the linked documentation, this method takes animportObject
. Failing to define a function/value in this object, one for each import of the WASM module, results in aWebAssembly.LinkError
. Makes sense.If you want to instantiate the WASM module defined by your file
hello.wasm
, you have to define all 62 of those imports. This seems really annoying, right? Indeed, you aren't really expected to do that: that's why Emscripten generated the necessary JS-code for you! WASM modules generated by Emscripten are supposed to be loaded with the Emscripten-generated JS-loader!Printing in a normal program?
It's worth taking a look at how programs running in a native environment (the operating system) do printing. They surely also need to be linked with the environment (i.e. the operating system), right? Not really.
While programming languages like Rust, C and C++ do have a standard library which is used for printing, this standard library is not part of the operating system. It just uses the operating system itself. In the end, in order to print, a syscall is used. Syscalls use CPU interrupts to call a function of the operating system. This has some advantages (e.g. you don't need to link your program with the operating system), but also some important disadvantages (e.g. it's not very fast).
AFAIK, these kinds of syscalls are not possible with WASM (at least as of now).
Not using Emscripten
Compiling to WASM requires two major things:
Emscripten does both¹ and can match code generation with linking since both parts are done by Emscripten. Are there alternatives?
Yes! What you are looking for is the
wasm32-unknown-unknown
target of Rust. This target uses LLVM's WASM backend to do the code generation. With this target you can generate small WASM modules completely without Emscripten. And what's more: you can also write the JS-loader yourself since you decide about your imports and nothing is magically added.To learn more about this exciting topic, I'd recommend you to visit hellorust.com. On that website you can find simple examples and instructions on how to setup your build environment.
¹ Emscripten doesn't generate WASM directly. It generates asm.js code which is then converted into WASM.