Minimal CPP to WASM Webpack Loader
Load C/C++ source files directly into javascript with a zero bloat.
- No external files, compiled webassembly is saved directly into the js bundle.
- Super small builds with minimal execution environment, bundles start at only 1.5kb gzipped.
- Uses
WebAssembly.instantiateStreaming
to bypass Chrome 4k limit for WebAssembly modules. - Provides export for WebAssembly memory management and access.
- Includes a small memory manager class to handle
malloc
and free
on the javascript side (saves ~6KB). - Easily add custom javascript functions to call from C/C++ or vise versa.
Installation
- Install Emscripten following the instructions here.
- Run
npm i cpp-min-wasm-loader --save-dev
. - Add this object to the
rules
section of your webpack build:
{
test: /\.(c|cpp)$/,
use: {
loader: 'cpp-min-wasm-loader'
}
}
- Make sure
.c
and .cpp
are in the webpack resolve object:
resolve: {
extensions: ['.js', ".c", ".cpp"]
}
A fully working example webpack config file can be found here.
Minimal Example
You can also view a complete working example on github here.
add.c
#include <emscripten.h>
extern "C"
{
extern int sub(int a, int b);
EMSCRIPTEN_KEEPALIVE
int add(int a, int b)
{
return a + b;
}
}
add.js
const wasm = require("./add.c");
wasm.init((imports) => {
imports._sub = (a, b) => a - b;
return imports;
}).then((module) => {
console.log(module.exports.add(1, 2));
console.log(module.memory)
console.log(module.memoryManager)
}).catch((err) => {
console.error(err);
})
Using The Memory Manager Class
The class can provide a list of available memory addresses upon request. The memory addresses can be used to set or access the value of that variable in javascript or C/C++;
If you're unfamiliar with pointers/memory management jump to this part of the readme.
Memory can be allocated through javascript or C/C++. Values can be read or adjusted by C/C++ or Javascript using the provided memory addresses.
Memory Manager API
To access the memory manager you can grab it off the module object after webassembly has initialized:
const wasm = require("./add.c");
wasm.init().then((module) => {
const memory = module.memoryManager;
const addr = memory.malloc(20);
memory.set(addr[0], 50);
console.log(memory.get(addr[0]));
memory.free(addr);
})
Memory Manager Example
You can pass the addresses provided by the memory manager directly into C/C++ as pointers.
The pointers are always referencing a float
type.
manager.c
#include <emscripten.h>
extern "C"
{
extern double mallocjs(int len);
extern void freejs(int start, int len);
EMSCRIPTEN_KEEPALIVE
double doMalloc(int len) {
return mallocjs(len);
}
EMSCRIPTEN_KEEPALIVE
void doFree(int start, int len) {
freejs(start, len);
}
EMSCRIPTEN_KEEPALIVE
void setC(float* ptr, float value) {
*ptr = value;
}
EMSCRIPTEN_KEEPALIVE
float getC(float* ptr)
{
return *ptr;
}
EMSCRIPTEN_KEEPALIVE
float addC(float* ptr1, float* ptr2)
{
float p1 = *ptr1;
float p2 = *ptr2;
return p1 + p2;
}
EMSCRIPTEN_KEEPALIVE
float* address(float* ptr)
{
return &*ptr;
}
}
manager.js
const wasm = require("./manager.c");
wasm.init().then((module) => {
const memory = module.memoryManager;
const addr = memory.malloc(2);
memory.set(addr[0], 50);
memory.set(addr[1], 25);
console.log(memory.get(addr[0]))
console.log(module.exports.getC(addr[0]))
memory.set(addr[0], 30);
console.log(memory.get(addr[0]))
console.log(module.exports.getC(addr[0]))
module.exports.setC(addr[0], 10);
console.log(memory.get(addr[0]))
console.log(module.exports.getC(addr[0]))
console.log(module.exports.addC(addr[0], addr[1]))
console.log(memory.get(addr[0]) + memory.get(addr[1]))
console.log(module.exports.address(addr[0]) === addr[0])
memory.free(addr);
})
Advanced Usage / Tips
Webpack Options
The webpack loader has several options:
{
test: /\.(c|cpp)$/,
use: {
loader: 'cpp-min-wasm-loader',
options: {
}
}
}
WebAssembly Memory
The module.memory
export is a buffer that holds memory that is shared between javascript and webassembly. You can read about how to use it in these MDN docs.
Calling JS Functions from C/C++
It's pretty easy to setup JS functions to be called from within C/C++. While it's technically possible to setup functions to use strings/charecters it's much easier to just stick to numbers.
Let's expose a function in javascript that will add two numbers
customFn.js
const wasm = require("./customFn.c");
wasm.init((imports) => {
imports._add = (a, b) => {
return a + b;
}
return imports;
}).then....
customFn.c
#include <emscripten.h>
extern "C"
{
extern int add(int a, int b);
EMSCRIPTEN_KEEPALIVE
int doAdd(int a, int b) {
return add(a, b);
}
}
WebAssembly can often crunch numbers several times faster than Javascript, but there is a HEAVY penalty to the execution speed when calling JS from C/C++ or vice versa. The more you can send a batch job to C/C++ and pick it up later when it's done the better performance you'll experience.
Pointers and Whatnot
WebAssembly shares a major limitation with many other low level languages: some memory must be managed by hand.
This isn't as difficult as it sounds, if you inilitize a variable inside a function in C/WebAssembly those are still cleaned up automatically. We only need to concern ourselves with variables that are intended to stick around and be used across many functions.
To make this process easy, C/C++ provides an abstraction called pointers. Pointers are just the memory address for a specific variable. In most cases it's much better and more efficient to provide addresses/pointers to a function in C rather than the variables themselves. We end up having to do much less work since values don't need to be copied around in memory.
But where do we get pointers? We have to allocate a new memory slot for each variable we want with malloc
, and when we perform that allocation we get a new pointer for each memory slot.
The problem is there are only a limited number of slots, around 1 million with this library. Once you use them all up, you're out of memory.
So to keep that from happening we want to use pointers whenever possible (so we're not creating copies of the same variable everywhere, but references to that variable), and when we're done with a variable we'll free up it's address with free
.
Let's see this in practice:
const wasm = require("./manager.c");
wasm.init().then((module) => {
const memory = module.memoryManager;
const addr = memory.malloc(1);
console.log("New Memory Addresses: " + addr);
memory.set(addr[0], 500);
console.log(memory.get(addr[0]))
memory.free(addr);
});
So why not just do javascript variables? The advantage of using the memory class is we're creating values/variables that can be accessed and modified from javascript and WebAssembly/C. So we can use Javascript to inilitize the values and save the address/pointers to a javascript class, then pass the pointers into C functions when we need to perform expensive calculations.
License
MIT