Help with getting a simple call to js through emscripten working

Ok, I think I kinda know what happens, and it’s not Zig specific but also happens in a pure Emscripten project…

First: it works if there’s at least one called C function in the same compilation unit as the EM_JS() function, it doesn’t matter what this C function does, it can be an empty dummy function, but it must be called from somewhere else.

Here’s I think what happens:

The source file which contains the EM_JS() function is compiled into a .o file, which is then assembled into a static link library along with the Zig code. Putting all this stuff into a library is needed because the actual linking is delegated to emcc, e.g. it’s not the typical situation of letting the Zig compiler build an executable.

But notably this static link library does not contain any implementation of the EM_JS() function, it only contains a global C string with the JS function body under a different symbol name (most likely __em_js__jsLog). Most importantly the static link library does not contain any symbol jsLog except for an UNDEFINED entry.

When the linker (in this case emcc) links a static library, it will ignore all .o items in the library which don’t have any symbols which the linker is looking for (e.g. the linker is looking for an implementation of jsLog, but this is not in the library, so it doesn’t pull in the jsLog.o item from the library which contains the global string with the Javascript function body… which then means the Emscripten-magic for EM_JS doesn’t kick in and the EM_JS Javascript function isn’t included in the output .js file which is created next to the .wasm file…

At least that’s my current theory… e.g. sort of a chicken-egg situation…

As a more generic workaround, change your libJs.c file like this (I’ll also explain the EM_JS_DEPS() later:

#include <emscripten.h>

EM_JS_DEPS(bla, "$UTF8ToString");

EM_JS(void, jsLog, (const char* s), {
  console.log(UTF8ToString(s));
});

void dummy(void) {};

And somewhere in your Zig code:

extern fn dummy() void;

…and you need to call that dummy function somewhere in the active code, for instance at the start of main():

pub fn main() void {
    dummy();
    ...
}

…this causes the libjs.o file to be pulled in from the ‘main library’, which means that also the magic EM_JS() global C string is pulled in and the Emscripten linker can do its magic to turn this string into a Javascript function.

Maybe there’s a more straightforward way, but I think this is an acceptable workaround. In a real-world application you would probably have a web_utils.c source file which contains a mix of web-specific EM_JS and regular C functions which would be called from the Zig code, so you wouldn’t need such an empty dummy function for the object file to be pulled in.

2 Likes