Skip to content

FieldStr — a string in 24 bytes

Every record field in Clinker is a FieldStr, and Value::String is the dominant payload in an ETL stream, so this one type’s width sets the per-Value cost, the very number that drives the RSS and spill thresholds from the last four lessons. This is the lowest the data layer goes. You met the 32-byte cell budget back in Data & Representation; FieldStr is how a string fits inside it: three different ownership strategies (inline, shared, unique) hand-packed into a single 24-byte type. The Rust tool that makes that packing possible is unsafe. This is the engine’s deepest use of it, and, more usefully, a model of how to use it responsibly: a tiny unsafe core, fenced in by layout assertions and documented invariants. If you want unsafe Rust from first principles, The Rustonomicon is the canonical treatment; here we use just enough to read the real type.

  • Name FieldStr’s three arms and say what each one optimizes: inline, shared (Arc<str>), unique (Box<str>).
  • Read an unsafe block guarded by a // SAFETY: comment and state the invariant the comment is asserting.
  • Explain why reading a union field requires unsafe, and how the compile-time layout assertions keep that unsafe sound across future edits.
  • Write a runnable unsafe call whose SAFETY comment names a true invariant, the same move FieldStr::as_str makes for its inline arm.

New terms in this lesson (each is also expanded inline at the point you first need it):

  • unsafe: operations the compiler can’t verify; you promise the invariant instead.
  • invariant: a fact that must always hold; the SAFETY comment writes it down.
  • soundness: no reachable program state can make the unsafe misbehave.
  • FieldStr: the 24-byte, three-arm string behind every record field.

A field string is one of three things, and which one is a memory-cost decision:

  • inline: short values (≤ 23 bytes: ids, codes, flags) live in the struct, zero heap allocation;
  • shared: longer values that repeat or get cloned across stages are Arc<str>-backed, so a clone is an O(1) refcount bump, not a copy;
  • unique: long values flagged unique by the schema are Box<str>-backed, dropping the ~16-byte Arc refcount header that’s pure waste when a value never repeats (UUIDs, addresses, notes).

All three share one 24-byte footprint via a private union, discriminated by a single trailing tag byte:

clinker-record ·field_str.rs ·FieldStr type @19acdcb4
#[repr(C)]
union Repr {
inline: Inline, // [u8; 23] of UTF-8 + a tag byte
heap: HeapHeader, // ptr + len + padding + the same tag byte
}
pub struct FieldStr {
repr: Repr, // inline bytes, an Arc<str> (shared), or a Box<str> (unique)
}
FieldStr — 24 bytes, two overlapping arms (a union), ONE shared tag byte:
inline: [ 23 bytes of UTF-8 ........................ ][ tag = len 0..=23 ]
heap: [ ptr: *const u8 ][ len: usize ][ padding ...][ tag = 0xFF/0xFE ]
^ same offset (INLINE_CAP) in both arms

The trailing byte does double duty: for the inline arm it’s the length (always ≤ 23); for the heap arms it’s a sentinel (0xFF shared, 0xFE unique) chosen above 23 so it can never be mistaken for an inline length. That overlap is the trick that hits 24 bytes, and it’s the reason unsafe is unavoidable: reading a field of a union is unsafe, because the compiler can’t know which arm is currently valid.

clinker-record ·field_str.rs ·INLINE_CAP doc @19acdcb4

Here’s the read path. as_str checks the tag, then reads the matching arm, each read in an unsafe block prefaced by a // SAFETY: comment stating the invariant that makes it sound:

pub fn as_str(&self) -> &str {
let tag = self.tag();
if tag <= INLINE_CAP as u8 {
// SAFETY: inline arm — `tag` is the byte length (<= INLINE_CAP), and the
// constructor only ever wrote valid UTF-8 into `data[..len]`.
unsafe {
let inline = &self.repr.inline;
std::str::from_utf8_unchecked(&inline.data[..tag as usize])
}
} else {
// SAFETY: heap arm — ptr/len came from Arc::into_raw / Box::into_raw on a
// `str`, so the bytes are live, owned by this FieldStr, and valid UTF-8.
unsafe {
let heap = &self.repr.heap;
let bytes = std::slice::from_raw_parts(heap.ptr, heap.len);
std::str::from_utf8_unchecked(bytes)
}
}
}

unsafe doesn’t mean “unchecked and hope.” It means the compiler can’t verify this, so I’m asserting an invariant I’m responsible for. The SAFETY comment is that assertion written down. The Drop impl is the same discipline in reverse: it reconstitutes the Arc/Box from the raw pointer to release exactly one ownership token, with a SAFETY note that each heap arm owns exactly one such token.

What makes it actually sound: layout assertions

Section titled “What makes it actually sound: layout assertions”

A SAFETY comment is a promise; the layout assertions are how the promise is kept. The whole design rests on the tag byte sitting at the same offset in both union arms, so reading the tag through either arm reads the live discriminant. A compile-time block pins exactly that:

