I implemented a wasm example (running on Node.js, but could just as easily be in the browser) that doesn't have a host! https://github.com/roc-lang/roc/blob/4cb02f1d04e5f458389e44a78c542e497f589266/examples/nodejs-interop/wasm/hello.js#L59
so there's no dependency on Zig or clang or Rust or anything - all you need to build and run the example is roc
and node
(or roc
and a browser)
it works by implementing roc_alloc
and such directly in JS, working with the WebAssembly's raw byte array representation of memory using JS commands instead of C commands
(thanks to @Brian Carroll for explaining to me how all that works in wasm!)
Awesome!!!
That is actually really exciting. Gets rid of a whole layer of wrapping and dependencies.
yeah! I'm not sure what the runtime performance implications are; presumably writing JS code instead of wasm code is slower in the general case, but in this case is it noticeable? I very much doubt it, but I don't really know :big_smile:
I guess it depends how much you delegate to roc vs run in js. Given most stuff we are talking about here is just thin wrapper, I don't think it should make a tangible difference, but I haven't dug into anything here.
I could try porting my old wasm cpu example to this to see the perf diff.
These thin functions around Uint8Array
/ DataView
probably optimize well
When I saw that last commit I thought it was super cool. I'm very interested to play with this some more and see what I can build with it. Are you thinking of going this direction for the Node TS integration?
yep, exactly!
at Vendr we're planning to introduce Roc in multiple places in the code base using calls to small Roc functions (at first, then more and more Roc over time), which means it will be common to rebuild the platform
I want to make that rebuild as fast as possible, so taking out the step of building and linking the host (even if it's zig
doing the build and wasm-ld
instead of ld
) will get multiplied by the number of different entrypoints we have
also, it means I don't have to introduce a zig
dependency for everyone (nontrivial; we aren't currently using Nix, but rather Homebrew on Mac and Docker on Linux)
(well I guess introducing it is fairly trivial, but people have to run a manual command to get it, and then the versions have to be kept in sync, which requires re-running that command, etc. etc.)
I could try porting my old wasm cpu example to this to see the perf diff.
Actually that won't be useful. It currently never allocates, that said, it is using effects that directly call into JS just like your JS host is doing. And it was way faster than the JS only version, so at least that is a good sign. It has to call into JS for every memory read and write that the cpu does. So essentially they are super thin functions that just wrap array access. So you would think the overhead of function calls could matter a lot in that case, but it doesn't seem to.
Also, I wonder what the perf cost of having JS do memory management would be. As in alloc would essentially be a wrapper around new Uint8Array(requestedBytes)
. dealloc would be delete mem
or a noop.
whoa, I didn't even think of that! Although I don't know if wasm can "see" those bytes :thinking:
like I don't know if it can address into them
but if so, that would be really cool!
Ah, sounds like no
Wasm has a buffer that js can read and write to, but wasm can't access js memory
I guess this is where the wasm GC proposal should help in the future, iirc.
Yeah, looked into this again.the new wasm GC stuff should resolve this. Discussed some in this talk: https://youtu.be/Nkjc9r0WDNo?t=855s
Specifically for this, it adds reference types to wasm that can reference host data.
Wow this is a really interesting idea!
I bet JS is plenty fast enough. These functions will be called a lot so v8 will optimise the heck out of them. Especially since they're doing low-level things with numbers and byte arrays.
new Uint8Array
won't work for memory management though. The Wasm module has a single Uint8Array
and "allocation" means dividing it up into chunks on demand, and keeping track of each chunk so that you can free it and then hand it out to someone else again. In other words you are writing an allocator in JS. You cannot build on top of the JS allocator.
In other words you are sub-dividing the piece of memory that JS has already allocated to Wasm.
And growing it and so on
you can't grab more memory and shove it in there
Yeah, makes sense. Though i guess you could implement the malloc algorithm that sub divides in js if wanted.
Otherwise, more convenient apis should be possible once the reference types proposal is implemented (apparently it was split out of the wasm GC proposal and is already implemented in wasmtime)
Oh, actually it looks like the host references will be opaque to wasm, so this still doesn't help here. Wasm can't twiddle the bytes.
Also, found an interesting related read: https://fitzgeraldnick.com/2020/08/27/reference-types-in-wasmtime.html
That's right. When you hear that some GC feature is coming to Wasm, you naturally think it sounds related to this and really useful. But in fact it has zero relevance to implementing roc_alloc. What is needed is a malloc implementation written in JS that manipulates a UInt8Array.
So one approach would be to try to find a simple open source allocator in C or Zig and translate it line by line to JS. I know the Zig folks have a small-code-size allocator that they use for Wasm. Can't remember the name of it.
Last updated: Jul 06 2025 at 12:14 UTC