My first take on WebAssembly
Igor Proskurin
Posted on September 4, 2023
Here we go, I decided to try WebAssembly having a project in mind, but before dedicating myself and take on coding, there is always fun of fooling around with some code.
If you are new to WebAssembly, the first place to go after Wikipedia is probably MDN Web Docs that gives a decent introductory background. It explains that WebAssembly is binary format and the corresponding assembly language that runs in a Virtual Machine built into all modern browsers. It sounds like a blasphemy if you are an acolyte of Unix Philosophy, but otherwise it is a cool client-side portable technology to deal with high-performance numeric computations (no, no, I am not talking about crypto mining).
So where should we start?
First comes a compiler
In order to use WebAssembly with our shiny cool numeric C/C++ libraries (you are not going to manually write assembly, aren't you?), we need a compiler that takes C/C++ source as an input and generates a *.wasm
binary file along with some JavaScript glue code, which is needed to call binary code from a web-page or Node.js. Well, MDN recommends to use Emscipten, which wraps around LLVM clang
compiler, and I am not in a position to make a reasonable argument about other options...
I will skip the details of how to install Emscripten SDK. It is pretty easy. Just note that the most reasonable and sane way to use emcc
compiler is, probably, from bash
. But I decided to give it a try from PowerShell first without additional abstractions like WSL, and it worked... Nice surprise.
Let's compile something
Well, let me skip a hello world example, and start with some basic questions one needs to figure out before starting a project.
How can I interface my C/C++ code with JavaScript? At minimum, I need to call some functions. It would be nice to have a full support of classes, threads, etc -- but maybe later, okay?
How can I pass anything from JavaScript to C/C++. Passing a couple of
double
s would be nice, but I also need to passFloat64
arrays and aggregate data likestruct
.How would my C/C++ library functions return anything meaningful rather than a single numeric value?
Let's move one step after another and take time to look around...
Calling C-functions using ccall()
Let us prepare a very basic "numeric library".
// ccall_testing.cpp
#include <cmath>
#include <iostream>
#include <emscripten/emscripten.h>
extern "C" {
EMSCRIPTEN_KEEPALIVE double my_norm(double x, double y) {
std::cout << "my_norm is called\n";
return std::sqrt(x * x + y * y);
}
}
And yes, ccall()
is to call C-functions, so here is extern "C"
to deal with C++ name mangling. We also need EMSCRIPTEN_KEEPALIVE
to prevent this code to be optimized out as dead code.
Now, don't forget to source all the environmental variables from your shell of choice. There is a collection of shell scripts for doing it in your Emscripten SDK source directory. In PowerShell, I just run ./emcmdprompt.bat
.
Then compile our source file using emcc
:
$emcc ccall_testing.cpp -o ccall_testing.js -sEXPORTED_RUNTIME_METHODS=ccall
We have two files. The first is WebAssembly binary ccall_testing.wasm
and the second is JavaScript glue code ccall_testing.js
. It is possible to use html
as the compiler's target, and it will generate a web-page, but I will stick to *.js
to remain in control.
It is time to call our library function from a web page. Let's come up with something very basic:
<!DOCTYPE html>
<html lang="en">
<head>
<title>My WASM experiments</title>
</head>
<body>
<button id="mybutton">Run</button>
<script>
document.getElementById("mybutton").addEventListener("click", ()=>{
const result = Module.ccall("my_norm", // yes, that's our function
"number", // return type
["number", "number"], // argument types
[3.0, 4.0] // yeah, Pythagorean triple
);
console.log(`result = ${result}`);
});
</script>
<script src="ccall_testing.js" type="text/javascript" async></script>
</body>
</html>
Save this as ccall_testing.html
and then run in the browther. Does not work? There is additional step. In order to load *.wasm
blob, we can serve the page from a simple http-server:
$python -m http.server
which will open a connection at localhost:8000
. Now just open html
-file in the directory of your choice:
http://localhost:8000/wasm_testing/ccall_testing.html
Press Run
, open a console Ctrl-Shift-I
and Voila! (don't forget Ctrl+R
to reload cache):
my_norm is called ccall_testing.js:1559:16
result = 5
Prefer wraps instead of calls?
No problem! It is possible to wrap C-function directly into JavaScript function using cwrap()
.
In this case, we don't need EMSCRIPTEN_KEEPALIVE
any more because we will tell the compiler to export this function anyway so our cpp
-file becomes a bit cleaner
// cwrap_testing.cpp
#include <cmath>
#include <iostream>
#include <emscripten/emscripten.h>
extern "C" {
double my_norm(double x, double y) {
std::cout << "my_norm is called\n";
return std::sqrt(x * x + y * y);
}
}
Call the compiler and add -sEXPORTED_FUNCTIONS=_my_norm
with an underscore in front of the function name (that's an assembly symbol, like _start
)
$emcc cwrap_testing.cpp -o cwrap_testing.js -sEXPORTED_FUNCTIONS=_my_norm -sEXPORTED_RUNTIME_METHODS=cwrap
Our html
file also becomes slightly different. Here, I introduce a new JavaScript function myNorm
that wraps around the C-library function call
<button id="mybutton">Run</button>
<script>
document.getElementById("mybutton").addEventListener("click", ()=>{
const myNorm = Module.cwrap("my_norm", // no underscore
'number', // return type
['number', 'number']); // param types;
const result = myNorm(3.0, 4.0);
console.log(`result = ${result}`);
});
</script>
<script src="cwrap_testing.js" type="text/javascript" async></script>
The result is the same as for ccall()
. That's nice. But how can we pass something more meaningful?
But I need to pass an array of Float64
...
The naive question would be: can we use 'array'
instead of 'number'
in the function call? So our code would look like: Module.ccall("my_norm", "number", "array", [3.0, 4.0])
?
The answer is "not so simple"... The array
type here really means something like "raw byte buffer".
According to the API documentation: " 'array' [is] for JavaScript arrays and typed arrays, containing 8-bit integer data - that is, the data is written into a C array of 8-bit integers".
So if we need to pass an array of double
s, it is either a manual allocation and passing a pointer (as a 'number'
) or Embind.
Yeah, Embind is cool! Let's come back to that...
Posted on September 4, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.