Structs & complex types
Hover has native structs: named records of primitive fields that live on the logic side of a simulation — state variables, locals, function parameters and return values. A C++ struct owned by an external library is a different matter, and still crosses the FFI through a wrapper; both patterns are at the end of this page.
Declaring a struct
A struct is declared at the top level, alongside modules and functions, with no semicolon after the closing brace. A field's type is a primitive numeric (double, float, int, unsigned int), a fixed-size array of one, or a struct declared earlier in the file — the same discipline C uses, which makes self-referential and cyclic structs impossible to write in the first place. Fields can never be wires or circuit elements: a struct is a logic value, not topology.
Literals & field access
Struct literals name their fields, in any order:
Setting the same field twice in one literal, or naming a field the struct doesn't have, is a compile error. Nesting and indexing compose as they do in C — line.a.x reads through two levels, pts[0].y = 5.0; writes through an array element.
Where structs live
Structs work anywhere a logic value does: state variables and locals inside a module body, fixed-size arrays of structs, and function parameters and return values. Functions take and return them by value — C semantics, a memberwise copy rather than an alias — so a callee mutating its parameter never touches the caller's struct:
Rules & limits
- No cycles. A struct may only name structs declared strictly before it, so self-reference is unwritable rather than merely rejected.
- Not alias-qualifiable. A struct type is named by its bare name everywhere — there is no
math.Point. A struct declared in an imported directory registers under that bare name, so two files in one build declaring the same struct name collide, exactly like two modules with the same name. .save()is not struct-field-aware. It validates against the flat signal table, so.save(main.box.acc.x)does not work. Mirror the fields you want to observe into plain signals —x_out = acc.x;— and save those.- The FFI stays primitive. An
extern funccannot take or return a struct; the compiler rejects it and points here. The two patterns below are the way across.
C++ structs across the FFI
A Hover struct is a logic-side value with a layout the compiler owns. A struct defined by external C++ is opaque to it, and an extern func signature stays primitive, so reaching one takes a C++ wrapper you write yourself, pulled in with importc — see Calling C++ from Hover for how the compiler finds and links those sources. Two patterns cover it.
Flattened wrappers
When the struct is just a bag of parameters the callee reads, take it apart. Write a C++ function that accepts the fields individually, rebuilds the struct, and forwards the call:
The Hover side then declares the wrapper, not the original:
This is the simpler of the two and the right default. Its limit is that nothing persists: the struct is rebuilt on every call, so it can't carry state between timesteps.
The opaque pointer pattern
When the struct is state — an object that has to live across timesteps — build it once on the C++ side and hand Hover its address as a pointer. Hover never looks inside; it just holds the handle and passes it back:
On the Hover side the handle is stored with state, which is what makes it survive from one timestep to the next — a plain local would be recreated every step, leaking a struct each time:
Pointer sizes
Hold addresses in a real pointer type — int* — and never in a bare int or unsigned. Those are 32 bits; a modern address is 64. Storing a pointer in one truncates it silently, and the crash arrives later, when C++ dereferences the mangled handle. It reads as a mysterious segfault well away from the line that caused it.