StructUnion: the C# source generator that turns memory layout into a discriminated union

A Roslyn generator for zero-allocation unions, explicit-layout tricks, safe fallbacks, and an ASCII byte map right in your IDE.

9 min read • View on GitHub • More from kerem-acer

A wide workshop scene in black ink on white paper, where a metal press stamps several variant blocks into one compact mold beside a tiny byte ruler. It explains the article’s core idea: StructUnion treats memory layout as something you can see, not just something the runtime hides.
StructUnion’s pitch is visual as much as technical: one value type, several variants, and the bytes made legible.
Key Takeaways

C# has plenty of ways to model variants. StructUnion is interesting because it does not stop at modeling. It turns the layout itself into part of the interface, then explains that layout back to you in the IDE.

The union that shows you its byte map

The most memorable part of StructUnion is not the attribute or the generated type. It is the ASCII memory map in the generated XML docs, which tells you where the tag byte lives and how the payload is packed. That is a small feature with a big consequence: the generator is not just producing code, it is producing explanation.

That matters because low-level correctness is usually opaque. If a union is dense, you have to trust it. StructUnion tries to earn that trust by making the bytes visible where you already work, in tooltips and generated source.

Why C# still makes discriminated unions awkward

C# does not give you native discriminated unions, so developers reach for substitutes. Class hierarchies give you polymorphism, but they allocate and push decisions into virtual dispatch. Libraries like OneOf improve ergonomics, but they still ask you to think in wrapper types instead of raw value layout.

The hand-rolled alternative is worse in a different way. You can build your own tag plus payload structure, but then you own every edge case: exhaustiveness, alignment, validation, and the accidental heap cost of the wrong abstraction. StructUnion aims at that gap. It gives performance-sensitive C# code a denser representation without making the developer manually police the bytes.

The two-zone layout trick

This is the heart of StructUnion’s design: the layout engine separates reference-bearing data from value data, then changes course when the CLR would not permit a clever overlap.

The interesting bit is not that StructUnion uses explicit layout. It is that it treats explicit layout like a constrained puzzle, not a loophole. Reference-bearing fields and pure value fields do not get mixed casually, because the CLR’s rules around GC-tracked data are real, not decorative.

That is why the repository talks in zones. One zone handles references, the other handles values, and the tag byte decides which view of the union is active. The design is dense, but it is not reckless.

When the generator refuses to over-optimize

A packed mechanical assembly on the left begins to split apart on the right when one inner part turns from a solid block into a fragile linked chain. It explains the fallback strategy: when a managed value type would make explicit overlap unsafe, StructUnion backs away from the clever layout and chooses safety.
StructUnion is strict about density, but it is more interested in correctness than in winning a microbenchmark.

That safety valve is one of the repo’s best signs of maturity. The generator can detect managed value types, including the awkward cases where a value type still contains references, and then fall back to a safer layout strategy instead of pretending the runtime will forgive it.

This is the difference between a demo and a tool. A demo optimizes for the happy path. StructUnion is willing to give up some density when the type system says density would become a lie.

How the generator stays fast enough for the IDE

A clean left-to-right flow of stages, from syntax tree to cached transforms to layout calculation to emitters and final generated source. It explains why StructUnion feels responsive in the editor: the Roslyn pipeline is built to do less work when only part of the project changes.
StructUnion’s architecture is not just about output quality. It is about keeping source generation cheap enough to live inside the IDE.

StructUnion is built as an incremental Roslyn generator, which means it does not need to recompute everything every time a file changes. That matters more than it sounds, because source generators run in the same environment as your editor. If they are sloppy, they make the whole project feel sluggish.

The repository’s shape reflects that constraint. Parsing, modeling, and emission are split into separate layers, so the generator can cache transforms, reuse immutable models, and keep the expensive reasoning about layout isolated from the string-building step that writes the final code.

Match APIs, diagnostics, and the line between fast and usable

The matching API shows the same bias toward practical performance. StructUnion includes stateful overloads so callers can pass context without creating a closure and paying for a hidden heap allocation. That is a small ergonomic choice with a real runtime effect.

public TResult Match<TState, TResult>(
    TState state,
    Func<TState, T1, TResult> case1,
    Func<TState, T2, TResult> case2
)

The important part is not the signature alone. It is the combination of zero-allocation intent and diagnostics that keep the API hard to misuse. The generator does not just emit fast code. It tries to make the wrong code obvious at compile time.

That is the difference between a clever library and a dependable one. Fast code is easy to admire. Fast code that fails early, explains itself, and stays readable in the IDE is what people can build on.

StructUnion versus the obvious alternatives

ApproachAllocation behaviorMemory densityExhaustivenessAPI ergonomicsSafety with reference-bearing value typesDebuggability
StructUnionZero-allocation in the happy pathHigh, with a tag plus packed payloadStrong, through generated match surfacesGood if you accept generated typesExplicitly handled with safe fallbackHigh, because the byte map is visible
OneOfUsually low overhead, but wrapper basedModerateGood at the type levelVery familiar to C# developersDepends on the wrapped variantsSolid, but less layout transparency
InheritanceAllocates per object instanceLow, because each variant is a separate objectWeak unless you enforce it manuallyNatural for OO code, less so for dataSafe by runtime rules, but less compactEasy to inspect, harder to reason about density
Hand-rolled tagged unionCan be zero-allocation, if you get it rightPotentially high, but fragileAs strong as your disciplineVerbose and easy to get wrongEntirely on youDepends on how much documentation you add

StructUnion is not the universal answer. It is the answer for teams that care about bytes, want the compiler to guard the contract, and are willing to trade some abstraction for explicit control. If your problem is modeling business states cleanly, other tools may be simpler. If your problem is making a union dense, inspectable, and cheap to move around, StructUnion has a sharper point.