My first take on WebAssembly

iprosk

Igor Proskurin

Posted on September 4, 2023

My first take on WebAssembly

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 doubles would be nice, but I also need to pass Float64 arrays and aggregate data like struct.

  • 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);
}

}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
}

}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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 doubles, 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...

💖 💪 🙅 🚩
iprosk
Igor Proskurin

Posted on September 4, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

C/C++ code in React using WebAssembly
javascript C/C++ code in React using WebAssembly

September 13, 2023

My first take on WebAssembly
javascript My first take on WebAssembly

September 4, 2023