2026-03-23 12:53:54 +03:00
2026-03-23 12:52:30 +03:00
2026-03-23 09:47:14 +03:00
2026-03-23 09:47:14 +03:00
2026-03-23 09:47:14 +03:00
2026-03-19 14:52:12 +03:00
2026-03-23 09:47:14 +03:00
2026-03-19 14:52:12 +03:00
2026-03-23 12:52:30 +03:00
2026-03-19 14:52:12 +03:00
2026-03-23 12:52:30 +03:00
2026-03-23 12:52:30 +03:00
2026-03-23 12:52:30 +03:00
2026-03-23 12:52:30 +03:00
2026-03-23 12:53:54 +03:00

arpack logo

ArPack

Binary serialization code generator for Go and C#. Define messages once as Go structs — get zero-allocation Marshal/Unmarshal for Go and unsafe pointer-based Serialize/Deserialize for C#.

Features

  • Single source of truth — define messages in Go, generate both Go and C# code
  • Float quantization — compress float32/float64 to 8 or 16 bits with a pack struct tag
  • Boolean packing — consecutive bool fields are packed into single bytes (up to 8 per byte)
  • Enumstype Opcode uint16 + const block becomes a C# enum
  • Nested types, fixed arrays, slices — full support for complex message structures
  • Cross-language binary compatibility — Go and C# produce identical wire formats

When to use

ArPack is designed for real-time multiplayer games and other latency-sensitive systems where a Go backend talks to a C# client over a binary protocol.

Typical setups:

  • Nakama + Unity — define all network messages in Go, generate C# structs for Unity. Both sides share the exact same wire format with no reflection or boxing.
  • Custom Go game server + Unity — roll your own server without pulling in a serialization framework. ArPack generates plain Marshal/Unmarshal methods with zero allocations on the hot path.
  • Any Go service + .NET client — works anywhere you control both ends and want a compact binary protocol without Protobuf's runtime overhead or code-gen complexity.

ArPack is a poor fit if you need schema evolution (adding/removing fields without redeploying both sides) — use Protobuf or FlatBuffers instead.

Installation

go install github.com/edmand46/arpack/cmd/arpack@latest

Usage

arpack -in messages.go -out-go ./gen -out-cs ../Unity/Assets/Scripts
Flag Description
-in Input Go file with struct definitions (required)
-out-go Output directory for generated Go code
-out-cs Output directory for generated C# code
-cs-namespace C# namespace (default: Arpack.Messages)

Output files:

  • Go: {name}_gen.go
  • C#: {Name}.gen.cs

Schema Definition

Messages are defined as Go structs in a single .go file:

package messages

// Quantized 3D vector — 6 bytes instead of 12
type Vector3 struct {
    X float32 `pack:"min=-500,max=500,bits=16"`
    Y float32 `pack:"min=-500,max=500,bits=16"`
    Z float32 `pack:"min=-500,max=500,bits=16"`
}

// Enum
type Opcode uint16

const (
    OpcodeUnknown   Opcode = iota
    OpcodeAuthorize
    OpcodeJoinRoom
)

type MoveMessage struct {
    Position  Vector3    // nested type
    Velocity  [3]float32 // fixed-length array
    Waypoints []Vector3  // variable-length slice
    PlayerID  uint32
    Active    bool       // 3 consecutive bools →
    Visible   bool       //   packed into 1 byte
    Ghost     bool
    Name      string
}

Supported Types

Type Wire Size
bool (packed) 1 bit (up to 8 per byte)
int8, uint8 1 byte
int16, uint16 2 bytes
int32, uint32, float32 4 bytes
int64, uint64, float64 8 bytes
string 2-byte length prefix + UTF-8
[N]T N × sizeof(T)
[]T 2-byte length prefix + N × sizeof(T)

Float Quantization

Use the pack struct tag to compress floats:

X float32 `pack:"min=-500,max=500,bits=16"`  // 2 bytes instead of 4
Y float32 `pack:"min=0,max=1,bits=8"`        // 1 byte instead of 4
Parameter Description
min Minimum expected value
max Maximum expected value
bits Target size: 8 (uint8) or 16 (uint16)

Values are linearly mapped: encoded = (value - min) / (max - min) * maxUint.

Generated Code

Go

func (m *MoveMessage) Marshal(buf []byte) []byte
func (m *MoveMessage) Unmarshal(data []byte) (int, error)

Marshal appends to the buffer and returns it. Unmarshal reads from the buffer and returns bytes consumed.

C#

public unsafe int Serialize(byte* buffer)
public static unsafe int Deserialize(byte* buffer, out MoveMessage msg)

Uses unsafe pointers for zero-copy serialization. Returns bytes written/consumed.

Wire Format

  • Little-endian byte order
  • No message framing — fields are written in declaration order
  • Variable-length fields (string, []T) prefixed with uint16 length
  • Booleans packed as bitfields (LSB first, up to 8 per byte)
  • Quantized floats stored as uint8 or uint16

Benchmarks

Go Results (M3 Max)

BenchmarkArPack_Marshal-16        382568360    9.5 ns/op    5065 MB/s    0 B/op    0 allocs/op
BenchmarkArPack_Unmarshal-16       98895892   34.6 ns/op    1388 MB/s   40 B/op    2 allocs/op
BenchmarkProto_Marshal-16          21989466  163.6 ns/op     416 MB/s    0 B/op    0 allocs/op
BenchmarkProto_Unmarshal-16        13950333  256.9 ns/op     265 MB/s  248 B/op    7 allocs/op
BenchmarkFlatBuffers_Marshal-16    16297458  221.4 ns/op     687 MB/s    0 B/op    0 allocs/op
BenchmarkFlatBuffers_Unmarshal-16  56095480   64.8 ns/op    2345 MB/s   24 B/op    1 allocs/op
Format Size
ArPack 48 bytes
Protobuf 68 bytes
FlatBuffers 152 bytes
go test ./benchmarks/... -bench=. -benchmem

Unity Mono (M3 Max)

ArPack Serialize:           96.7 ns/op |    0 B/op
ArPack Deserialize:        205.4 ns/op |    0 B/op
Proto Serialize (alloc):   930.2 ns/op |    0 B/op
Proto Deserialize (alloc): 1621.2 ns/op |   29 B/op
Proto Serialize (reuse):   652.7 ns/op |    0 B/op

ArPack serialize is ~10× faster than Protobuf in Unity. Protobuf deserialize allocates on every call — a GC pressure source in hot game loops. ArPack deserialize is zero-alloc.

make gen-unity
# then attach BenchmarkRunner to any GameObject in SampleScene and press Play

Running Tests

# Unit tests
go test ./parser/... ./generator/...

# End-to-end cross-language tests
go test ./e2e/...
S
Description
No description provided
Readme MIT 685 KiB
Languages
Go 74.3%
C# 25%
Makefile 0.7%