It is interesting to replace Node.js/Deno C/C++ addons with WebAssembly modules (portability, libraries, multilingualism)
2023-11-27T00:00:00+0100
It is interesting to replace Node.js/Deno C/C++ addons with WebAssembly modules (portability, libraries, multilingualism)
WebAssembly for Node.js has been on the road for almost as long as the implementation in the web browser. It is interesting to replace C/C++ Addons with WebAssembly modules. These C/C++ Addons are compiled into specific machine code using node-gyp, which complicates the portability of the addon, making a WebAssembly implementation attractive. Especially when, for example, using Wasmtime with WASI (WebAssembly System Interface), so you can access system functions. WebAssembly Modules also allows the use of various alternative programming languages such as Rust, Go, or AssemblyScript.
To demonstrate that the newer and by default secure sibling Deno also supports WebAssembly modules, a small example application is shown here. In this case, a C program is compiled into a WebAssembly module using Emscripten and executed in Deno in a server application.
A brief note on Supply Chain Attacks: Caution is always advised when using externally created modules.
#include <emscripten.h>
EMSCRIPTEN_KEEPALIVEint mul(int a, int b) {
return a * b;
}
EMSCRIPTEN_KEEPALIVEint div(int a, int b) {
return a / b;
}
Compiling the C program with Emscripten: emcc math.c
This creates two files, a.out.js
and
a.out.wasm
. In this case, only the WebAssembly module is
needed. The JavaScript file could be used directly as an alternative, as
shown in other articles like Porting
Third Party to WebAssembly.
Analysis in WebAssembly Text Format:
wasm2wat a.out.wasm > a.out.wat
Or alternatively:
$ wasm-objdump -x a.out.wasm
a.out.wasm: file format wasm 0x1
Section Details:
Type[7]:
- type[0] () -> i32
- type[1] () -> nil
- type[2] (i32) -> nil
- type[3] (i32, i32) -> i32
- type[4] (i32) -> i32
- type[5] (i32, i32, i32) -> i32
- type[6] (i32, i64, i32) -> i64
Function[16]:
- func[0] sig=1 <__wasm_call_ctors>
- func[1] sig=3 <mul>
- func[2] sig=3 <div>
- func[3] sig=0 <stackSave>
- func[4] sig=2 <stackRestore>
- func[5] sig=4 <stackAlloc>
- func[6] sig=1 <emscripten_stack_init>
- func[7] sig=0 <emscripten_stack_get_free>
- func[8] sig=0 <emscripten_stack_get_base>
- func[9] sig=0 <emscripten_stack_get_end>
- func[10] sig=2
- func[11] sig=0
- func[12] sig=4
- func[13] sig=1 <__stdio_exit>
- func[14] sig=2
- func[15] sig=0 <__errno_location>
Table[1]:
- table[0] type=funcref initial=1 max=1
Memory[1]:
- memory[0] pages: initial=256 max=256
Global[3]:
- global[0] i32 mutable=1 - init i32=5243920
- global[1] i32 mutable=1 - init i32=0
- global[2] i32 mutable=1 - init i32=0
Export[14]:
- memory[0] -> "memory"
- func[0] <__wasm_call_ctors> -> "__wasm_call_ctors"
- func[1] <mul> -> "mul"
- func[2] <div> -> "div"
- table[0] -> "__indirect_function_table"
- func[15] <__errno_location> -> "__errno_location"
- func[13] <__stdio_exit> -> "__stdio_exit"
- func[6] <emscripten_stack_init> -> "emscripten_stack_init"
- func[7] <emscripten_stack_get_free> -> "emscripten_stack_get_free"
- func[8] <emscripten_stack_get_base> -> "emscripten_stack_get_base"
- func[9] <emscripten_stack_get_end> -> "emscripten_stack_get_end"
- func[3] <stackSave> -> "stackSave"
- func[4] <stackRestore> -> "stackRestore"
- func[5] <stackAlloc> -> "stackAlloc"
Code[16]:
- func[0] size=4 <__wasm_call_ctors>
- func[1] size=57 <mul>
- func[2] size=57 <div>
- func[3] size=4 <stackSave>
- func[4] size=6 <stackRestore>
- func[5] size=18 <stackAlloc>
- func[6] size=20 <emscripten_stack_init>
- func[7] size=7 <emscripten_stack_get_free>
- func[8] size=4 <emscripten_stack_get_base>
- func[9] size=4 <emscripten_stack_get_end>
- func[10] size=2
- func[11] size=10
- func[12] size=4
- func[13] size=57 <__stdio_exit>
- func[14] size=97
- func[15] size=5 <__errno_location>
Interesting at this point are the designations mul
and
div
of the exported functions under
Export[14]
. These are now used in the Deno server
application.
import { serve } from "https://deno.land/std/http/server.ts";
import { join } from "https://deno.land/std/path/mod.ts";
const wasmPath = join(Deno.cwd(), "a.out.wasm");
const wasmCode = await Deno.readFile(wasmPath);
const wasmModule = new WebAssembly.Module(wasmCode);
const wasmInstance = new WebAssembly.Instance(wasmModule);
const wasmExports = wasmInstance.exports;
const mul = wasmExports.mul as (a: number, b: number) => number;
const div = wasmExports.div as (a: number, b: number) => number;
function requestHandler(request: Request): Response {
const searchParams = new URL(request.url).searchParams;
const a = parseInt(searchParams.get('a') || '14')
const b = parseInt(searchParams.get('b') || '3')
const result = `mul(${a}, ${b}) = ${mul(a, b)}\ndiv(${a}, ${b}) = ${div(a, b)}`;
return new Response(result);
}
const server = serve(requestHandler, { port: 8000 });
To start the server:
$ deno run --allow-read --allow-net server.ts
Listening on http://localhost:8000/
Calling the server:
http://localhost:8000/?a=14&b=3
Short Explanation: * First, the WebAssembly module is loaded from the
filesystem and instantiated. * The exported functions are then assigned
to TypeScript variables. * A server on port 8000 is started, and the
requestHandler
function is passed as the handler. * In the
handler, an attempt is made to read and convert the search parameters
a
and b
from the URL into integers. * The
integers are passed to the WebAssembly functions. * The result of the
two WebAssembly functions is returned as a string in the body of the
Response
. * Deno is also started with the
--allow-read
and --allow-net
flags to allow
the server to access the filesystem and the network.
A brief note on the experimental implementation of ES6 WebAssembly Modules. This allows for a elegant use of WebAssembly modules in Node.js.
import * as M from './module.wasm';
console.log(M);
I am open to refining, expanding, or correcting the article. Feel free to provide a feedback or get in touch with me.
Created by Marco Kuoni, November 2023