Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Wire types

Native and wasm mods share mod_api::wire::{Wire, Reader, Writer, WireError}. Derive mod_api::Wire explicitly on a type whose values cross a target boundary. The build never adds a derive. Types used only inside one target do not need it. Every field of a derived type must also implement Wire.

#![allow(unused)]
fn main() {
#[derive(mod_api::Wire)]
pub struct ChemicalAmount {
    pub chemical: String,
    pub amount: u64,
}
}

Records encode fields in declaration order. Enums encode a case index followed by its fields. Lengths and numbers are little-endian; collections are length-prefixed. Decoding returns WireError for invalid data rather than accepting an unknown tag, invalid UTF-8 or an unsupported length.

Nesting and stacks

Structural decoding and encoding allow 32 nested records, payload-carrying variants and Box/Arc values (MAX_DEPTH). Collection iteration, Option and tuples do not consume a level. The reader returns WireError::Depth before entering level 33; the writer records the same error. Always check Writer::error before sending its bytes. This is a transport budget for unbounded mod-defined trees, including ingredients, item places, loot vehicles and recipe slot displays.

Vanilla trees are not restricted by that structural budget: NBT Tag uses an explicit work stack with 512 nested containers; Text crosses as the existing validated flat TextForm table, preserving its 512 component-depth contract. These encodings are identical on native and wasm. Text's layout identity is the flat table's identity; mods must rebuild.

JSON-backed serde_json::Value, JSON maps and ModelPart also refuse beyond 32 JSON containers. Their encoders preflight the in-memory tree iteratively before serde recursion; their decoders scan the JSON's nesting outside quoted strings before parsing. A model part's JSON includes object, children-list and named-child-tuple containers, so one part can consume several JSON levels.

The smallest configured guest shadow stack is 256 KiB (horizon-mod-wasm's shared linker). Wasmtime also has its default 512 KiB native call-stack budget. Native decoding runs synchronously on the calling thread: world workers use Rust's normal spawned-thread stack (normally 2 MiB), while integrated server threads reserve at least 8 MiB. The depth regression tests use 128 KiB, leaving half the guest shadow stack for callers. An embedding that supplies smaller stacks or arbitrarily recursive handwritten Wire implementations must enforce its own stack contract; the reader cannot police code outside its codec.

Wire does not own a borrowed value's destructor. Constructing an arbitrarily deep Rust tree and then dropping it can still overflow a thread independently of Wire. Use explicit work stacks when disposing of hostile trees (discard_tag for NBT), and do not transfer arbitrarily deep owned Rust trees to an encoder. The source scanner tools/scan_recursive_wire.py and its classification test require review of every new recursive public vocabulary type.

Checked representations

Use #[wire(via = String)] (or another Wire type) when a checked constructor owns validation. Implement From<&YourType> for String and TryFrom<String> for YourType. The derive writes the representation and maps a failed conversion to WireError::Invalid. Its layout is the representation's layout.

Layout identity

Wire::layout computes an encoding identity recursively, including field names, case names and child layouts. It is a runtime method, not a constant, because recursive types need a visited-type stack. Container layout follows encoding: Vec<T> and VecDeque<T> agree, as do String and Box<str>; Box<T> is transparent. Changing a crossing field requires rebuilding dependents; the wire format has no compatibility versions or default fields.

cargo horizon-mod build writes layouts.json at the package root, mapping mod IDs to public type paths and their 16-digit hexadecimal layout identities. It includes the mod dependencies it compiled against. A cached host probe under target/horizon-mod/ computes these values, for both native and wasm builds. When there are no concrete crossing types, no probe is built and no file is written. Private types without a public path and uninstantiated generic declarations are not listed; a public re-export can give a private module's type a public path.

A mixed-target dependent with a different layout is refused naming the library and differing type. Even matching layouts do not enable mixed native and wasm dependencies yet: those edges are refused explicitly. See the extension design for the complete layout contract.

Hand-written implementations

The trait is unsealed. A hand-written implementation must supply put, take, MIN, and LAYOUT_SEED; compound encodings must override the hidden layout method to incorporate their children and handle recursive visits. Never use a constant leaf seed for a compound encoding: field changes must change its identity. The derive handles this bookkeeping automatically and is preferred.

A hand decoder owns tag validation and must use Reader::nested for recursive values. Reader checks byte bounds and UTF-8, but cannot enforce invariants that an implementation neglects. Keep validation at the checked constructor boundary.

Public declarations used in layout discovery must be unconditional. The layout probe refuses #[cfg] and #[cfg_attr] on public types, modules, and re-exports, naming the declaration. Keep target-specific implementation details private; publish an unconditional Wire representation.