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

repr(C) struct interop

rut has no C-layout struct interop. There is no #[repr(C)] mirror contract, no host-side zero-copy view over rut record fields, and none of the API that such a contract implies: register_struct::<T>(), StructRef<'v, T>, size_of<T>(), align_of<T>(), or per-field offsets exposed to the host. The title is kept because the question comes up on every FFI: here is what actually exists.

The layout rut records really have

A user record (struct or dataclass) is an array of 8-byte slots:

rulecontent
slot width8 bytes; primitive fields sit inline in their slot
composite fieldsa cell-handle slot — a reference to the shared cell (the Rc heap)
declaration orderfields keep declaration order; no hidden members; visibility and out-of-body impl blocks change nothing
heapnon-moving — a slot’s handle stays valid for the cell’s life
where it livesthe module’s reified type table, not a header contract (reified types and layout)

This layout is a VM-internal invariant, not a stable ABI. It is never promised to Rust code byte-for-byte, and nothing in the toolchain checks a host mirror against it.

What the host sees instead

A host touches record data only through the checked boundary (value boundary):

  • Flat crossing records — declare a host struct in the package’s declaration file. Fields-only, every field a crossing type, no methods. The host constructs and reads the values through the declared field table; the shape is the whole surface.

    // server.d.rut
    pub host struct Point { x: f64, y: f64, tag: str }
    pub host fn measure(p: Point) -> f64;
    
  • Opaque state — the rut side holds a class wrapping an opaque handle; the host owns the layout entirely (native containers API surface).

  • Structured reads — for reflection over arbitrary records (field names, types, children by index), use the reflection surface (reflection). It is descriptor-based and read-only; it never exposes raw memory.

Why there is no shared-layout contract

  • The boundary’s crossing set is deliberately scalars, immutable buffers, and opaque handles; a borrowed user structure with host-writable fields would reintroduce aliasing the borrow guards exist to prevent.
  • Records are shared cells, not values: two bindings alias one record, so a &mut C view would race with rut-side mutation.
  • Layout stability belongs to the module binary and its type table (module binary and verification) — a compile-checked, versioned artifact — not to a C header.

Record values at the boundary

rut shapewhat the host sees
tuple (a record of crossing-typed fields)a Rust tuple, positionally, field-by-field under the record’s declared field types — arity 1–8 (value boundary)
host structthe declared flat record, decoded through the field table; the shape is the whole surface
user record (struct/dataclass)never crosses whole — pass it as opaque, mirror it as a host struct, or walk it with reflection
Vec<T> / [T]never crosses; per-element fns, or bytes for raw payloads

The positional tuple decode is the one place record structure reaches Rust, and it is fully checked: a field whose type does not cross is a compile error on the entry, and a runtime shape mismatch traps naming both sides.

What the toolchain checks instead

A shared-layout contract would be checked once, at registration. rut replaces it with checks it can actually enforce:

guaranteeenforced by
every value a host receives has the declared typethe boundary decode (value boundary)
surfaces and bindings agreethe boot join (host fns)
a consumer and a published package agreethe decl digest at link (module binary and verification)
registers hold their declared typesthe verifier (typed bytecode)

Practical recipes

needshape
pass numeric payloads cheaplycross the fields as primitives, or pack into bytes
share mutable native stateopaque box + wrapper class
read rut records from the hostreflection descriptors, or a host struct mirror
fixed binary recordsbytes + b.len()/decode helpers (primitive types)