How to run C++ applications as Webassembly with Javascript
Everything you need to know to build and deploy c++ code as Webassembly on the Web. A step by step tutorial for absolute beginners to get started with Webassembly and its ecosystem. Learn how to compile c++ code to wasm, use it in your javascript projects, and deploy it on the web.
Webassembly codes are a perfect complement to the javascript world. They excel at compute heavy tasks at near native speed on a web page. Javascript runs on a single thread which is responsible for handling user interactions on the page, so its never a good choice to run calculations that take more than 10ms.
Below we will be exploring how to build a simple c++ code to wasm and running it on the browser.
Prerequisites
To follow along with this tutorial, you would need
- C++ basics - able to read and understand basic c++ syntax and compilation steps
- Javascript - advanced This tutorial is intended for JS developers who have experience with javascript and want to explore the wasm in their next projects.
Chapter 1: A simple C++ program
Lets say we have a simple C++ code that calculates the sum from 0..N when N is input, using loops to to demonstrate CPU intensive task.
The below logic is extremely inefficient for production use. Please use the usual arithmetic progression sum formula to calculate the sum. i.e. n * (n + 1) / 2
cppLooper.cpp
#include <iostream>
#include <cstdlib>
#include <cstdint>
std::uint64_t runLoop(std::uint64_t N)
{
std::uint64_t sum = 0;
for (std::uint64_t i = 0; i <= N; ++i)
{
/* Some fake task, just to demonstrate
* a CPU intensive work. */
sum += i;
}
return sum;
}
int main(int argc, char **argv)
{
char *end = nullptr;
const std::uint64_t N = std::strtoull(argv[1], &end, 10);
std::cout << runLoop(N) << std::endl;
return 0;
}
runLoop function takes in an Integer till which we have to calculate the sum. E.g.
Input: 5
Output: 15
Explanation: 0 + 1 + 2 + 3 + 4 + 5 = 15
main is the entry point of our program that asks user for N, calls runLoop function and prints the outputs
Compile and test the program
I will be using Ubuntu in Windows Subsystem Linux (WSL2) for this tutorial, but you can use any unix based OS. I will also be using g++ to compile the code. Assuming you do not have c++ compiler setup on your machine, you can follow along with the commands
# updates the distribution database on your system
$ sudo apt update
# upgrades to latest versions of packages installed on your system
$ sudo apt upgrade
Next we will install the build-essential which will c++ compiler (g++) along with its dependencies. Learn more about build-essential
# install c++ compiler
$ sudo apt install build-essential
Now, lets test if we actually have g++ installed.
# Check if c++ compiler is installed by checking the version of g++
$ g++ --version
# g++ (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0
# Copyright (C) 2023 Free Software Foundation, Inc.
# This is free software; see the source for copying conditions. There is NO
# warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Once we have c++ compiler installed, Lets compile and run our dumb c++ code.
# Compile cppLooper.cpp code and create cppLooper executable.
$ g++ -std=c++17 -O2 -DNDEBUG ./cppLooper.cpp -o cppLooper
-std=c++17: Expects the source code is written in modern C++ v17-O2: Compilation Optimization level 2. Recommended for production/release artifacts-DNDEBUG: Removed support for debugging the artifact a small optimization to reduce build size and condition branching.-o cppLooper: Name of the binary output will be cppLooper.
Next, lets run the cppLooper binary.
# Adds permissions to execute cppLooper binary
$ chmod +x ./cppLooper
$ ls -lrt
# -rw-r--r-- 1 nayaabh devyin 468 Feb 17 23:12 cppLooper.cpp
# -rwxr-xr-x 1 nayaabh devyin 16520 Feb 17 23:40 cppLooper
$ ./cppLooper 10
55
$ ./cppLooper 5
15
$ ./cppLooper 909090
413222768595
If you get the output as above, then our code run flawlessly. Sure, its just runs on your machine, but how do we expose it on Web? But, before I answer that, we need to dig a bit deeper inside the contents of the binary we just created. Lets see what functions(symbols) are exposed from the cppLooper binary, using nm command.
$ nm --extern-only --defined-only ./cppLooper
0000000000002000 R _IO_stdin_used
0000000000001320 T _Z7runLoopm # <-- O_- hmm.. This looks familiar. something something runLoop something..
0000000000001370 W _ZNKSt5ctypeIcE8do_widenEc
0000000000004040 B _ZSt4cout@GLIBCXX_3.4
0000000000004010 D __TMC_END__
0000000000004040 B __bss_start
0000000000004000 D __data_start
0000000000004008 D __dso_handle
0000000000004010 D _edata
0000000000004158 B _end
0000000000001378 T _fini
0000000000001000 T _init
0000000000001230 T _start
0000000000004000 W data_start
0000000000001120 T main # <-- oh! Oh! i know this
Oh look! we have a familiar symbol main. Which is exposed, for obvious reasons, else you won't be able to run the binary directly in the command line. Also, there is a function _Z7runLoopm, which seems to be our runLoop function, but it is mangled with extra characters _Z7 and m. This random chars make it impossible to know the actual name of the function we need to pick and expose while compiling to Webassembly.
Chapter 2: Exposing C++ functions
Ok, we know our function runLoop is available to use in the binary, but we need to expose it with a static name which doesn't change. To do we wrap the functions we need to expose in extern "C" block as shown below.
cppLooper.cpp
#include <iostream>
#include <cstdlib>
#include <cstdint>
extern "C" // 👈🏼 Exposes symbols as-is
{
std::uint64_t runLoop(std::uint64_t N)
{
std::uint64_t sum = 0;
for (std::uint64_t i = 0; i <= N; ++i)
{
// Some fake work, just to demonstrate a CPU intensive work.
sum += i;
}
return sum;
}
}
int main(int argc, char **argv)
{
char *end = nullptr;
const std::uint64_t N = std::strtoull(argv[1], &end, 10);
std::cout << runLoop(N) << std::endl;
return 0;
}
We don't need to expose main as it's already exposed, also, it uses stdin/stdout which will not be available on the web browser and doesn't make sense to expose them. On a side note, if you are building wasm for nodejs to run in terminal then you can definitely expose functions that use standard I/O.
# Compile with exposed functions
$ g++ -std=c++17 -O2 -DNDEBUG ./cppLooper.cpp -o cppLooper
$ nm --extern-only --defined-only ./cppLooper
0000000000002000 R _IO_stdin_used
0000000000001370 W _ZNKSt5ctypeIcE8do_widenEc
0000000000004040 B _ZSt4cout@GLIBCXX_3.4
0000000000004010 D __TMC_END__
0000000000004040 B __bss_start
0000000000004000 D __data_start
0000000000004008 D __dso_handle
0000000000004010 D _edata
0000000000004158 B _end
0000000000001378 T _fini
0000000000001000 T _init
0000000000001230 T _start
0000000000004000 W data_start
0000000000001120 T main
0000000000001320 T runLoop # Nice! just the way we defined it.
By now we have learnt
- how a c++ code is compiled
- how we can expose custom functions.
Chapter 3: Emscripten C++ compiler to Webassembly
Using g++ gave an basic idea what our c++ code compiles to, it can take you this far. But, we need to compile the c++ code to Webassembly code that can be run on the browser. To do that we need a all-in-one compiler toolchain called "Emscripten". It does two important steps
- Compiles C++ to Webassembly
- Generates a glue code in Javascript that we can use to load wasm in our frontend code.
Lets start with installing Emscripten.
$ git clone https://github.com/emscripten-core/emsdk.git
# Cloning into 'emsdk'...
# remote: Enumerating objects: 4805, done.
# remote: Counting objects: 100% (8/8), done.
# remote: Compressing objects: 100% (5/5), done.
# remote: Total 4805 (delta 4), reused 3 (delta 3), pack-reused 4797 (from 2)
# Receiving objects: 100% (4805/4805), 2.65 MiB | 1.44 MiB/s, done.
# Resolving deltas: 100% (3188/3188), done.
$ cd emsdk
$ ./emsdk install latest
# Resolving SDK alias 'latest' to '5.0.1'
# Resolving SDK version '5.0.1' to 'sdk-releases-bf32ae8b61ac8efeb7eca01b54c8307f992724f7-64bit'
# Installing SDK 'sdk-releases-bf32ae8b61ac8efeb7eca01b54c8307f992724f7-64bit'..
# Installing tool 'node-22.16.0-64bit'..
# Downloading: /home/nayaabh/emsdk/downloads/node-v22.16.0-linux-x64.tar.xz from https://storage.googleapis.com/webassembly/emscripten-releases-builds/deps/node-v22.16.0-linux-x64.tar.xz, 30425588 Bytes
# Unpacking '/home/nayaabh/emsdk/downloads/node-v22.16.0-linux-x64.tar.xz' to '/home/nayaabh/emsdk/node/22.16.0_64bit'
# Done installing tool 'node-22.16.0-64bit'.
# Installing tool 'releases-bf32ae8b61ac8efeb7eca01b54c8307f992724f7-64bit'..
# Downloading: /home/nayaabh/emsdk/downloads/bf32ae8b61ac8efeb7eca01b54c8307f992724f7-wasm-binaries.tar.xz from https://storage.googleapis.com/webassembly/emscripten-releases-builds/linux/bf32ae8b61ac8efeb7eca01b54c8307f992724f7/wasm-binaries.tar.xz, 342144156 Bytes
# Unpacking '/home/nayaabh/emsdk/downloads/bf32ae8b61ac8efeb7eca01b54c8307f992724f7-wasm-binaries.tar.xz' to '/home/nayaabh/emsdk/upstream'
# Done installing tool 'releases-bf32ae8b61ac8efeb7eca01b54c8307f992724f7-64bit'.
# Done installing SDK 'sdk-releases-bf32ae8b61ac8efeb7eca01b54c8307f992724f7-64bit'.
$ ./emsdk activate latest
# Resolving SDK alias 'latest' to '5.0.1'
# Resolving SDK version '5.0.1' to 'sdk-releases-bf32ae8b61ac8efeb7eca01b54c8307f992724f7-64bit'
# Setting the following tools as active:
# node-22.16.0-64bit
# releases-bf32ae8b61ac8efeb7eca01b54c8307f992724f7-64bit
# Next steps:
# - To conveniently access emsdk tools from the command line,
# consider adding the following directories to your PATH:
# /home/nayaabh/emsdk
# /home/nayaabh/emsdk/upstream/emscripten
# - This can be done for the current shell by running:
# source "/home/nayaabh/emsdk/emsdk_env.sh"
# - Configure emsdk in your shell startup scripts by running:
# echo 'source "/home/nayaabh/emsdk/emsdk_env.sh"' >> $HOME/.bash_profile # 👈🏼 We will do this
$ echo 'source "/home/nayaabh/emsdk/emsdk_env.sh"' >> $HOME/.bash_profile
$ emcc --version
# shared:INFO: (Emscripten: Running sanity checks)
# emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 5.0.1 (8c5f43157a3f069ade75876e23061330521eabde)
# Copyright (C) 2026 the Emscripten authors (see AUTHORS.txt)
# This is free and open source software under the MIT license.
# There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
$ which emcc
# /home/nayaabh/emsdk/upstream/emscripten/emcc
Now we need to optimize our c++ code a little bit to expose only that is necessary in web environment to bring down the binary size. We use MACROS to include/exclude code base on the compile environment.
#ifdef __EMSCRIPTEN__ ...#endifis used to include a part of our code if__EMSCRIPTEN__is defined during compile time.#ifndef __EMSCRIPTEN__ ...#endifis used to exclude a part of our code if__EMSCRIPTEN__is defined during compile time. E.g. we do not need to expose main function, so we exclude it when compiling.EMSCRIPTEN_KEEPALIVEis used to ensure compiler doesn't remove it during the compilation/optimization process. E.g. a function that is not called directly or indirectly by themain()will get removed during compilation process. Also, since we are removingmain()during compilation, this annotation becomes necessary.
cppLooper.cpp
#include <iostream>
#include <cstdlib>
#include <cstdint>
#ifdef __EMSCRIPTEN__
#include <emscripten/emscripten.h>
#endif
extern "C"
{
#ifdef __EMSCRIPTEN__
EMSCRIPTEN_KEEPALIVE
#endif
std::uint64_t runLoop(std::uint64_t N)
{
std::uint64_t sum = 0;
for (std::uint64_t i = 0; i <= N; ++i)
{
// Some fake work, just to demonstrate a CPU intensive work.
sum += i;
}
return sum;
}
}
#ifndef __EMSCRIPTEN__
int main(int argc, char **argv)
{
char *end = nullptr;
const std::uint64_t N = std::strtoull(argv[1], &end, 10);
std::cout << runLoop(N) << std::endl;
return 0;
}
#endif
Finally! its time to compile our c++ code to Webassembly(wasm). The below command compiles our c++ code to cppLooper.wasm and also creates a glue js code cppLooper.js using ES6 modules. It also exposes runtime methods cwrap and ccall, which we will explore in next section.
$ emcc ./cppLooper.cpp -O2 -DNDEBUG -sWASM=1 -sMODULARIZE=1 -sEXPORT_ES6=1 -sENVIRONMENT=web -sEXPORTED_RUNTIME_METHODS=['cwrap','ccall'] -o ./cppLooper.js
$ ls -lrt
# ...
# -rw-r--r-- 1 nayaabh devyin 701 Feb 18 01:53 cppLooper.cpp
# -rwxr-xr-x 1 nayaabh devyin 439 Feb 18 01:58 cppLooper.wasm # 🎉
# -rw-r--r-- 1 nayaabh devyin 9310 Feb 18 01:58 cppLooper.js # 🎉
Chapter 4: Loading WASM in ReactJS Component
Now, we teleport to Javascript realm to use Webassembly binary code. Lets begin the exciting journey by first copying the cppLooper.wasm and cppLooper.js in public/wasm/ directory. So that we don't forget to include it in our final ui build.
We will create a simple react component that will have a basic form to take input number from user, load wasm, call the c++ function and show the output.
$ mkdir -p public/wasm
$ cp ./cppLooper.wasm ./public/wasm/
$ cp ./cppLooper.js ./public/wasm/
Since, we used -sEXPORT_ES6=1 flag while compiling, our cppLooper.js is an ES6 module and we can import it directly in our React component.
type WasmModule = {
cwrap: (
name: string,
returnType: "bigint",
argTypes: "bigint"[],
) => (...args: (number | string | boolean | bigint)[]) => number | string | boolean | bigint | void;
};
type WasmFactory = (options?: { locateFile?: (path: string) => string }) => Promise<WasmModule>;
const WASM_BASE = "/wasm"; // /public/wasm
const WASM_MODULE = `${WASM_BASE}/cppLooper.js`; // Path to the generated JS glue code
// Dynamically import the generated JS glue code to load the wasm module
const moduleImport = await import(/* webpackIgnore: true */ WASM_MODULE);
// Create the module instance by calling the default export of the imported module
const createModule = moduleImport.default;
// Load the wasm module and get the exported functions ready to use
const cppLooperModule = await createModule({
locateFile: (path) => `${WASM_BASE}/${path}`,
});
// Use cwrap to get a callable version of the exported "runLoop" function
const runLoop = cppLooperModule.cwrap("runLoop", "bigint", ["bigint"]) as (n: bigint) => bigint;
// Call the runLoop function with an input and log the result
const result = runLoop(100n);
console.log(result.toString()); // 5050n
Whats coming next?
- How can we use callbacks?
- How do we pass complex data structures like arrays and objects?
- How do we get this built and deployed?
- Case Study and Performance benchmarks of wasm vs js in real world applications.
- Further reading and resources to learn more about wasm and its ecosystem.
