Skip to content

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.