Learning WebAssembly #8: Compiling into Wasm

There are plenty of languages Wasm can be compiled from. C, Kotlin, and AssemblyScript are only a few of them.


In the previous part of this series, we introduced WASI, a family of APIs to access system resources. In this part, we will move away from low-level programming in Wat and consider compilation into Wasm from more high-level programming languages like C, Kotlin, or AssemblyScript.

I deliberately did not choose Rust as, together with C/C++, it is a historically obvious choose and one can find plenty of articles about Rust and WebAssembly on the Internet.

I choose C with respect to Emscripten, the first tool for producing WebAssembly, Kotlin for my Java background, and finally AssemblyScript as an obvious choice to develop modern Wasm programs.

C and Emscripten

Emscripten is a complete Open Source compiler toolchain to WebAssembly. It emulates a particular OS system interface, POSIX, on the web. This means that you can use functions from the C standard library (libc).

Emscripten achieves this by providing its own implementation of libc. The implementation has two parts: the Wasm module and JavaScript glue. The JavaScript part is called in the browser or other JavaScript runtime like Node.js, which then talks to the operating system.

Unfortunately, the JavaScript glue interface was not designed to be a standard, which is a problem WASI is trying to solve.

Under the hood, Emscripten complies not only C/C++ code, but any LLVM-based language, like Scala, Crystal, Haskell, Julia, Swift, or Ruby.

All right, let’s write a Hello world in C:

#include <stdio.h>

int main() {
  printf("Hello, world!\n");
  return 0;
}

To compile this C code into Wasm, you can either install Emscripten or simply use the official Docker image:

$ docker run --rm -v $(pwd):/src
  \ emscripten/emsdk 
  \ emcc hello.c -o hello.js

This will generate two files: hello.wasm and hello.js. You can run it in Node.js:

$ node hello.js
Hello, world!

Emscripten can also create an HTML page to run the Wasm module in:

$ docker run --rm -v $(pwd):/src
  \ emscripten/emsdk 
  \ emcc hello.c -o hello.html

Now, you can open hello.html in a browser.

To make the Fetch API work you must serve the web page via HTTP(S).

Emscripten is the oldest compiler tools for Wasm. Nowadays, there many other options you can choose from. However, if you are interested in compiling into Wasm from an LLVM language, Emscripten is a natural choice.

Kotlin

Kotlin is a modern practical language running on JVM. Kotlin can be also compiled into native code, JavaScript, and, obviously, Wasm.

As already mentioned, Kotlin is based on LLVM and LLVM supports WebAssembly, ergo we can generate Wasm files from Kotlin source code.

fun main(args: Array<String>) {
    println("Hello, world!")
}

You have to download (or build) the Kotlin/Native compiler and set Wasm as the compilation target:

$ konanc -target wasm32 hello.kt -o hello

This generates two files: hello.wasm and hello.wasm.js. The last step is to create an HTML page:

<html>
  <body>
    <script wasm="./hello.wasm" 
            src="./hello.wasm.js">
    </script>
  </body>
</html>

Serve the page via HTTP and you should see the result in the dev console:

Hello, world!

In the time of writing, the Wasm support in Kotlin may not be as mature as in other tools. However, if you are a developer coming from a JVM environment, you should definitely keep an eye on Kotlin.

AssemblyScript

AssemblyScript is a free and open source TypeScript-like language made for Wasm. It targets WebAssembly's feature set specifically, giving developers low-level control over their code.

In AssemblyScript, you can access Wasm features directly via low-level built-ins, or you can use a JavaScript-like standard library, making it suitable for non-browser use cases.

We will discuss these features in detail in the following article. For now, let’s create a Hello world program in AssemblyScript:

// assembly/index.ts

declare function log(s: string): void;

export function main(): void {
  log("Hello, world!");
}

Load and execute it from JavaScript code:

// index.js

const fs = require("fs");
const loader = require("@assemblyscript/loader");

const wasm = loader.instantiateSync(
  fs.readFileSync(__dirname + "/build/optimized.wasm"), { 
    "index": {
      log: function(pts) {
        console.log(wasm.exports.__getString(pts));
      } 
    } 
  }
);

wasm.exports.main();  // prints "Hello, world!"

AssemblyScript an obvious and natural choice for developers that come from the JavaScript environment and want to have low-level control over Wasm features.

Further Steps

This time, we have seen three different approaches on how to compile into WebAssembly.

In the next part of this series we will focus on AssemblyScript exclusively.

Stay tuned!