const _: () = {
assert!(std::mem::size_of::<FieldStr>() == 24); // the Value-budget invariant
assert!(std::mem::offset_of!(Inline, tag) == INLINE_CAP); // tag offset coincides...
assert!(std::mem::offset_of!(HeapHeader, tag) == INLINE_CAP); // ...in both arms
assert!(TAG_SHARED as usize > INLINE_CAP); // sentinels can't be inline lengths
assert!(TAG_UNIQUE as usize > INLINE_CAP);
};

If anyone changes a field and breaks the layout, the build fails: the unsafe code can never run against a layout it wasn’t written for. And because the auto-derived Send/Sync can’t see through the raw pointer in the union, those are hand-written unsafe impls with their own SAFETY note (every arm’s backing is Send + Sync; the raw pointer is only ever an owned Arc/Box in disguise). A runtime test pins the size too:

clinker-record ·field_str.rs ·size_is_24_bytes test @19acdcb4

This is the responsible-unsafe pattern, and it’s worth internalising: keep the unsafe core small, pin every layout invariant it relies on with a static assertion, document each block’s SAFETY contract, and back it with a drop/round-trip test. That discipline is what gives the module its soundness. Unsafe earns its keep here (one allocation saved per short field, across billions of fields) precisely because it’s this tightly fenced.

You can’t write a union in a quick playground, but you can practise the core move at the heart of every arm of as_str: skip a check you can prove is unnecessary, and write the proof down as a SAFETY comment. We build that up one rung at a time.

Worked: the unchecked move, fully annotated

Section titled “Worked: the unchecked move, fully annotated”

Here is the inline arm’s move on its own. The bytes really are valid UTF-8, so the validation scan from_utf8 would run is provably redundant, and from_utf8_unchecked skips it. Run it and confirm the unchecked and checked results agree.

rust // editable

The unsafe block is sound because the SAFETY comment’s claim is true: the bytes really are valid UTF-8. Feed from_utf8_unchecked genuinely invalid bytes and you’d have undefined behaviour with no error; the entire responsibility moves from the compiler to you, which is why the invariant must be real and written down.

Below, a constructor builds the bytes and the unsafe call is already there, but the // SAFETY: line is a TODO. A SAFETY comment must name the specific invariant that makes the unchecked call sound: here, why are bytes guaranteed to be valid UTF-8? Fill it in.

rust // editable
💡 Hint 1
A SAFETY comment points at the source of the guarantee, not at “trust me”. Here the only thing ever written into buf came from str::as_bytes(). What does that guarantee about the bytes?
Show solution
// SAFETY: every byte in `buf` was copied from `&str::as_bytes()`, which always
// yields valid UTF-8, and nothing else was written — so the whole buffer is a
// valid UTF-8 sequence and skipping the validation scan is sound.

The shape matches FieldStr::as_str exactly: the safety of the unchecked read rests entirely on a fact the constructor established (only valid UTF-8 was ever stored). The comment’s job is to name that fact so a future reader (or a future you, editing the constructor) knows precisely what must stay true.

Your turn with much less scaffolding. Write first_word, which returns the first space-delimited word of a &str as a &str, using from_utf8_unchecked on the relevant byte slice. The exercise is about one thing: you supply the SAFETY comment naming a true invariant, the way every arm of as_str does. (A safe from_utf8(...).unwrap() would also work; here you deliberately practise the unsafe move with its proof, because that’s the discipline the engine relies on.)

rust // editable
Show solution
fn first_word(line: &str) -> &str {
let bytes = line.as_bytes();
let end = bytes.iter().position(|&b| b == b' ').unwrap_or(bytes.len());
// SAFETY: `line` is already a &str (valid UTF-8), and a space (0x20) is an
// ASCII byte — never the middle of a multi-byte char — so `end` lands on a
// char boundary and `bytes[..end]` is itself a valid UTF-8 sequence.
unsafe { std::str::from_utf8_unchecked(&bytes[..end]) }
}

The invariant here is subtler than “it came from a &str”: it’s that the split point lands on a char boundary. Because a space is a single ASCII byte, it can never appear inside a multi-byte UTF-8 sequence, so slicing at it is safe. Naming that, rather than settling for “trust me”, is what makes the SAFETY comment do real work. This is the same care FieldStr takes: the inline arm only slices at tag because the constructor guarantees data[..tag] is a whole, valid UTF-8 string.

That deal unsafe offers (you keep the invariant true, the compiler steps aside) is the one FieldStr takes only where the payoff is large and the invariant is pinned by an assertion.

// quick check

What makes FieldStr's unsafe union reads sound rather than reckless?

You’ve reached the floor of the data layer. The final Execution & Memory lesson, Benchmark & measure memory, turns the lens around: how do you measure all this, the per-value cost model, the benchmark suite, and a custom allocator that counts every byte?

Go deeper on the Rust (optional, one-directional, for the building blocks FieldStr’s arms are made of, taught from first principles in The Rust Book):