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):
extern "env" fn jsLog(ptr: [*]const u8, len: usize) void;
extern "env" fn jsLog(ptr: [*]const u8, len: usize) void;
const wasm = await WebAssembly.instantiateStreaming(fetch("prog.wasm"), {
env: {
jsLog: (ptr, len) => {
/* read from memory and console.log */
},
},
});
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.
extern fn jsLog(ptr: [*]const u8, len: usize) void;
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.
// js/libshine.js
// Emscripten will provide these globals at link/runtime
declare var mergeInto: (lib: any, funcs: Record<string, Function>) => void;
declare var LibraryManager: { library: any };
declare function UTF8ToString(ptr: number, len?: number): string;
mergeInto(LibraryManager.library, {
jsLog: (ptr: number) => {
const msg = UTF8ToString(ptr);
console.log("🟢 Zig says:", msg);
},
});
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
.
// create a build step which invokes the Emscripten linker
const link_step = try sokol.emLinkStep(b, .{
.lib_main = shine,
.target = opts.mod_main.resolved_target.?,
.optimize = opts.mod_main.optimize.?,
.emsdk = dep_emsdk,
.use_webgl2 = true,
.use_emmalloc = true,
.use_filesystem = false,
.shell_file_path = opts.dep_sokol.path("src/sokol/web/shell.html"),
// set the js file here
.extra_args = &.{
"--js-library", "js/libshine.js",
},
});
We can then call it from zig, with something like:
pub fn main() void {
jsLog("hello from zig");
}
extern fn jsLog(ptr: [*]const u8) void;
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:
#include
<emscripten.h>
EM_JS_DEPS(bla, "$UTF8ToString");
EM_JS(void, jsLog, (const char* s), {
console.log(UTF8ToString(s));
});
and adding it into the build file:
// build the main file into a library, this is because the WASM 'exe'
// needs to be linked in a separate build step with the Emscripten linker
const shine = b.addLibrary(.{
.name = "shine",
.root_module = opts.mod_main,
});
// get the Emscripten SDK dependency from the sokol dependency
const dep_emsdk = opts.dep_sokol.builder.dependency("emsdk", .{});
// need to inject the Emscripten system header include path into
// the cimgui C library otherwise the C/C++ code won't find
// C stdlib headers
const emsdk_incl_path = dep_emsdk.path("upstream/emscripten/cache/sysroot/include");
shine.root_module.addCSourceFile(.{
.file = b.path("src/libjs.c"),
.flags = &.{}, // optional extra emcc flags
});
shine.addSystemIncludePath(emsdk_incl_path);
The calling code in main.zig
remains the same:
pub fn main() void {
jsLog("hello from zig");
}
extern fn jsLog(ptr: [*]const u8) void;
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:
#include
<emscripten.h>
EM_JS_DEPS(bla, "$UTF8ToString");
EM_JS(void, jsLog, (const char* s), {
console.log(UTF8ToString(s));
});
void dummy(void) {};
and in main.zig
:
pub fn main() void {
dummy();
jsLog("hello from zig");
}
extern fn jsLog(ptr: [*]const u8) void;
extern fn dummy() void;
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.
#define _EM_JS(ret, c_name, js_name, params, code) \
_EM_BEGIN_CDECL \
ret c_name params EM_IMPORT(js_name); \
__attribute__((visibility("hidden"))) \
void* __em_js_ref_##c_name = (void*)&c_name; \
EMSCRIPTEN_KEEPALIVE \
__attribute__((section("em_js"), aligned(1))) char __em_js__##js_name[] = \
#params "
<::>
" code; \
_EM_END_CDECL
The above macro translates to zig roughly (with help from ChatGPT) as:
extern fn jsLog(ptr: [*]const u8) void;
/// 2. Keep a reference to avoid the linker removing the function.
/// Same role as __em_js_ref_* in the C macro.
pub export const __em_js_ref_jsLog = &jsLog;
/// 3. Embed the JS implementation in a special section called "em_js".
/// Emscripten will scan this and inject the code into the output JS.
export const __em_js__jsLog align(1) linksection("em_js") =
"(const char* s)
<::>
{ console.log(UTF8ToString(s)); }\x00";
pub export fn dummy() void {}
I added a pub fn
and called it from main:
pub fn log(ptr: [*]const u8) void {
jsLog(ptr);
}
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:
export const __em_js__jsLog align(1) linksection("em_js") = [_]u8{
'(', 'c','o','n','s','t',' ','c','h','a','r','*',' ','s',')',
'<',':',':','>','{',' ',
'c','o','n','s','o','l','e','.','l','o','g','(',
'U','T','F','8','T','o','S','t','r','i','n','g','(',
's',')',')',';',' ','}','\x00',
};
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:
EM_JS(void, jsLog, (const char* s), {
Module.jsLog(UTF8ToString(s));
});
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).
window.Module = {
jsLog: function (msg) {
console.log("🟢 Zig says:", msg);
},
};
Top comments (0)