6.0 KiB
AcBinary Wire Format
Complete wire format specification for the AcBinary serializer. Source of truth: Serializers/Binaries/BinaryTypeCode.cs.
For advanced features (compact encoding, string interning, reference tracking, property ordering) see
BINARY_FEATURES.md. For configuration options, presets, and option interactions seeBINARY_OPTIONS.md.
Stream Layout
[version : 1 byte] [flags : 1 byte] [cacheCount : VarUInt?] [payload...]
- version —
FormatVersion = 1(current). - flags — See Header Flags.
- cacheCount — Present only when
HeaderFlag_HasCacheCountis set. Number of type wrapper slots used by serializer.
Header Flags
The flags byte uses 0x90 (144) as base with bit flags in the lower nibble:
| Bit | Mask | Flag | Meaning |
|---|---|---|---|
| 0 | 0x01 |
Metadata | Property hash metadata included (cross-type deserialization) |
| 1 | 0x02 |
RefHandling_OnlyId | Reference tracking for IId objects only |
| 2 | 0x04 |
RefHandling_All | Reference tracking for all objects (always combined with bit 1) |
| 3 | 0x08 |
HasCacheCount | VarUInt cache count follows the flags byte |
Reference handling modes: None = 0x00, OnlyId = 0x02, All = 0x06 (bits 1+2).
Variable-Length Encoding
VarUInt (unsigned)
LEB128: 7 data bits per byte, MSB = continuation flag.
value < 128 → 1 byte [0xxxxxxx]
value < 16384 → 2 bytes [1xxxxxxx] [0xxxxxxx]
value < 2097152 → 3 bytes ...
(max 5 bytes for uint32)
VarInt (signed)
ZigZag encoding maps signed to unsigned, then LEB128:
encode: (value << 1) ^ (value >> 31)
decode: (raw >> 1) ^ -(raw & 1)
Maps: 0 → 0, -1 → 1, 1 → 2, -2 → 3, etc.
VarULong (unsigned 64-bit)
Same LEB128 encoding, max 10 bytes for uint64.
Type Markers
All markers defined in BinaryTypeCode.cs. SlotCount = 64.
FixObj (0–63)
Single-byte object type. The marker byte is the type slot index — no additional type identifier needed.
[FixObj(N)] [properties...]
Slot allocation: Slots 0–63 are reserved for runtime polymorphic types, assigned dynamically on first encounter during serialization. Source-generated (SGen) types receive slots starting at 64+ via AllocateWrapperSlot() (sequential, Interlocked.Increment). SGen slots are compile-time stable; runtime slots depend on serialization order.
Complex Types (64–71)
| Code | Name | Wire format |
|---|---|---|
| 64 | Object | [64] [VarUInt typeIndex] [properties...] |
| 65 | ObjectRef | [65] [VarUInt refCacheIndex] |
| 66 | Array | [66] [VarUInt count] [elements...] |
| 67 | Dictionary | [67] [VarUInt count] [key, value pairs...] |
| 68 | ByteArray | [68] [VarUInt length] [raw bytes] |
| 69 | ObjectWithMetadata | [69] [VarUInt typeIndex] [VarUInt hashCount] [hashes...] [properties...] |
| 70 | ObjectRefFirst | [70] [VarUInt refCacheIndex] [object body...] |
| 71 | ObjectWithMetadataRefFirst | [71] [VarUInt refCacheIndex] [metadata + properties...] |
Polymorphic Types (72–75)
Used when runtime type differs from declared property type and UseMetadata=false.
| Code | Name | Wire format |
|---|---|---|
| 72 | ObjectWithTypeName | [72] [UTF8 typeName] [inner marker] [body...] — prefix, inner Object/Array/Dict follows |
| 73 | ObjectWithTypeNameRefFirst | [73] [UTF8 typeName] [VarUInt refCacheIndex] [properties...] — combined, no inner marker |
| 74 | ObjectWithTypeIndex | [74] [VarUInt typeIndex] [inner marker] [body...] — prefix |
| 75 | ObjectWithTypeIndexRefFirst | [75] [VarUInt typeIndex] [VarUInt refCacheIndex] [properties...] — combined |
Second occurrence of a referenced polymorphic object uses plain ObjectRef(65) — no polymorphic prefix needed.
Primitives (76–90)
| Code | Name | Wire format |
|---|---|---|
| 76 | Null | [76] — no payload |
| 77 | True | [77] — no payload |
| 78 | False | [78] — no payload |
| 79 | Int8 | [79] [1 byte] |
| 80 | UInt8 | [80] [1 byte] |
| 81 | Int16 | [81] [VarInt] |
| 82 | UInt16 | [82] [VarUInt] |
| 83 | Int32 | [83] [VarInt] |
| 84 | UInt32 | [84] [VarUInt] |
| 85 | Int64 | [85] [VarLong] |
| 86 | UInt64 | [86] [VarULong] |
| 87 | Float32 | [87] [4 bytes IEEE 754] |
| 88 | Float64 | [88] [8 bytes IEEE 754] |
| 89 | Decimal | [89] [16 bytes] |
| 90 | Char | [90] [VarUInt] |
Strings (91–94)
| Code | Name | Wire format |
|---|---|---|
| 91 | String | [91] [VarUInt byteLength] [UTF-8 bytes] |
| 92 | StringInterned | [92] [VarUInt cacheIndex] — 2nd+ occurrence |
| 93 | StringEmpty | [93] — no payload |
| 94 | StringInternFirst | [94] [VarUInt cacheIndex] [VarUInt byteLength] [UTF-8 bytes] — 1st occurrence |
Date/Time (95–98)
| Code | Name | Wire format |
|---|---|---|
| 95 | DateTime | [95] [8 bytes ticks] |
| 96 | DateTimeOffset | [96] [8 bytes ticks] [VarInt offsetMinutes] |
| 97 | TimeSpan | [97] [VarLong ticks] |
| 98 | Guid | [98] [16 bytes] |
Other Markers
| Code | Name | Wire format |
|---|---|---|
| 99 | Enum | [99] [VarInt underlyingValue] |
| 100 | MetadataHeader | Legacy: implies RefHandling=true + metadata present |
| 101 | NoMetadataHeader | Legacy: implies RefHandling=true, no metadata |
| 102 | PropertySkip | [102] — marks skipped property (default/null value) |
FixStr (103–134)
Short ASCII strings encoded in a single marker byte + raw bytes (no length prefix):
[FixStrBase + byteLength] [ASCII bytes]
- Length range: 0–31 bytes (
FixStrBase=103,FixStrMax=134) - Saves 1 byte vs
Stringmarker + VarUInt length - Falls back to
String(91)if content is non-ASCII
TinyInt (192–255)
Single-byte integer encoding for small values:
value = marker - 192 - 16 (range: -16 to 47)
marker = value + 16 + 192 (64 values total)
Saves 2+ bytes vs Int32(83) + VarInt for frequently occurring small integers.