Binary layout code generator for Go. Generates type-safe marshal/unmarshal code for fixed-size buffers with bidirectional packing.
Built for B-tree pages, database records, network protocols - anywhere you need deterministic memory layout without reflection overhead.
- Bidirectional packing: Fixed fields, forward-growing regions (
start-end), backward-growing regions (end-start) - Zero-allocation unmarshaling: Buffer reuse via capacity checks (5.5x faster)
- True zero-copy mode: Direct memory access with
unsafe.Pointer, no allocations - Aligned buffers: Generate aligned buffers for O_DIRECT I/O (512/4096-byte alignment)
- Custom allocators: Integrate with buffer pools via
allocator=annotation - Compile-time layout validation: Collision detection, boundary calculation, count field validation, struct field requirements
- Type-safe generated code:
encoding/binaryorunsafedepending on mode
// page.go
package btree
// @layout size=4096
type Page struct {
Header uint16 `layout:"@0"`
Body []byte `layout:"start-end"`
Footer uint64 `layout:"@4088"`
}Generate code:
$ layout generate page.go
Generated: page_layout.go
- Page.MarshalLayout() ([]byte, error)
- Page.UnmarshalLayout([]byte) errorUse generated methods:
page := &Page{
Header: 42,
Body: []byte{1, 2, 3},
Footer: 0xdeadbeef,
}
buf, _ := page.MarshalLayout() // []byte of length 4096
page2 := &Page{}
page2.UnmarshalLayout(buf) // Zero allocations if pre-allocatedPlace field at byte offset N.
Header uint64 `layout:"@0"` // [0, 8)
Footer uint64 `layout:"@4088"` // [4088, 4096)Grow from previous field/offset towards end of buffer.
// @layout size=4096
type Page struct {
Header uint16 `layout:"@0"` // [0, 2)
Body []byte `layout:"start-end"` // [2, 4088) - fills to Footer
Footer uint64 `layout:"@4088"` // [4088, 4096)
}Grow from end of buffer backwards.
// @layout size=4096
type Page struct {
Header uint16 `layout:"@0"` // [0, 2)
Keys []byte `layout:"end-start"` // [4096, 2) - grows backward
}Start dynamic region at specific offset.
Values []byte `layout:"@8,start-end"` // Start at offset 8, grow forward
Keys []byte `layout:"@4096,end-start"` // Start at 4096, grow backwardExplicit slice length (required when boundary is ambiguous).
type Page struct {
NumKeys uint16 `layout:"@0"`
Keys []byte `layout:"start-end,count=NumKeys"`
}Without count field, length is implicit from boundaries:
// Body length = 4088 - 2 = 4086 bytes
Body []byte `layout:"start-end"` // Fills from Header to FooterRequired at type level to specify buffer size:
// @layout size=4096
type Page struct { ... }
// @layout size=8192 endian=big
type Record struct { ... }Parameters:
size=N: Buffer size in bytes (required)endian=little|big: Byte order (default: little)mode=copy|zerocopy: Marshal/unmarshal mode (default: copy)align=N: Buffer alignment in bytes (power of 2, requires mode=zerocopy)allocator=FuncName: Custom allocator function (requires mode=zerocopy with align)
True zero-copy I/O: no allocations, slice directly into embedded buffer.
// @layout size=4096 mode=zerocopy
type Page struct {
buf [4096]byte // Required: fixed-size buffer
Header uint16 `layout:"@0"`
Body []byte `layout:"start-end"`
Footer uint64 `layout:"@4088"`
}Required field: buf [size]byte matching annotation size
Generated methods:
func (p *Page) MarshalLayout() ([]byte, error) // Writes to p.buf using unsafe
func (p *Page) UnmarshalLayout() error // Reads from p.buf, no params
func (p *Page) LoadFrom(r io.Reader) error // Helper: read then unmarshal
func (p *Page) WriteTo(w io.Writer) error // Helper: marshal then writeUsage:
page := &Page{}
// Direct load: complete control
n, err := io.ReadFull(disk, page.buf[:])
page.UnmarshalLayout()
// Or use helper
page.LoadFrom(disk)Performance: No allocations, direct memory access via unsafe.Pointer.
For O_DIRECT I/O requiring aligned buffers:
// @layout size=4096 mode=zerocopy align=512
type Page struct {
backing []byte // Required: over-allocated buffer
buf []byte // Required: aligned slice
Header uint16 `layout:"@0"`
Body []byte `layout:"start-end"`
Footer uint64 `layout:"@4088"`
}Required fields:
backing []byte- Over-allocated buffer for alignmentbuf []byte- Slice into aligned region
Generated constructor:
func New() *Page {
p := &Page{}
// Allocate size + (align-1) to guarantee alignment
p.backing = make([]byte, 4607) // 4096 + 511
// Find 512-byte aligned offset
addr := uintptr(unsafe.Pointer(&p.backing[0]))
offset := int(((addr + 511) &^ 511) - addr)
// Slice aligned region
p.buf = p.backing[offset : offset+4096]
return p
}Usage:
page := New() // Allocates aligned buffer
// Open file with O_DIRECT
file, _ := os.OpenFile("data.db", os.O_RDWR|syscall.O_DIRECT, 0644)
// Direct I/O - no kernel buffering
io.ReadFull(file, page.buf[:])
page.UnmarshalLayout()
// Modify and write back
page.Header = 42
page.MarshalLayout()
file.Write(page.buf[:])Use buffer pools with custom allocators:
var pagePool = sync.Pool{
New: func() interface{} {
// Allocate 4096 + 511 for 512-byte alignment
return make([]byte, 4607)
},
}
func AllocateAlignedPage() []byte {
return pagePool.Get().([]byte)
}
// @layout size=4096 mode=zerocopy align=512 allocator=AllocateAlignedPage
type Page struct {
backing []byte
buf []byte
Header uint16 `layout:"@0"`
Body []byte `layout:"start-end"`
Footer uint64 `layout:"@4088"`
}Generated code includes validation:
func New() *Page {
p := &Page{}
// IMPORTANT: AllocateAlignedPage() must return a buffer of at least 4607 bytes
// (4096 bytes for data + 511 bytes for 512-byte alignment)
p.backing = AllocateAlignedPage()
// Validate buffer size to prevent out-of-bounds access
if len(p.backing) < 4607 {
panic(fmt.Sprintf("AllocateAlignedPage returned buffer of %d bytes, need at least 4607", len(p.backing)))
}
// Find aligned offset...
}Usage:
page := New() // Gets buffer from pool
// Use page...
io.ReadFull(disk, page.buf[:])
page.UnmarshalLayout()
// Return to pool when done
pagePool.Put(page.backing)| Mode | Alignment | Required Fields |
|---|---|---|
copy |
N/A | None (generated code allocates) |
zerocopy |
None | buf [size]byte |
zerocopy |
Yes | backing []byte + buf []byte |
Validation: Parser checks struct has required fields, prints warning if missing.
uint8,uint16,uint32,uint64int8,int16,int32,int64byte,bool[N]byte- byte arrays- Struct types with
@layoutannotation - Type aliases to primitive types (e.g.,
type PageID uint64)
[]byte- byte slices (with or without count)[]StructType- struct slices (requires count field)[][]byte- indirect slices via metadata (see Indirect Slices)
Define custom types as aliases to primitives for type safety:
type PageID uint64
type Offset uint32
// @layout size=16
type PageHeader struct {
ID PageID `layout:"@0"`
Next PageID `layout:"@8"`
}Generated code handles automatic type conversion:
// Marshal: cast to underlying type
binary.LittleEndian.PutUint64(buf[0:8], uint64(p.ID))
// Unmarshal: cast back to alias type
p.ID = PageID(binary.LittleEndian.Uint64(buf[0:8]))Supported: Type aliases to any primitive integer type (uint8/16/32/64, int8/16/32/64).
Not supported: Type aliases to structs, slices, or complex types.
Input:
// @layout size=4096
type Page struct {
Header uint16 `layout:"@0"`
Body []byte `layout:"start-end"`
Footer uint64 `layout:"@4088"`
}Output:
func (p *Page) MarshalLayout() ([]byte, error) {
buf := make([]byte, 4096)
// Header: uint16 at [0, 2)
binary.LittleEndian.PutUint16(buf[0:2], p.Header)
// Body: []byte at [2, 4088)
offset := 2
for i := range p.Body {
if offset >= 4088 {
return nil, fmt.Errorf("Body collision at offset %d", offset)
}
buf[offset] = p.Body[i]
offset++
}
// Footer: uint64 at [4088, 4096)
binary.LittleEndian.PutUint64(buf[4088:4096], p.Footer)
return buf, nil
}
func (p *Page) UnmarshalLayout(buf []byte) error {
if len(buf) != 4096 {
return fmt.Errorf("expected 4096 bytes, got %d", len(buf))
}
// Header: uint16 at [0, 2)
p.Header = binary.LittleEndian.Uint16(buf[0:2])
// Body: []byte at [2, 4088)
bLen := 4088 - 2
// Reuse buffer if capacity allows
if cap(p.Body) >= bLen {
p.Body = p.Body[:bLen]
} else {
p.Body = make([]byte, bLen)
}
copy(p.Body, buf[2:4088])
// Footer: uint64 at [4088, 4096)
p.Footer = binary.LittleEndian.Uint64(buf[4088:4096])
return nil
}Zero-allocation unmarshaling via capacity checks:
// One-time allocation
page := &Page{
Body: make([]byte, 0, 4096), // Pre-allocate capacity
}
// Subsequent unmarshals reuse backing array
page.UnmarshalLayout(diskBuf1) // No allocation
page.UnmarshalLayout(diskBuf2) // No allocation
page.UnmarshalLayout(diskBuf3) // No allocation// @layout size=4096
type BTreePage struct {
NumKeys uint16 `layout:"@0"`
Keys []uint32 `layout:"end-start,count=NumKeys"`
NumVals uint16 `layout:"@2"`
Values []byte `layout:"start-end,count=NumVals"`
}Layout:
[0 2 4 ? 4096]
[NumKeys|NumVals|Values---> <---Keys ]
// @layout size=1024 endian=big
type Packet struct {
Magic uint32 `layout:"@0"`
Length uint16 `layout:"@4"`
Payload []byte `layout:"start-end,count=Length"`
Checksum uint32 `layout:"@1020"`
}// @layout size=8192
type Record struct {
RecordID uint64 `layout:"@0"`
Flags uint16 `layout:"@8"`
Data []byte `layout:"start-end"`
Footer [16]byte `layout:"@8176"` // Last 16 bytes
}Count field only required when dynamic region has no fixed boundary:
// NO count needed - Footer provides boundary
Body []byte `layout:"start-end"` // [2, 4088)
Footer uint64 `layout:"@4088"`
// Count REQUIRED - no fixed boundary after Values
Values []byte `layout:"start-end,count=NumValues"` // Length unknown
Keys []byte `layout:"end-start"` // Also dynamicReference struct fields using dot notation:
// @layout size=4096
type LeafPage struct {
Header PageHeader `layout:"@0"`
Elements []LeafElement `layout:"start-end,count=Header.NumKeys"`
Data []byte `layout:"end-start"`
}Supports one level of nesting: Header.NumKeys is valid, A.B.C is not.
Count fields must be integer types and sized appropriately:
// ✓ Valid
NumKeys uint16 `layout:"@0"`
Keys []byte `layout:"start-end,count=NumKeys"`
// ✗ Invalid - wrong type
NumKeys string `layout:"@0"`
Keys []byte `layout:"start-end,count=NumKeys"` // Error: count field must be integer
// ✗ Invalid - overflow
NumKeys uint8 `layout:"@0"` // Max 255
Keys [4000]byte `layout:"start-end,count=NumKeys"` // Error: max 4000 elements exceeds uint8Compile-time checks:
- Count field type: Must be
int8/16/32/64oruint8/16/32/64 - Count capacity: Validates count type can hold maximum possible elements
[]StructType requires count field (always):
// @layout
type LeafElement struct {
Key uint32 `layout:"@0"`
Offset uint32 `layout:"@4"`
}
// @layout size=4096
type LeafPage struct {
Header PageHeader `layout:"@0"`
Elements []LeafElement `layout:"@24,start-end,count=Header.NumKeys"`
}Generated code calls MarshalLayout/UnmarshalLayout on each element.
See COUNT_SEMANTICS.md for details.
[][]byte fields with metadata indirection - slices backed by a single data region with offsets stored in a separate metadata array.
Use case: B-tree leaf pages where keys/values are variable-length and stored in a packed data region.
Keys [][]byte `layout:"from=Elements,offset=KeyOffset,size=KeySize,region=Data"`Required parameters:
from=FieldName- Source slice containing metadata (must be[]StructType)offset=FieldName- Field in source elements holding offset (must be integer type)size=FieldName- Field in source elements holding size (must be integer type)region=FieldName- Data region field (must be[]byte)
// @layout
type LeafElement struct {
KeyOffset uint32 `layout:"@0"`
KeySize uint32 `layout:"@4"`
ValueOffset uint32 `layout:"@8"`
ValueSize uint32 `layout:"@12"`
}
// @layout size=4096
type LeafPage struct {
Header PageHeader `layout:"@0"`
Elements []LeafElement `layout:"@24,start-end,count=Header.NumKeys"`
Data []byte `layout:"end-start"`
Keys [][]byte `layout:"from=Elements,offset=KeyOffset,size=KeySize,region=Data"`
Values [][]byte `layout:"from=Elements,offset=ValueOffset,size=ValueSize,region=Data"`
}Unmarshal: Loop through metadata, slice into buffer
// Keys: [][]byte from=Elements offset=KeyOffset size=KeySize region=Data
for i := range p.Elements {
offset := int(p.Elements[i].KeyOffset)
size := int(p.Elements[i].KeySize)
p.Keys[i] = buf[offset:offset+size]
}Marshal: Pack backward, update metadata
// Keys: [][]byte packed backward into Data, updating Elements metadata
offset = 4096
for i := len(p.Keys) - 1; i >= 0; i-- {
size := len(p.Keys[i])
offset -= size
copy(buf[offset:offset+size], p.Keys[i])
p.Elements[i].KeyOffset = uint32(offset)
p.Elements[i].KeySize = uint32(size)
}Offsets: Absolute offsets into buffer (not relative to Data region).
Memory: Zero-copy - Keys[i] slices directly into buf, no allocation.
Compile-time checks:
- Overlapping fixed fields:
collision: Field1 [0, 8) overlaps Field2 [4, 12) - Missing count fields:
field 'Body' requires count= (no fixed boundary) - Invalid count types:
count field 'Len' must be int/uint 8/16/32/64, got: string - Count capacity overflow:
count field 'Count' (type uint8, max 255) cannot hold max 512 elements - Nested count field errors:
count field 'A.B.C' has invalid nested reference (only one level supported) - Indirect slice validation:
field 'Keys': source field 'Elements' must be a struct slice, not []byte - Out of bounds:
field [4088, 4100) exceeds buffer size 4096
Runtime checks:
- Collision detection:
return nil, fmt.Errorf("Body collision at offset %d", offset) - Count mismatches:
return nil, fmt.Errorf("Body length mismatch: have %d, want %d") - Buffer size validation:
return fmt.Errorf("expected 4096 bytes, got %d", len(buf))
go install github.com/alexhholmes/layout/cmd/layout@latestAfter install, layout command available globally.
//go:generate layout generate page.golayout generate page.go # Generate page_layout.go
layout generate btree/*.go # Generate for packageMIT