2026-03-19 14:52:12 +03:00
<p align="center">
<img src="images/logo.png" alt="arpack logo" width="240">
</p>
# 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)
- **Enums** — `type 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
## Installation
``` bash
2026-03-23 09:47:14 +03:00
go install github.com/edmand46/arpack/cmd/arpack@latest
2026-03-19 14:52:12 +03:00
```
## Usage
``` bash
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:
``` go
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:
``` go
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
``` 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#
``` csharp
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`
## Running Tests
``` bash
2026-03-23 09:50:10 +03:00
# Unit tests
2026-03-19 15:16:04 +03:00
go test ./parser/... ./generator/...
2026-03-19 14:52:12 +03:00
2026-03-23 09:50:10 +03:00
# End-to-end cross-language tests
2026-03-19 15:16:04 +03:00
go test ./e2e/...
2026-03-19 14:52:12 +03:00
```