Creating a Library
In the previous section we saw how to compile a simple executable and run it in the browser. There are two shortcomings of this example. First, it would be great if we could use Rust's awesome package manager Cargo instead of calling rustc
directly. Secondly, how do we go from a standalone executable to a library which can be called by JavaScript? Let's get started addressing these two concerns.
First, create a new Rust project using Cargo.
$ cargo new hello_world --bin
You may think it odd that we are passing the --bin
flag to Cargo indicating that we want an executable opposed to a library. Currently, due to this issue, the asm.js and WebAssembly targets do not behave correctly when compiling a library. This is an inconvenience of working with developing technologies, but we can work around it easily enough.
This creates a new project with a main.rs. Replace the contents of main.rs with the following:
#[no_mangle]
pub extern "C" fn testing() {
println!("Testing!");
}
fn main() {}
You will notice we need to keep the main
function even though it is empty. Because this is an executable project, Cargo will complain if there is no main
. Secondly, we must tell the Rust compiler to use the "C" ABI for our foreign function interface (FFI) and to not mangle the function name (for more on FFI in Rust see the book ).
Next we must add a .cargo/config file to our project in order to set appropriate flags when compiling to the asm.js target.
[target.asmjs-unknown-emscripten]
rustflags = [
"-Clink-args=-s EXPORTED_FUNCTIONS=['_testing']",
]
This tells Cargo that when targeting asm.js to pass in the following linker argument:
-s EXPORTED_FUNCTIONS=['_testing']
- this is a list of all the functions we would like to be part of our libraries external API. Here we only have one function calledtesting
. Notice the '_'. This is necessary when telling Emscripten what functions to export, but will not be necessary when calling the function.
This is a great time to mention how indispensable the Emscripten docs are. I would highly recommend diving into these docs, but the biggest take away is that all interaction with the resulting asm.js code is done through a Module object.
Now we can build the project using cargo.
$ cargo build --target asmjs-unknown-emscripten
This will produce a JavaScript file located at target/asmjs-unknown-emscripten/debug/hello_world.js. This is our library.
Lets create a simple HTML file that includes this library. Create an index.html file in the top level of the project and paste the following:
<!DOCTYPE HTML>
<html>
<head>
<script src="target/asmjs-unknown-emscripten/debug/hello_world.js"></script>
</head>
</html>
Now open the index.html file in a browser and we can interact with the asm.js Module
. Open up the developer console of the browser and play with the following commands:
> console.log(Module);
> const testing = Module.cwrap("testing", "", []);
> testing();
The Module
object should be available in the top level. We use the Module.cwrap
function to bind an asm.js function to a JavaScript variable. This is how we expose our libraries API. The Module.cwrap
function takes three arguments. The name of the function we want to wrap, the return type of the function, and an array of parameter types to the function. In this case, the function name is testing
, the function does not return anything, and it takes no argument. Now testing
can be called directly from JavaScript. The Emscripten book provides a whole section on interacting with code.