DEV Community

Cover image for Introduction to WebAssembly (WASM)
Piyush Chauhan
Piyush Chauhan

Posted on

Introduction to WebAssembly (WASM)

WebAssembly (WASM) is a binary instruction format for a stack-based virtual machine, designed as a portable target for high-performance applications. In this article, we'll explore how to compile a simple C program to WebAssembly, load it into a web browser, and interact with it using JavaScript. We'll also explore some useful tools and commands for working with WASM outside the dev container environment.


Setting Up the Development Environment

Create the necessary folder structure and files for your WebAssembly project.

Create Project Folder:

Begin by creating a new directory for your project. Inside this folder, you'll add the necessary files and configurations.

mkdir wasm-web-example
cd wasm-web-example
Enter fullscreen mode Exit fullscreen mode

Set Up Dev Container:

In the wasm-web-example directory, create the .devcontainer folder to store the dev container configuration files. These files will set up a container with Emscripten installed to compile C code into WebAssembly.

Inside the .devcontainer folder, create the following files:

  • devcontainer.json: The devcontainer.json file configures VSCode to use the Docker container with the necessary extensions and environment settings.
{
    "name": "Emscripten DevContainer",
    "build": {
        "dockerfile": "Dockerfile"
    },
    "customizations": {
        "vscode": {
            "settings": {
                "terminal.integrated.shell.linux": "/bin/bash",
                "C_Cpp.default.configurationProvider": "ms-vscode.cmake-tools",
                "C_Cpp.default.intelliSenseMode": "gcc-x64"
            },
            "extensions": [
                "ms-vscode.cpptools",
                "ms-vscode.cmake-tools"
            ]
        }
    },
    "postCreateCommand": "emcc --version"
}
Enter fullscreen mode Exit fullscreen mode
  • Dockerfile: The Dockerfile will set up the Emscripten environment. Here's the content for that file:
# Use the official Emscripten image
FROM emscripten/emsdk:3.1.74

# Set the working directory
WORKDIR /workspace

# Copy the source code into the container
COPY . .

# Install any additional packages if necessary (optional)
# Ensure to clean up cache to minimize image size
RUN apt-get update && \
    apt-get install -y build-essential && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*
Enter fullscreen mode Exit fullscreen mode

Create VSCode Settings:

In the root of your project, create a .vscode folder with the following files:

  • c_cpp_properties.json: This file configures the C++ IntelliSense and include paths for your project.
{
    "configurations": [
        {
        "name": "Linux",
        "includePath": [
            "${workspaceFolder}/**",
            "/emsdk/upstream/emscripten/system/include"
        ],
        "defines": [],
        "compilerPath": "/usr/bin/gcc",
        "cStandard": "c17",
        "cppStandard": "gnu++17",
        "configurationProvider": "ms-vscode.cmake-tools"
        }
    ],
    "version": 4
}
Enter fullscreen mode Exit fullscreen mode
  • settings.json: This file includes specific VSCode settings for language associations.
{
    "files.associations": {
        "emscripten.h": "c"
    },
    "[javascript]": {
        "editor.defaultFormatter": "vscode.typescript-language-features"
    },
    "[typescript]": {
        "editor.defaultFormatter": "vscode.typescript-language-features"
    },
    "[jsonc]": {
        "editor.defaultFormatter": "vscode.json-language-features"
    },
    "[json]": {
        "editor.defaultFormatter": "vscode.json-language-features"
    },
    "[html]": {
        "editor.defaultFormatter": "vscode.html-language-features"
    }
}
Enter fullscreen mode Exit fullscreen mode

Create C, JavaScript, and HTML Files:

Now, create the following files for your project:

  • test.c: This C file contains the simple function that will be compiled to WebAssembly.
// test.c
int add(int lhs, int rhs) {
    return lhs + rhs;
}
Enter fullscreen mode Exit fullscreen mode
  • test.html: This HTML file will load the WebAssembly module using JavaScript.
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>WebAssembly Example</title>
</head>

<body>
  <h1>WebAssembly Example</h1>
  <div id="output"></div>

  <script src="test.js"></script>
</body>

</html>
Enter fullscreen mode Exit fullscreen mode
  • test.js: This JavaScript file will fetch the WebAssembly module and call the exported function.
// test.js
// Path to the .wasm file
const wasmFile = 'test.wasm';

