Date: 2026-07-01
A deftypez or defrecordz field had to be a carrier scalar, an enum,
or a buffer (:string, :bytes, or a slice). A named non-enum field
was rejected at layout time, so a struct could not compose another
struct: no Rect of two Points, no Sprite carrying a position and
a velocity. Real records nest, and the natural representation on both
sides is a struct embedded by value.
A named field whose type resolves to a scalar-only struct is a nested
field, crossed by value. The inner extern struct is embedded in the
outer wire struct (origin: Point), which is a C-ABI-correct by-value
embedding: Zig lays the inner struct's fields inline, and the outer
layout's offsets reflect that.
The outer layout's offset walk treats a nested field as the inner type's
size and alignment. The wire extern struct and the nice struct both
emit name: InnerType for a nested field (no :target, no expansion to
{ptr, len} words); the wrapper writes a nested field as a direct
assignment, which Zig lowers to a struct copy. The FFM reader slices the
out-segment at the nested field's offset and recurses with the inner
layout, building a sub-map; the marshaller mirrors that for an argument.
A nested field is gated on the inner type being scalar-only: every
field, recursively, is a carrier scalar or a further nested scalar
struct. A nested inner type with a buffer field (an owned string inside
each instance) is rejected with :clj-zig/unsupported-field, and a
named field whose type is undeclared is rejected with
:clj-zig/unknown-field. The scalar-only gate keeps the by-value
embedding and the free shim single-allocator: no per-field buffer lives
inside a nested value, so an owned outer struct still frees only its
own (outer) buffer fields.
A developer writes (deftypez Rect [origin Point size Point]) and a
function returning Rect comes back as {:origin {:x ... :y ...} :size {...}}. A nested struct argument reads inner fields in the body
(r.size.x). The composition is recursive: a Scene carrying a Rect
carrying a Point lays out and round-trips through the same machinery.
A nested field returns as a map even when the inner type is a
defrecordz; the record rebuild the top-level return path performs
does not yet recurse into fields. A caller that needs a record literal
calls the inner map factory. Per-field record rebuild is a follow-up if
real use asks for it.
Flatten a nested struct into individually-named scalar fields
(origin_x, origin_y). Rejected: it loses the structural grouping on
both sides, forces a name-mangling convention, and makes the field count
and order a hidden coupling between the contract and the body.
Pass a nested struct by pointer rather than by value. Rejected: the boundary passes top-level structs by value already, and a nested field is data inside that value, not a separately-owned allocation. By-value embedding matches the C ABI and needs no lifetime management.
Defer until nested buffer-carrying inners are worked out. Rejected: the scalar-only case covers the motivating uses (points, rectangles, vectors, transforms), and the gate keeps the buffer case from complicating the free shim. Nested buffers remain a follow-up.
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |