loading...
Microsoft Azure

Embedding emscripten in a Node.js library

nebrius profile image Bryan Hughes ・4 min read

I've been experimenting with Web Assembly lately, and right now I'm in the early stages of getting the messaging stack of my wireless LED control system running in Node.js via WASM. I've gotten it up and running (yay!), but it's pretty hacky right now (boo!).

The Scenario

My library is written in C++. I intentionally avoided using anything from the C++ Standard Library and instead used the C Standard Library exclusively (it's more portable and I suspect less complicated to compile). Either way though, both of these standard libraries are runtime libraries that need to be compiled in to the output. If you're a JavaScript developer and have no idea what I'm talking about, imagine if jQuery or Underscore/Lodash were defined as part of the JavaScript spec and were bundled with Node.js, but were still separate libraries. That's the C/C++ Standard Libraries.

All C++ compilers come with these libraries built-in, and you don't have to do anything special to use them. emscripten comes with implementation for these as well, but IME they're still tricky to use. You have to change the compile flags. Specifically, you have to remove the -s ONLY_MY_CODE=1 flag that I mentioned in my previous blog post on WASM. If this is the only change you make to everything in that post, your app will crash with the following:

[LinkError: WebAssembly Instantiation: Import #1 module="env" function="nullFunc_ii" error: function import requires a callable]

Uhm, excuse me, what exactly is nullFunc_ii and why should I care?

So here's what's going on, to the best of my understanding. emscripten compiles your code and injects any and all runtime libraries necessary to run a C++ application. This includes the aforementioned standard libraries, but also includes some other things. Most notably, emscripten injects some runtime libraries to handle things like stack overflows, exception handling, segfaults, etc. I'm about 75% certain that the nullFunc_xxx methods are part of the latter.

These methods are all defined in JavaScript, not C++, and so are not included in the output.wasm file. Instead, they're included with a JavaScript runtime file called output.js (given my -o flag value).

My Hacky Solution

So how did I get around this? My first step was to check out the emscripten docs on output files and formats and the various emscripten specific config flags.

As far as I can tell, what I want to do isn't possible. emscripten allows you to either compile code on it's own (via the -s ONLY_MY_CODE=1 flag), or to compile a complete application that includes a void main() {} (i.e. not a library). Uhmmm...ok...?

After a lot of trial and error, I found a really hacky solution that seems to work.

First, here's my complete compile command I'm now using (note: you can ignore the ERROR_ON_UNDEFINED_SYMBOLS part, I'll talk about that in a later post):

em++ -s WASM=1 -s ERROR_ON_UNDEFINED_SYMBOLS=0 -s EXPORTED_FUNCTIONS=\"['_init','_loop','_setWaveParameters']\" -std=c++11 -Isrc -g4 -o js-dist/output.js js/*.cpp

Note how we have -o js-dist/output.js in the command. This tells emscripten to generate a JavaScript runtime file. This file is intended to be used as a "main" file, i.e. an entire application. We want to use it as a library though, not an application. There are a lot of things in here we need though, most notably two variables it creates called asmGlobalArg and asmLibraryArg. These variables define all of the nullFunc_xxx methods, among others. These variables are not exported in any way though, and as far as I can tell are not meant to be consumed directly.

We're going to do it anways, damn the consequences! I wrote a script to automatically hack this file with the following contents:

const { readFileSync, writeFileSync } = require('fs');
const { join } = require('path');

const OUTPUT_FILE = join(__dirname, '..', 'js-dist', 'output.js');

let source = readFileSync(OUTPUT_FILE).toString();

source = source.replace('var asmGlobalArg =', 'var asmGlobalArg = module.exports.asmGlobalArg =');
source = source.replace('var asmLibraryArg =', 'var asmLibraryArg = module.exports.asmLibraryArg =');

writeFileSync(OUTPUT_FILE, source);

Now we can import these variables into our main file (which is now written in TypeScript FWIW):

import { readFile } from 'fs';
import { join } from 'path';

import { asmGlobalArg, asmLibraryArg } from './output';

let wasmExports: WebAssembly.ResultObject | undefined;
const memory = new WebAssembly.Memory({ initial: 256, maximum: 256 });

readFile(join(__dirname, 'output.wasm'), (readErr, buf) => {
  const bytes = new Uint8Array(buf);
  const env = {
    ...asmLibraryArg,
    table: new WebAssembly.Table({
      'initial': 192,
      'maximum': 192,
      'element': 'anyfunc'
    }),
    __table_base: 0,
    memory,
    __memory_base: 1024,
    STACKTOP: 0,
    STACK_MAX: memory.buffer.byteLength
  };
  const global = {
    ...asmGlobalArg
  };
  WebAssembly.instantiate(bytes, { env, global })
    .then((result) => {
      wasmExports = result;
      wasmExports.instance.exports._init(deviceId);
    })
});

And it works! But it's also pretty ugly IMO. I'm excited though, cause I'm one step closer to integrating this system with Azure IoT Edge so I can control my LEDs from the cloud!

Question for all of you

Am I missing something here? Is there a proper way to do this? I find it hard to believe that emscripten doesn't have an option for compiling a library that includes all the necessary runtime libraries, but I can't seem to figure out how.

Discussion

pic
Editor guide
Collapse
adam_cyclones profile image
Adam Crockett

Bryan, I just had a eureka moment and thought as thanks for keeping me going, I should share this.

If your still working in WASM, I have found the official way with no hacks, no env manually defined, just require the glue code and setup your c++ in the following way, I also have my lua repo which I could share as an example.

emscripten.org/docs/porting/connec...

Collapse
nebrius profile image
Bryan Hughes Author

Alas, I couldn't get embind (or ccall or cwrap) working in my case. I probably could have figured it out eventually, but I had deadlines 😉

Collapse
adam_cyclones profile image
Adam Crockett

Thank you once again for saving the day .. sort of, this rewire is a hacky thing and feels very wrong considering the glue code does include some if node then do this type stuff.
But for the life of me I can't seem to make a library unless I take this approach. I would love to see a script to remove all the browser bits from the glue code.

Collapse
nebrius profile image
Bryan Hughes Author

"But for the life of me I can't seem to make a library unless I take this approach." I think I said exactly the same thing at one point in time 😂

Yeah, this approach is really hacky, and I'm not particularly happy that I had to do it this way.

Collapse
tranphuoctien profile image
Tran Tien
Error:  LinkError: WebAssembly.instantiate(): Import #0 module="env" function="abort" error: function import requires a callable
(anonymous) @ app.js:25
Promise.catch (async)
(anonymous) @ app.js:24

Im using github:AssemblyScript/assemblyscript
How to fix please? Im newbie

Thanks

Collapse
cancerberosgx profile image
Sebastián Gurin

I'm trying to run ImageMagick (wasm-imagemagick). We avoiding compiling two .wasm (probably we will end doing that). In this case the project provides a js API library to users ourselves are consuming it the application using its CLI.

Besides correctness I think is great that people start the discussion this way without shame. Build and distribution of emscripten binaries is still painful (distribution and installation of libraries particularly) and although a hack it could save lots of pain (so I'm trying it now).

In general we are engineers and if the right solution cost too much then it's not the right solution.
IMO, sometimes Elegant, and right are words that apply to other career while the engineer just need to make it work... Thanks.