The contract between the cuttlefish host and its guest proc-blocks.
Both sides depend on this crate precisely so that they cannot drift: a block is compiled separately from the host, often at a different time by a different person, and the only thing keeping them able to talk is that they agreed on these types.
Why a command loop, not function calls
A block does not call the host. It returns a [Command] describing what it
wants done, and the host — after doing it — hands back an [Event] and asks
for the next command. Control is inverted relative to the obvious design, and
not for taste:
- A core-wasm guest is single-threaded and offers no execution context the host could call back into while the guest is blocked. A "call the host and wait" design has nowhere to deliver the answer.
- Inference must run on a different thread from the wasm store, which is
!Syncand cannot be touched from there. - Because the host decides whether to take the next step, cancellation needs no cooperation from the guest at all: the host simply stops stepping. A guest cannot ignore, delay, or trap its way out of being cancelled.
Everything crosses the boundary as JSON. That is slower than a packed binary layout, deliberately: the boundary stays inspectable, a mismatch produces a legible error rather than a misread integer, and the volume is low because bulk data does not cross it. Revisit only if profiling says to.
Why bulk data does not cross this boundary
No command hands a block the contents of a file. A block [Command::Open]s a
path, receives a [Handle] and a length, then pulls bounded windows with
[Command::Slice].
This keeps guest memory proportional to the window a block chooses rather
than to the size of its input. A block written against a small file behaves
identically against a huge one, and the 4 GiB ceiling of 32-bit wasm stops
being something block authors must reason about — which is what lets this
project stay on wasm32 instead of paying for wasm64.