WebAssembly for Beginners Part 4: WebAssembly and JavaScript Companionship

Overview

In this post, we will take an in-depth look at using WebAssembly modules with JavaScript to build high-performance web applications.

First, we will learn how JavaScript allows us to load and execute WebAssembly code at runtime. Then we‘ll go through concrete examples of compiling C++ to WebAssembly using Emscripten and calling it from JavaScript.

Finally, we‘ll explore the WebAssembly JavaScript API in detail – including ahead-of-time compilation, instantiating modules, and more. We‘ll also compare WebAssembly and JavaScript to understand their complementary strengths.

Introduction to WebAssembly

WebAssembly (WASM) is a game-changing new standard that allows near-native performance web apps. It provides a portable bytecode format that runs at native speeds across browsers.

Some key benefits of WebAssembly include:

  • Near-native speeds – WebAssembly code runs at ~80% of native speed
  • Safe execution sandbox
  • Portability across browsers and platforms
  • Compact binary format over slow JavaScript parsing
  • Tooling support from C/C++, Rust, AssemblyScript and more

WebAssembly modules cannot directly access the DOM or other web platform APIs. Instead, they need to be invoked from JavaScript.

This companionship between WebAssembly and JavaScript unlocks new possibilities:

  • Web developers can write performance-critical sections in C/C++/Rust and compile to WebAssembly.
  • The rest of the app code can remain JavaScript.
  • Libraries and frameworks can provide WebAssembly powered modules for acceleration.

Next, let‘s see how to load and execute WebAssembly code from JavaScript at runtime.

Using WebAssembly Modules in JavaScript

JavaScript handles all aspects of loading and compiling WebAssembly code in the browser. Here are the key steps:

  1. Fetch the .wasm bytecode using the fetch API or from local storage
  2. Compile the binary code into a WebAssembly.Module instance
  3. Instantiate the module by providing imports
  4. Call exported WebAssembly functions from JavaScript

Consider this example:

fetch(‘app.wasm‘)
  .then(response => response.arrayBuffer())
  .then(bytes => WebAssembly.instantiate(bytes, imports))
  .then(results => {
     results.instance.exports.main(); 
  });

This demonstrates asynchronous loading of the WebAssembly module, followed by instantiation and calling an export.

Key JavaScript APIs for working with WebAssembly

  • WebAssembly.compile(): Ahead-of-Time compilation of modules
  • WebAssembly.instantiate(): Synchronous compilation and instantiation
  • WebAssembly.instantiateStreaming(): Streaming instantiation

Next, let‘s walk through a concrete demo of building and running a WebAssembly module with JavaScript.

Compiling C++ to WebAssembly

For this demo, we‘ll use Emscripten to compile a C++ function to WebAssembly.

First, install the Emscripten SDK:

git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
./emsdk install latest
./emsdk activate latest
source ./emsdk_env.sh

Next, create a simple C++ file called main.cpp:

int add(int x, int y) {
  return x + y;
}

Compile this to WebAssembly with:

emcc main.cpp -o main.js

This generates two files:

  • main.wasm – Contains the WebAssembly module
  • main.js – JavaScript glue code to load and call WASM exports

We can now execute the WebAssembly function from Node.js:

const main = require(‘./main.js‘);
console.log(main._add(10, 20)); // Prints 30

Or in the browser by serving main.js.

This demonstrates invoking our WebAssembly function add() from JavaScript!

The WebAssembly JavaScript API

Now that we‘ve seen a demo, let‘s deep dive into the WebAssembly JavaScript API.

The main purposes of this API are:

  1. Compile WebAssembly bytecode into modules
  2. Instantiate modules by providing imports
  3. Execute code by calling exports

Key API interfaces

  • WebAssembly.Module: Compiled WebAssembly code ready for instantiation. Created from binary code using WebAssembly.compile().
  • WebAssembly.Instance: An instantiated WebAssembly module, ready for execution. Created by providing imports to a WebAssembly.Module.
  • WebAssembly.Memory: Resizable linear memory that can be shared between JS and WASM.
  • WebAssembly.Table: Resizable array of function references. Used to indirectly call functions across JS and WASM.

Ahead-of-Time Compilation

WebAssembly compilation can be performed ahead-of-time using WebAssembly.compile():

// Load the .wasm bytes 
fetch(‘app.wasm‘)
  .then(response => response.arrayBuffer())  

// Compile the module
  .then(bytes => WebAssembly.compile(bytes))     
  .then(module => {
    // module contains WebAssembly.Module ready for instantiation  
  });

This separates compilation from instantiation, allowing compiled modules to be cached.

Instantiating WebAssembly Modules

To instantiate a WebAssembly.Module, we need to provide imports using WebAssembly.instantiate():


// Compiled module
const module = WebAssembly.compile(wasmBytes);  

// Imports (if any)  
const imports = {
  js: {
    global1: 10, 
  }  
};

WebAssembly.instantiate(module, imports)
  .then(instance => {
    // Instantiated instance with exports    
  });  

The imports map WebAssembly functions to JavaScript implementations. Providing this environment allows executing the WebAssembly instance.

Comparing JavaScript and WebAssembly

Finally, let‘s compare JS and WebAssembly to understand their complementary powers:

WebAssembly Advantages

  • Near-native speeds
  • Optimized for ahead-of-time compilation
  • Binary format – Smaller than JavaScript source
  • Safe low-level execution environment
  • System interface support via WASI

JavaScript Advantages

  • Ubiquitous platform and rich ecosystem
  • Rapid development with dynamic types
  • Vast community resources and libraries
  • Full access to web platform APIs
  • Cross-browser portability

The key takeaway is that WebAssembly enhances JavaScript instead of replacing it. Combining both unlocks new possibilities for web developers!

Conclusion

In this article, we took a deep dive into WebAssembly and JavaScript companionship. We learned how JavaScript handles loading, compiling and instantiating WebAssembly modules.

We walked through examples of building a WebAssembly module with Emscripten and calling it from Node.js and browsers. Finally, we explored relevant APIs like WebAssembly.compile(), instantiate(), etc. and compared JS and WASM.

I hope you enjoyed this post! Let me know if you have any other questions.