Wax fits inside the software you already have.
It takes two
Most applications already have a huge amount of machinery that is doing its job just fine. They have a renderer, a network stack, storage, native libraries, platform code, and years of work that nobody wants to rewrite. Wax does not ask you to replace any of that.
Wax is designed to live inside the application you already have. We call that surrounding application the host. The host keeps the operating system, graphics, networking, storage, hardware access, and everything else it already knows how to do. Wax owns the part of the program you want to make deterministic, recordable, and replayable.
In a game, the host might keep the renderer, audio engine, and platform code while Wax owns the game rules and world state. In a server, the host might keep the network and database connections while Wax owns the request logic. The line can sit wherever it makes sense for the application.
That split is not just a convenient way to embed another language. It is what actually makes the rest of Wax possible. If Wax code could reach into arbitrary host memory, call anything it found, or quietly read from the outside world, the runtime could never know what affected the program. A recording would always be missing something, and replay would need the original application and machine around it.
So Wax only sees what the host deliberately shows it. Everything inside that line belongs to the Wax world. Everything outside remains the host's responsibility. The boundary between them is where the interesting stuff happens.
Taking turns
Once we have two worlds, they need a predictable way to talk to each other. Wax does that by making the host and the Wax program take turns.
The host begins a frame, calls into Wax as many times as it needs to, and then ends the frame. While Wax is running, it owns its world. Once the frame ends, control is completely back with the host and the Wax stack is empty. That clean boundary is where snapshots happen, the recording closes one unit of work, and staged channel data becomes visible when the next frame begins.
What a frame represents is up to the application. A game might use one frame for one update of the world. A server might use one for a request. An agent might use one for a single action. Wax does not care what the unit of work means, only that the unit has a clear beginning and end.
Inside that rhythm, calls can go both ways. An api fn is something the host can ask Wax to do. A host fn is something Wax can ask the host to do when it needs a capability from the outside world.
Channels go both ways too. A host channel lets the host stage values and events for Wax. An api channel carries values produced by Wax back to the host, which reads them between frames -- single registers, lists, per-frame batches, and json documents alike. Channels move that data together at frame boundaries instead of turning every value into another function call.
All of these are declared in Wax source. That source is the interface definition. If a host function was not declared, Wax cannot call it. If a Wax function was not exposed, the host cannot call it. There is no second description of the interface for the two sides to disagree about.
The compiler turns that definition into an API that feels native in the host language. It handles loading the Wax program, setting up its memory, connecting the declared functions, and converting values on the way through. There is no pile of handwritten glue code waiting to drift out of sync.
What crosses the line
The most important rule here is ownership. If Wax might remember a value after a call returns, that value cannot still point into memory owned by the host.
Small values can pass directly. Strings, arrays, and other compound values are copied into Wax memory before Wax can keep them. Wax owns that copy and knows it will still be there later. Results going the other way are copied back into the host language's own representation for exactly the same reason.
The compiler only allows types it knows how to move safely. Primitive values, strings, arrays, JSON, enums, and simple aggregates are the common ones. Specialized boundary types cover things like host resources and text that must stay out of a recording. The values are checked as they cross too. An enum has to contain one of its declared values, an array needs a valid length, and a string has to contain valid text. Bad input never reaches Wax code.
That copy is an important part of the safety model, but it is not free. Passing a few values or a small array is nothing worth thinking about. Shoving a huge object across the line every frame can cost more than the work we moved into Wax. Big state should generally stay in Wax, with only the things that changed crossing the border.
Some host resources cannot sensibly be copied at all. A file handle, GPU texture, or database connection stays with the host. Wax can hold an opaque handle for that resource and hand it back later, but it cannot inspect the resource or turn the handle into a pointer.
There is one narrow exception for large outputs. The host can lend Wax a WriteOnlySpan<T>, which gives Wax permission to write into a specific range of host memory for the duration of one call. Wax cannot read from it, grow it, or keep it afterward.
Safety first
Copying values keeps Wax from holding onto host pointers. We also need to make sure Wax itself cannot wander out of its own memory and into the rest of the host process.
Wax is memory safe and null safe. A reference cannot be null unless its type says it can be, and every operation that could step outside an array, span, or allocation is checked. The optimizer removes a check when it can prove the access is safe, but it never removes the guarantee that an access is valid.
Wax uses offsets to represent its objects and index its memory. There is no operation that turns one of those offsets into an arbitrary address somewhere else in the process.
The runtime also reserves the entire range Wax can address and puts inaccessible memory beyond it using virtual memory tricks. Every address Wax code can construct either lands inside its own world or hits that fence explosively. It cannot overflow out of Wax memory and accidentally land in some neighboring memory.
If Wax makes an invalid access, execution stops and the host gets a terminal panic. WebAssembly provides the same basic protection through linear memory.
Basically: a problem in Wax code cannot break the host or corrupt its memory, stack, or any of its resources.
Leaving the host behind
So why go to all this trouble?
Because once every conversation with the outside world has to cross a border we control, Wax can record it.
Suppose Wax asks the host to read a file. The recording does not need the file handle, the file system, or even the original machine. It only needs the answer the host gave back. During replay, Wax gets that same answer from the recording and the host call never happens at all.
The same thing happens in the other direction. If the host calls Wax or sends it channel data, Wax records the values and the frame where they arrived. Replay feeds them back in at the same point.
Even opaque resources fit this model. Wax can remember a handle, but it cannot peek inside whatever the handle represents. If it wants to learn something about that resource, it has to ask the host. The answer comes back across the border and can be recorded like anything else.
That is basically the whole trick. Wax does not record the host, it records Wax's experience of the host. Restore the Wax world, feed those observations back in, and determinism rebuilds the rest. The original application can be somewhere else, turned off, or gone entirely.