// Load the WebAssembly module
fetch(wasmFile)
  .then(response => {
    if (!response.ok) {
      throw new Error(`Failed to load ${wasmFile}: ${response.statusText}`);
    }
    return response.arrayBuffer();
  })
  .then(bytes => WebAssembly.instantiate(bytes))
  .then(({ instance }) => {
    // Access exported functions
    const wasmExports = instance.exports;
    console.log({ wasmExports })

    // Example: Call a function exported from the WebAssembly module
    if (wasmExports.add) {
      const result = wasmExports.add(5, 3); // Example function call
      document.getElementById('output').textContent = `Result from WebAssembly: ${result}`;
    } else {
      document.getElementById('output').textContent = 'No "add" function found in the WebAssembly module.';
    }
  })
  .catch(error => {
    console.error('Error loading or running the WebAssembly module:', error);
    document.getElementById('output').textContent = 'Error loading WebAssembly module.';
  });
Enter fullscreen mode Exit fullscreen mode

Now that you've set up all the necessary files and configurations, you can move on to compiling and interacting with WebAssembly.

The project structure looks like following now:

➜  wasm-web-example: tree . -a
.
├── .devcontainer
│   ├── Dockerfile
│   └── devcontainer.json
├── .vscode
│   ├── c_cpp_properties.json
│   └── settings.json
├── test.c
├── test.html
├── test.js
Enter fullscreen mode Exit fullscreen mode

Compiling C Code to WebAssembly Using Emscripten

Basic C Program:

The file test.c contains a simple function add that adds two integers. We will compile this C function into WebAssembly using Emscripten.

// test.c
int add(int lhs, int rhs) {
    return lhs + rhs;
}
Enter fullscreen mode Exit fullscreen mode

Emscripten Command:

Inside the dev container, open the terminal (use cmd+j in VSCode) and run the following Emscripten command to compile the C code to WebAssembly:

emcc test.c -O3 -s STANDALONE_WASM -s EXPORTED_FUNCTIONS='["_add"]' --no-entry -o test.wasm
Enter fullscreen mode Exit fullscreen mode

Webassembly Example Devcontainer

Breakdown of the Command

  1. emcc: This is the Emscripten C/C++ compiler command. It compiles C/C++ source files into WebAssembly or asm.js.

  2. test.c: This specifies the input C source file that you want to compile.

  3. -O3: This flag enables aggressive optimizations during the compilation process. The -O3 optimization level is typically used for performance-critical applications, as it applies various optimization techniques that can significantly improve runtime performance.

  4. -s STANDALONE_WASM: This option instructs Emscripten to generate a standalone WebAssembly module. A standalone WASM module does not depend on any JavaScript glue code and can be executed independently in environments that support WebAssembly.

  5. -s EXPORTED_FUNCTIONS='["_add"]': This flag specifies which functions from the C code should be exported and made available for calling from JavaScript. In this case, the function named _add will be accessible in the resulting WASM module.

  6. --no-entry: This option tells the compiler that there is no entry point (like a main() function) in the program. This is useful for libraries or modules that are intended to be used by other code rather than executed directly.

  7. -o test.wasm: This specifies the output file name for the compiled WebAssembly module. In this case, it will create a file named test.wasm.

This command will generate test.wasm, the WebAssembly binary, and ensure that the add function is exported for use in JavaScript.


Loading and Interacting with WebAssembly in the Browser

HTML Setup:

The file test.html contains a simple HTML page that loads the WebAssembly binary using JavaScript. The JavaScript code in test.js fetches the .wasm file and instantiates it.

<!-- test.html -->
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>WebAssembly Example</title>
</head>

<body>
  <h1>WebAssembly Example</h1>
  <div id="output"></div>

  <script src="test.js"></script>
</body>

</html>
Enter fullscreen mode Exit fullscreen mode

JavaScript Setup:

The JavaScript file test.js loads the test.wasm file and calls the exported add function:

// test.js
// Path to the .wasm file
const wasmFile = 'test.wasm';

