Compiling C to WebAssembly


Why WebAssembly? For most developers, it's not necessary to learn WebAssembly (wasm) directly. After all, there are more and more tools that compile one language X to WebAssembly.

Common examples today are Go, Kotlin, Rust, etc. The repo Awesome WebAssembly Languages contains a list of languages that currently compile to or have their VMs in WebAssembly.

Front-end developers may take a look at AssemblyScript. It compiles strictly typed TypeScript (basically JavaScript with types) to WebAssembly. For most web developers used to writing JavaScript, it may be an easier option to learn and compile code to WebAssembly.

Over the past 3 months, I've been working on a programming language called ToyLang. It uses a simple parser to build a JavaScript AST evaluated directly by a JavaScript engine. If there is a chance in the future, I'll try a language which complies code to bytecode. Learning WebAssembly text format seems a great way to get familiar with how to handle bytecode.

There are many options to get started with WebAssembly today. The most known project is Emscripten which takes LLVM bitcode - which can be generated from C/C++ - and compiles that into JavaScript. Emscripten now supports compiling to WebAssembly, too.

The most common example is compiling C to WebAssembly. Some online tools even enable this in your browser, such as WasmFiddle.

Compiling C to WebAssembly

Type C code in the top left editor and click “Build”. It compiles C to WebAssembly and shows the text format in the bottom. The WebAssembly text format is what I want to learn primarily. You may type JavaScript in the top right editor if you know how to use WebAssembly JavaScript API. It shows an example about how to invoke the add function. Click “Run” if you want to see the result.

Compiling C to WebAssembly doesn't mean WebAssembly is as fast as C. Making WebAssembly fast is a goal, not the current status. You are executing WebAssembly, not C.

In WasFiddlel, you may click “Wasm” to download a compiled “.wasm“. The default name is “program.wasm”. After downloading, write a html file.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
  </head>
  <body>
    <script>    
    WebAssembly.instantiateStreaming(fetch('program.wasm'))
               .then(prog => {
                   console.log(prog.instance.exports.add(1, 2)); 
               });
    </script>
  </body>
</html>

In the future, browser will support script tag with type = "module" to load a WebAssembly module. Currently, you can use Fetch API (or XMLHttpRequest). One of WebAssembly JavaScript API is WebAssembly.instantiateStreaming. Passing the result of the fetch function to WebAssembly.instantiateStreaming is the simplest way to compile and instantiate a WebAssembly module.

WebAssembly.instantiateStreaming returns Promise. After completing the task, you get an object which contains two properties, module and instance. The module property is an instance of WebAssembly.Module. It contains stateless WebAssembly code that has already been compiled by the browser. The instance property is an instance of WebAssembly.Instance. It contains all the exported WebAssembly functions that allow calling into WebAssembly code from JavaScript.

(WebAssembly.Instance.prototype is not an instance of WebAssembly.Module. In WebAssembly JavaScript API, the instance of WebAssembly.Module only provides the module information while constructing the instance of WebAssembly.Instance.)

In the above example, we only care about the exported add function. It can be obtained through the export property of the instance of WebAssembly.Instance. The example shows 3 in the console. Don't forget a HTTP server because you use Fetch API to fetch .wasm.

You can invoke functions imported from JavaScript. For example, type the folloing code in WasmFiddle.

void log(int n);
int add(int n1, int n2) {
  int result = n1 + n2;
  log(result);
  return result;
}

After clicking “Build”, WasmFiddle generates the WebAssembly text format bellow.

(module
 (type $FUNCSIG$vi (func (param i32)))
 (import "env" "log" (func $log (param i32)))
 (table 0 anyfunc)
 (memory $0 1)
 (export "memory" (memory $0))
 (export "add" (func $add))
 (func $add (; 1 ;) (param $0 i32) (param $1 i32) (result i32)
  (call $log
   (tee_local $1
    (i32.add
     (get_local $1)
     (get_local $0)
    )
   )
  )
  (get_local $1)
 )
)

Currently, you can only have a look at import. The imported function "log" is a property of the imported object "env". The name "env" can be customized. The result of using "env" here is that WasmFiddle uses "env" as the name of the imported object when compiling C to WebAssembly.

After downloading “program.wasm”, create a HTML.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
  </head>
  <body>
    <script>
    const importObj = {
        env: {
            log: n => console.log(n)
        }
    };

    WebAssembly.instantiateStreaming(fetch('program.wasm'), importObj)
               .then(prog => {
                   console.log(prog.instance.exports.add(1, 2)); 
               });
    </script>
  </body>
</html>

After executing the script, the console displays two 3s. One is from the log function in the add function. The other is the returned value of add and written to the console by console.log.

The process of compiling C to WebAssembly helps us to understand a basic process of how to use WebAssembly.

  • Write code in a language
  • Compile to “.wasm“
  • Load “.wasm“ in a browser
  • Use WebAssembly JavaScript API to compile and instantiate the module
  • Call exported WebAssembly functions in JavaScript

In later documents, you'll use WebAssembly text format. Compiling C to WebAssembly, however, is still helpful because you can observe the WebAssembly text format to know corresponding WebAssembly instructions.

Modern browsers, such as Chrome and Firefox, already have some support for WebAssembly debugging. That's one of the results why we need to understand WebAssembly text format.