dev-resources.site
for different kinds of informations.
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
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"
}
-
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/*
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
}
- 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"
}
}
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;
}
- 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>
- 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.';
});
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
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;
}
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
Breakdown of the Command
emcc
: This is the Emscripten C/C++ compiler command. It compiles C/C++ source files into WebAssembly or asm.js.test.c
: This specifies the input C source file that you want to compile.-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.-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.-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.--no-entry
: This option tells the compiler that there is no entry point (like amain()
function) in the program. This is useful for libraries or modules that are intended to be used by other code rather than executed directly.-o test.wasm
: This specifies the output file name for the compiled WebAssembly module. In this case, it will create a file namedtest.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>
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.';
});
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
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
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))
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
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.
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.
Featured ones: