Calling Javascript from Zig through WebAssembly
The next step for shine is to build a bridge between zig and javascript .
I am currently planning to using supabase for storage. Unsurprisingly, it does not have a zig sdk. It does, however, have a javascript sdk.
If I can write basic CRUD operations in javascript and call that from zig through webassembly, that could make that integration a lot easier.
Goals
There are a few ideal restrictions for me - mainly because writing javascript is not fun for me.
- Use the Supabase js/ts library through zig
- Use TypeScript as much as possible. (I don’t love TypeScript, but at least it’s not javascript)
- Keep as much of the supabase related code in the web part so that deno and lume can handle any heavy lifting.
Options
I can’t use FFI(Foreign Function Interface):
|
|
|
|
because imgui pulls in emscripten, which means we don’t have the ability to call
instantiateStreaming
.
With emscripten, declaring an external function is easy enough.
|
|
There are a couple of options to wire them up to the javascript:
library.js
/ mergeInto
This option requires javascript files on the zig side. If you want to start with typescript, you’ll need to integrate a transpiler into the build chain as well.
First, you want a javascript file - let’s call it libshine.js
, and pop it into
a js
dir.
|
|
We then need to pass this js
file into the build step
as part of my sokol build step, I pass it in as .extra_args
.
|
|
We can then call it from zig, with something like:
|
|
From my firefox console:
Lume live reloading is ready. Listening for changes... localhost:3000:102:15
🟢 Zig says: hello from zig shine.js:3168:11
EM_JS
/ EM_ASM
The other option is to use
EM_JS
which involves writing a wee bit of C
, which can embed the javascript
.
In theory, it’s as simple as:
|
|
and adding it into the build file:
|
|
The calling code in main.zig
remains the same:
|
|
However, this didn’t work, and failed with:
error: undefined symbol: jsLog (referenced by root reference (e.g. compiled C/C++ code))
warning: To disable errors for undefined symbols use `-sERROR_ON_UNDEFINED_SYMBOLS=0`
warning: _jsLog may need to be added to EXPORTED_FUNCTIONS if it arrives from a system library
Error: Aborting compilation due to previous errors
Thanks to some help from flooh (who btw put together the sokol and sokol-zig packages as well the sokol-imgui-sample template which I used to kick start this project.), I was able to get it working.
Turns out the c file needs to have a function in it that is used in the zig file - it doesn’t need to do anything.
So, based on the suggestion, libjs.c
changes to:
|
|
and in main.zig
:
|
|
From my firefox console:
Lume live reloading is ready. Listening for changes... localhost:3000:102:15
🟢 Zig says: hello from zig shine.js:3168:11
You can see a working example in [my forked repo](
EM_JS
directly through zig
[unsuccessful]
Looking at the macro for EM_JS
and with my good friend ChatGPT, I attempted
translating it to zig and made some progress, but ultimately failed to get it
working. I’ll leave the work here in the hopes it might be helpful.
|
|
The above macro translates to zig roughly (with help from ChatGPT) as:
|
|
I added a pub fn
and called it from main:
|
|
Which gave me the familiar error about not being able to find jsLog
.
comparing the linker sections gave some clues:
❯ wasm-objdump --section=linking -x <path/to/libjs.o>
libjs.o: file format wasm 0x1
Section Details:
Custom:
- name: "linking"
- symbol table [count=9]
- 0: F <dummy> func=1 [ binding=global vis=hidden ]
- 1: D <__em_js_ref_jsLog> segment=0 offset=0 size=4 [ binding=global vis=hidden ]
- 2: F <jsLog> func=0 [ undefined explicit_name binding=global vis=default ]
- 3: D <__em_js__jsLog> segment=1 offset=0 size=53 [ exported no_strip binding=global vis=hidden ]
- 4: S <.debug_abbrev> section=7 [ binding=local vis=default ]
- 5: G <env.__stack_pointer> global=0 [ undefined binding=global vis=default ]
- 6: S <.debug_str> section=9 [ binding=local vis=default ]
- 7: T <env.__indirect_function_table> table=0 [ undefined exported no_strip binding=global vis=default ]
- 8: S <.debug_line> section=10 [ binding=local vis=default ]
- segment info [count=2]
- 0: .data.__em_js_ref_jsLog p2align=2 [ ]
- 1: em_js p2align=0 [ RETAIN ]
and the zig object:
❯ wasm-objdump --section=linking -x js.o
js.o: file format wasm 0x1
Section Details:
Custom:
- name: "linking"
- symbol table [count=6]
- 0: F <dummy> func=1 [ binding=global vis=default ]
- 1: D <__em_js_ref_jsLog> segment=0 offset=0 size=4 [ binding=global vis=default ]
- 2: F <jsLog> func=0 [ undefined explicit_name binding=global vis=default ]
- 3: D <__em_js__jsLog> segment=1 offset=0 size=4 [ binding=global vis=default ]
- 4: D <__anon_946> segment=2 offset=0 size=54 [ binding=local vis=default ]
- 5: T <env.__indirect_function_table> table=0 [ undefined exported no_strip binding=global vis=default ]
- segment info [count=3]
- 0: .rodata.__em_js_ref_jsLog p2align=2 [ ]
- 1: em_js p2align=0 [ ]
- 2: .rodata.__anon_946 p2align=0 [ ]`
From what I could understand (which is little), it looks like __em_js__jsLog
in the zig obj is a pointer while from C, it’s the full string.
hardcoding it as a static array helped:
|
|
The output from this is a little more promising
❯ wasm-objdump --section=linking -x js.o
js.o: file format wasm 0x1
Section Details:
Custom:
- name: "linking"
- symbol table [count=5]
- 0: F <dummy> func=1 [ binding=global vis=default ]
- 1: D <__em_js_ref_jsLog> segment=0 offset=0 size=4 [ binding=global vis=default ]
- 2: F <jsLog> func=0 [ undefined explicit_name binding=global vis=default ]
- 3: D <__em_js__jsLog> segment=1 offset=0 size=53 [ binding=global vis=default ]
- 4: T <env.__indirect_function_table> table=0 [ undefined exported no_strip binding=global vis=default ]
- segment info [count=2]
- 0: .rodata.__em_js_ref_jsLog p2align=2 [ ]
- 1: em_js p2align=0 [ ]
Let’s look at the two side by side
# From C
- 3: D <__em_js__jsLog> segment=1 offset=0 size=53 [ exported no_strip binding=global vis=hidden ]
# From zig
- 3: D <__em_js__jsLog> segment=1 offset=0 size=53 [ binding=global vis=default ]
There are some clear differences in how the two are output and I am already beyond my knowledge level here - so I’ll leave it to someone who knows this stuff better (or wait until I do)
You can check out the code in the branch of my forked repo
Next steps
My plan is to use EM_JS
through C
to implement glue JavaScript functions -
something like:
|
|
By doing this, I can have one-line js code in the .c
file and all the
implementation can go into the web side (and can easily be TypeScript too).
|
|