Get Started with Wasm

Wasm allows us to run compiled code in the browser (C, C++, Rust, Go, etc.). As handy as Javascript is, it does have its limitations. As the web grows people will want better functionality and an ability to run native applications within their browsers (think editing software, games, etc.). WASM, coupled with JS, gives us options. But how do we get this going?

The below examples will give you a good path forward, as well as a flavor of what WASM development is like. Keep in mind, however, that as of now the WASM specs only define four types (two integers and two floats), so we're in very new territory which means limited capability. Still, it's an interesting experiment and one that will no doubt mature and provide much-needed options to the ever-evolving web.

Enjoy.

C


  1. Download, compile, and source the Emscripten SDK

    Emscripten is an LLVM to Javascript (asm.js) compiler. It allows us to write compiled code and run it in the browser. You'll also need git, cmake, and g++ installed in your OS.

    • Do these next steps in order (be prepared to wait, this takes a nauseatingly long time)

      • git clone https://github.com/juj/emsdk.git

      • cd emsdk/

      • ./emsdk install sdk-incoming-64bit binaryen-master-64bit

      • ./emsdk activate sdk-incoming-64bit binaryen-master-64bit

      • source ./emsdk_env.sh (this enables emscripten)

      • emcc -v should give output if the above steps succeeded

    • For those last two steps, you might want to run emsdk_env.sh from the directory where your code is actually going to be compiled and change the path of your ./emsdk_env.sh as necessary. For instance if you cloned it into /home/me/experiments/emsdk, but your compilation will take place in /home/me/code/c_code/, running source /home/me/experiments/emsdk_env.sh (from your c_code/ dir) would be required.

  2. Create your C source file:

    // my-file.c
    #include 
    #include  // This will be provided by emsdk
    
    int main(int argc, char ** argv) {
        printf("Hello, WASM!\n");
    }
    
    int EMSCRIPTEN_KEEPALIVE plus_sixteen(int some_number) {
      return some_number + 16;
    }
  3. emcc my-file.c -o output.js -s WASM=1 -O3

    output.js (which you can name whatever you want) is a javascript file automatically generated by emsdk. -O3 is a compiler flag to optimize the javascript output, with 3 being the highest. Check the empscripten docs for more information.

  4. Create a generic HTML file that loads your javascript output file, e.g.

    <html>
      <head>
        <script type="text/javascript" src="output.js"></script>
      </head>
      <body>
        <h1>Hello, WASM.</h1>
        <div id="wasm_button">
          <button>Click me for WASM output</button>
        </div>
        <script>
          var clickButton = document.getElementById('wasm_button');
          clickButton.addEventListener('click', function() {
            var plusSixteenResult = _plus_sixteen(16); // 32
            alert(plusSixteenResult);
          });
        </script>
      </body>
    </html>
    
  5. Serve your HTML file. You can do this with either Node's http-server or python's SimpleHTTPServer, or whatever else gets the job done.

    At this point, you should see your wasm output in the console. Congrats!

Important Points to Note:

  • The main function is always built into the Javascript file, but other functions must be prefaced with EMSCRIPTEN_KEEPALIVE to prevent removal by the compiler (it'll think it's dead code).
  • Notice how we called the C function (plus_sixteen) in the HTML file as _plus_sixteen. This is something that emscripten gives us for free, the ability to call C-declared functions by underscoring their original names. Behind the scenes emscripten is using the Module API and wrapping its true call. Specifically, it's making our lives easier by allowing _plus_sixteen(16) to be a wrapper for

    Module.ccall('plus_sixteen', 'number', ['number'], [16]);

 where the `ccall` signature is the function name, return type, argument type, and argument (respectively).

Take a moment to digest what just happened here. C code was compiled and called directly from a Javascript file to run a calculation...in the browser without a network call (ignoring the call to download the WASM file itself). While this specific calculation was laughably small, imagine the potential to run more computationally-expensive calculations. This is where WASM really shows its utility.

C++


This is very similar to the C example, but with a few changes. Reference the C example as noted.

  1. Same as the C example's first step

  2. Create your C++ source file:

    // my-file.cpp
    #include 
    #include  // This will be provided by emsdk
    
    extern "C" { 	// This is necessary for C++, not so for C
      int plus_sixteen(int some_number) {
        return some_number + 16;
      }
    }
    
    int main() {
      std::cout << "Hello, WASM!\n";
    }

    Any function (with the exception of main) that you may want to export will need to go into the extern "C" block

  3. emcc my-file.cpp -o output.js -s WASM=1 -s EXPORTED_FUNCTIONS="['_plus_sixteen', '_main']"

    Notice the explicit exported functions (with the underscore) in the compiler options. This is different from the C example where it's being called out beforehand as well as exporting main, which is not exported automatically. Interestingly, if we leave out the EXPORTEDFUNCTIONS flag altogether main _does export, but _plus_sixteen's invocation will throw an error.

  4. Create some generic HTML file that loads your javascript output file. This is the same as the C example's fourth step.

  5. Serve your HTML file. See the C example.

