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.
- StructUnion makes memory layout part of the developer experience by generating a visible byte map alongside the union itself.
- Its core trick is a two-zone explicit-layout model that packs variants into a value type without pretending unsafe overlap is always acceptable.
- The generator behaves like production Roslyn infrastructure, using incremental stages, cached models, modular emitters, and diagnostics to stay editor-friendly.
- StructUnion competes on control and visibility, not abstraction, so it favors performance-sensitive C# code over convenience wrappers.
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
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
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
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
| Approach | Allocation behavior | Memory density | Exhaustiveness | API ergonomics | Safety with reference-bearing value types | Debuggability |
|---|---|---|---|---|---|---|
| StructUnion | Zero-allocation in the happy path | High, with a tag plus packed payload | Strong, through generated match surfaces | Good if you accept generated types | Explicitly handled with safe fallback | High, because the byte map is visible |
| OneOf | Usually low overhead, but wrapper based | Moderate | Good at the type level | Very familiar to C# developers | Depends on the wrapped variants | Solid, but less layout transparency |
| Inheritance | Allocates per object instance | Low, because each variant is a separate object | Weak unless you enforce it manually | Natural for OO code, less so for data | Safe by runtime rules, but less compact | Easy to inspect, harder to reason about density |
| Hand-rolled tagged union | Can be zero-allocation, if you get it right | Potentially high, but fragile | As strong as your discipline | Verbose and easy to get wrong | Entirely on you | Depends 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.