I think we’re looking at this from different angles. You’re focused on the compilation unit, but I’m looking at the execution boundary.
I see this as two distinct layers: an Execution Layer (JavaScript runtime) and a Performance/Data Layer (Zig core). I don’t need the JS runtime to be as fast as the network core. I can isolate them completely using io_uring for message passing.
Since the JS runtime itself introduces latency in the microsecond range (or higher), the overhead of an asynchronous message-passing interface becomes negligible. It’s not about finding a faster way to link modules—it’s about isolating the high-latency logic from the high-throughput data plane, so the business logic doesn’t bottleneck the network core.
One thing I don’t understand about the Bun Zig->Rust rewrite is why they didn’t just use V8? Last time I was researching this, it looked like their VM is (at least partially) available to be used for the C/C++ native objects. I have no idea if it would work in the end, but given that Google already put some energy into this and wrote quite a few articles about this problem (some objects living in native, some living in JS, and some of them being available in both and having to correctly garbage-collect non-trivial graphs of these), I’d just try that first… anyway maybe somebody else will do.
libcruller.so is a stable dynamic library containing JavaScriptCore. The host, protocols, and external interfaces can be changed and rebuilt without rebuilding WebKit/JSC.
Each runtime instance runs in its own thread and has its own VM, heap, scheduler, and allocator.
The external interfaces—HTTP/1.1, HTTP/2, HTTP/3, WebSocket, ZeroMQ, files, databases, and IPC—are implemented outside the runtime as replaceable host modules.
The controller converts interface events into messages for a selected runtime and maps runtime asynchronous calls back to the required external interface.
Command rings carry only small control messages:
INVOKE
ASYNC_CALL
COMPLETE
CANCEL
DRAIN
STOP
io_uring provides the shared Linux-native data path for both network and file operations. Runtime instances exchange buffer references with the controller instead of copying payloads through the command rings.
Runtime lifecycle:
ACTIVE → DRAINING → DESTROY → CREATE
The controller stops routing new work to a runtime, lets active operations finish, destroys the runtime together with its allocator, and creates a fresh instance from the same libcruller.so.
to be honest, even jetzig takes a long time to compile due to the overuse of comptime; it generates a new Zig file for templates and routes after any small change making it compile a new generated project everytime, making one question… things (I dont want to go OT)
why not use the @"#" syntax? also, it’t tempting to make a tool which removes them automatically, or just do :s/\.#/_ or some regex on the editor. anyways, it will be a journey migrating to Zig from Zig
I think I’m not experienced enough to understand? isn’t the problem in having a JS runtime in the first place?
Everything in my project that can be heavy already works on Zig wrappers around C++ libraries, databases, audio gateways, DAG processing, heavy slicers, while business logic and UI are built on TS because it’s cheap and efficient. Example: GitHub - solenopsys/behemoth: Behemoth Converged Storage · GitHub
it seems like it could be a great tool for using npm libraries as part of android apps, turn (old / secondary) android phones into local first self hosted servers, polyfill bun file access apis so code works across different platforms ( the api surface for reading and writing files in bun is simple enough so it can be polyfilled on different platforms / with different backends))
wondering if you had this in mind as well or if this is strictly focused on embedding bun in zig applications.
this approach is interesting. Bun itself contains a C compiler to conveniently do FFI with C snippets. It would be possible to do the same for zig, but this approach makes even more sense. If it leads to more stable and predictable behavior of the server executable that is great.
Definitely great to see an alternative and more developments in the bunzig space. Exciting!
Thanks. Yes, I actually wanted to make an isolated library with a simple interface to run anywhere, and also test it to track down memory leaks in the JS core, since it’s easier to look for them in an isolated core.
I also use QuickJS very actively in other parts of the application-embedding it in Zig for management and lightweight business logic-while Cruller is needed for heavy tasks with heavy npm libraries, but bundled into a single .js file instead of sitting in node_modules, for maximum production performance.
did you just strip the build cli or the .build() method on the Bun global object as well?
it would be great if the Bun api stayed exactly the same. I depend on this api to turn tagged html template strings with embedded ts into a deliverable at start time. The set of features that bun provides are very helpful even as part of a runtime. It is easier to distribute the code as typescript and assemble the html entrypoint at runtime in memory.
this also allows for some flexibility for end users to use agents to customize their ui. In the future there won’t be a clear distinction between compile time + tools for devs and the interface for users personal agents to customize their experience
Yes, I did that too as a raw version in one of my projects when I had TS files and they were being executed.
But why I removed it from the runtime: first, this solution will cascade and require node_modules since such solutions usually pull environment libraries from there; second, there is no type checking at runtime like there is in Java ; and third, compilation happens dynamically, and the exact same thing can be done by using a 50kb JS library that already exists.
And most importantly, JavaScript is the actual runtime target. TypeScript and Bun.build() are tools on top of it. As soon as you keep compilation inside the runtime, the scope starts growing very quickly. Now it has to walk directories, resolve imports, load packages, transform code, bundle it, minify it, and so on. In the end, the runtime becomes a large build system again.
That is the opposite of what I want. My idea is simple: the RT should take already prepared, stable JavaScript and execute it as fast and predictably as possible.
This also makes it easier to isolate memory problems and, potentially, replace JavaScriptCore with V8 or QuickJS later without dragging the whole Bun build stack along with it.
I’ve been always wondering if Oilpan could be used for the Zig side if Bun ever switched to V8.
BTW: It is an old post and it might require a bit more digging in order to be useful, but the eventual endgame is to have one GC shared for both JS and Zig side (and no memory leaks that are often created by such duality)