Write-once, dual-target¶
The headline model: one game-sim source file compiles, with no code change, into both a trusted native binary and a sandboxed WASM module. Hosting model is chosen at build/deploy time — your simulation logic is identical.
| Target | How it's built | lattice_* calls become |
Hosting tier |
|---|---|---|---|
| Native | clang/g++ + link liblattice |
linked C calls into the real core | self-host / P2P — trusted, your fleet |
| WASM | clang --target=wasm32 -nostdlib -Wl,--no-entry -Wl,--allow-undefined |
undefined imports env.lattice_ms_* |
managed hosting — untrusted, our fleet, sandboxed |
The module is authored against one narrow C-ABI capability surface
(lattice_managed.h).
The exact same calls:
- Native resolve at link time to the shim driving a live
lattice_runner. - WASM are left undefined; the linker emits
env.lattice_ms_*imports that only the managed host satisfies — so the sandboxed module reaches the core and nothing else.
flowchart TD
SRC["game_sim.cpp — authored once against lattice_managed.h"]
SRC -->|"(a) native: link liblattice"| N["game_sim_native"]
SRC -->|"(b) wasm: --target=wasm32, imports undefined"| W["game_sim.wasm"]
N --> RUN1["real lattice_runner (self-host / P2P)"]
W --> SCAN["import-allowlist scan"]
SCAN -->|admit| HOST["WASM sandbox host over liblattice<br/>grants ONLY env.lattice_ms_*"]
Roles are orthogonal to packaging¶
The SERVER / HOST / CLIENT role model is orthogonal to native-vs-wasm packaging. Role is
injected at runtime (lattice_runner_start(mode)); packaging is chosen at build/deploy time. A
managed-hosted authority is SERVER-role and WASM-packaged; a P2P host is HOST-role and
native-packaged — both run the same sim source.
Why a narrow subset, not the full lattice.h?¶
The full ABI returns raw host pointers (lattice_object_state), takes struct-of-pointer
descriptors, and pushes host→module callbacks through C function pointers — none of which cross a
wasm32 sandbox boundary unchanged. The managed surface is reshaped to the lowest common denominator
that is identical on both targets: state lives in the module's memory; everything crosses as
scalars or a (ptr, len) byte blob the host copies immediately; host→module delivery is
pulled by the module each tick. It is a faithful projection, not a different model.
Verified: both targets produce a bit-identical simulation¶
The reference dual-compiles one sample/game_sim.cpp to both game_sim_native
(liblattice-linked) and game_sim.wasm, and both produce the identical replicated field values
and a bit-identical simulation checksum:
=== NATIVE ===
objects=4
obj[0] netid=1 x=8 y=12 score=80
obj[1] netid=2 x=26 y=12 score=80
obj[2] netid=3 x=44 y=12 score=80
obj[3] netid=4 x=62 y=12 score=80
CHECKSUM=17699989211919191625
INBOUND rpc=0 event=0 http=1
=== WASM ===
objects=4
obj[0] netid=1 x=8 y=12 score=80
obj[1] netid=2 x=26 y=12 score=80
obj[2] netid=3 x=44 y=12 score=80
obj[3] netid=4 x=62 y=12 score=80
CHECKSUM=17699989211919191625
INBOUND rpc=0 event=0 http=1
The equal CHECKSUM covers the module-driven simulation, which must be identical; inbound-message
arrival timing rides the host transport and is reported separately. The same source, two compile
backends, one verified result — that's the promise.
Ready to ship? Go to Path A — self-hosted / P2P or Path B — managed WASM.