// Load the WebAssembly module
fetch(wasmFile)
  .then(response => {
    if (!response.ok) {
      throw new Error(`Failed to load ${wasmFile}: ${response.statusText}`);
    }
    return response.arrayBuffer();
  })
  .then(bytes => WebAssembly.instantiate(bytes))
  .then(({ instance }) => {
    // Access exported functions
    const wasmExports = instance.exports;
    console.log({ wasmExports })

    // Example: Call a function exported from the WebAssembly module
    if (wasmExports.add) {
      const result = wasmExports.add(5, 3); // Example function call
      document.getElementById('output').textContent = `Result from WebAssembly: ${result}`;
    } else {
      document.getElementById('output').textContent = 'No "add" function found in the WebAssembly module.';
    }
  })
  .catch(error => {
    console.error('Error loading or running the WebAssembly module:', error);
    document.getElementById('output').textContent = 'Error loading WebAssembly module.';
  });
Enter fullscreen mode Exit fullscreen mode

This will display the result of the add function in the HTML page when the module is loaded successfully.


Using External Tools on macOS

Outside the dev container, there are several useful commands you can run to work with WebAssembly on your Mac.

Install wabt:

wabt (WebAssembly Binary Toolkit) provides utilities for working with WebAssembly, including converting .wasm files to the human-readable WAT (WebAssembly Text) format. Install it via Homebrew:

brew install wabt
Enter fullscreen mode Exit fullscreen mode

Convert WASM to WAT:

Once wabt is installed, you can use the wasm2wat tool to convert your WebAssembly binary (test.wasm) to the WAT format:

wasm2wat test.wasm
Enter fullscreen mode Exit fullscreen mode

This will output a text representation of the WebAssembly module that you can read and inspect.

(module
  (type (;0;) (func))
  (type (;1;) (func (param i32 i32) (result i32)))
  (type (;2;) (func (param i32)))
  (type (;3;) (func (result i32)))
  (func (;0;) (type 0)
    nop)
  (func (;1;) (type 1) (param i32 i32) (result i32)
    local.get 0
    local.get 1
    i32.add)
  (func (;2;) (type 2) (param i32)
    local.get 0
    global.set 0)
  (func (;3;) (type 3) (result i32)
    global.get 0)
  (table (;0;) 2 2 funcref)
  (memory (;0;) 258 258)
  (global (;0;) (mut i32) (i32.const 66560))
  (export "memory" (memory 0))
  (export "add" (func 1))
  (export "_initialize" (func 0))
  (export "__indirect_function_table" (table 0))
  (export "_emscripten_stack_restore" (func 2))
  (export "emscripten_stack_get_current" (func 3))
  (elem (;0;) (i32.const 1) func 0))
Enter fullscreen mode Exit fullscreen mode

Serve the HTML Page:

To view the HTML page that interacts with the WebAssembly module, you can use Python’s simple HTTP server:

python -m http.server
Enter fullscreen mode Exit fullscreen mode

This command will start a local web server on http://localhost:8000, where you can open index.html in your browser to see the WebAssembly module in action.

Webassembly Example Output


Conclusion

By following the steps outlined in this article, you can set up a development environment to compile C code to WebAssembly, interact with it using JavaScript, and convert the resulting WebAssembly binary to the WAT format for inspection. The use of external tools like wabt and Python’s HTTP server makes it easier to manage and explore WebAssembly modules on your macOS system.

Top comments (1)

Collapse
 
kenneththomsen_ profile image
Kenneth Thomsen

Hey Piyush, really loved this article! I find WAT fascinating and I have done similar things with a different stack 💻

  1. Nodejs -- Express
  2. HTML, CSS, JavaScript And...
  3. WebAssembly text)

(GitHub.com/kenneththomsennmbk/wmen)

I love your add module, it is very succinct in C and looks nearly identical to the WAT text from @battlelinegames that I have on my wmen page.
Alas, I went the other route though; I do wat2wasm with the wabt tool created by Rick Battagline
I personally find it easier to say..

WebAssembly.instantiateStreaming(fetch('addwat.wasm').then((obj) => obj.instance.exports.add(2, 1));

[[the JavaScript]]
Here ^ Found at
[https:]//(developer.mozilla.org/en-US/docs/WebAssembly/JavaScript_interface/instantiateStreaming_static)

In addition WAT doesn't require so much code to build. For further information on that Rick Battagline's 'the Art of WebAssembly...'
*and..*

The WAT below can be found on MDN...

(module
(func (param $lhs i32) (param $rhs i32)
(result i32)
local.get $lhs
local.get $rhs
i32.add))

[[https:]]//(developer.mozilla.org/en-US/docs/WebAssembly/Understanding_the_text_format)

I cannot wait to see all the exciting creations from this emerging technology!

P.S.

-----Expressjs 5 is out

Happy ---

hello world

--- everybody!