Rust


  1. Get Rust on your machine

    • curl https://sh.rustup.rs -sSf | sh
    • Install the nightly distribution of Rust: rustup install nightly && rustup default nightly
    • Install the appropritate target: rustup target add wasm32-unknown-unknown
  2. Make some fun Rust file (see a Rust tutorial if you're not familiar enough with the language):

    ```

    [no_mangle]

    pub extern "C" fn plus_sixteen(x: i32) -> i32 {
        16 + x
    }
    
    fn main() {} // We need this here or the compiler will complain
    ```
  3. rustc +nightly --target wasm32-unknown-unknown <your-file.rs> -o <your-output.wasm>

  4. This is an optional step. It will make a smaller copy of your wasm output file:

    - `cargo install wasm-gc`  // Assuming you've setup Rust's `cargo` package management tool
    
    - `wasm-gc  `
  5. Get the wasm over to your browser by creating a simple HTML/JS file:

       <html>
         <head>
           <script type="text/javascript">
             WebAssembly.instantiateStreaming(fetch('your-output-smaller.wasm'), {}) // You could use either wasm file
             .then(obj => {
               alert('WASM output: ' + obj.instance.exports.plus_sixteen(16).toString()) // 32
             });
           </script>
         </head>
       </html>
    

    You can serve this with either python -m SimpleHTTPServer or node's http-server and then visiting the appropriate port on localhost. These packages will need to be installed if they aren't already on your machine.

If you experiment with this example a little bit and try to get the main function to println!("Hello, WASM!\n"), you'll quickly notice that it doesn't do what the C/C++ examples do. In that, it doesn't run automatically, nor does it actually print the string to your console if you call it from the export. Herein lies a limitation with the current Rust/WASM environment...strings. There are different ways to go about compiling Rust to WASM (see here) and they are also worth exploring.

Go (highly experimental)


WASM is not supported in Go as of 5/15/2018, but there is a long-running discussion to get support within the language (see here) and from reading through the thread it looks like it might show up in 1.11. In any case the Golang community certainly seems to want WASM support, so expect it in the future.

That being said, the below example is markedly limited in comparison to the above three. We can only compile the output WASM file via a javascript file (wasm_exec.js) located in the ./experimental/go directory. This is more of a proof-of-concept than anything else.

  1. Grab an experimental binary by cloning this repo (git checkout the wasm-wip branch)

  2. Create your Go source file in the newly-cloned repo:

    // main.go
    package main
    
    func main() {
      println("Hello, WASM!")
    }
  3. Set the target operating system, the target architecture, and then build from the experimental binary:

    This should be run from the root of the experiment branch you cloned if you want to follow this example to the letter.

    • GOOS=js GOARCH=wasm ./bin/go build -o output.wasm main.go

      • Notice the ./bin/go as opposed to simply go. We don't want to try this with our global ("normal") binary. The world won't end or anything, but you'll get an error, your blood pressure will go up, it'll probably lower your threshold towards argumentation and you'll end up in a spitting match with your SO over shutting the refrigerator door too loudly. So, remember to do ./bin/go
  4. Create an HTML file or copy this one (I shamelessly took it from the experimental branch):

    <!doctype html>
    <!--
    Copyright 2018 The Go Authors. All rights reserved.
    Use of this source code is governed by a BSD-style
    license that can be found in the LICENSE file.
    -->
    <html>
    <head>
    <meta charset="utf-8">
    <title>Go wasm</title>
    </head>
    <body>
        <script src="./experimental/go/wasm_exec.js"></script> <!-- this path was changed as we're looking to run our binary -->
        <script>
    	    async function loadAndCompile() {
    	    	let resp = await fetch("./output.wasm");
    		let bytes = await resp.arrayBuffer();
    		await go.compile(bytes);
    		document.getElementById("runButton").disabled = false;
    	    }
    	    loadAndCompile();
        </script>
        <button onClick="console.clear(); go.run();" id="runButton" disabled>Run</button>
    </body>
    </html>
    

The above build step will give you a WASM output file that will be quite large. Obviously this isn't ideal, but keep in mind we're in an experimental (and unoptimized) environment and one assumes that once this gets brought into Go it would (at least) be comparable to Rust's WASM output.


Hopefully, one of these examples was easy-enough and fun for you. There’s no doubt that WebAssembly will become a larger facet of the web world, but right now we’re in uncharted waters with loose specifications (which means multiple ways to do even the simplest of things). Yet, even in this nascent phase there are still potential performance gains for web applications that depend on expensive numeric calculations.

Happy hacking!

comment

Comments