A tool for describing binary protocols with a simple DSL (PDL) and generating type-safe serialization/deserialization code for Go and C.
pdlc eliminates the need for manual coding of binary protocol handling logic. By defining your protocol structure in a human-readable DSL, you can automatically generate:
- Go structs with
Encode()
andDecode()
methods (endianness-aware) - Bitfield helper methods for Go
- C headers with packed structs and bitfield macros
- Type validation and boundary checks
This ensures consistency between protocol documentation and implementation, while reducing errors from manual byte manipulation.
A PDL file describes a protocol with one or more struct definitions, using this general structure:
protocol ProtocolName {
endian = be|le; // Global endianness (big-endian or little-endian)
struct StructName {
// Field definitions
}
// Additional struct definitions
}
PDL supports the following base types:
Type | Description |
---|---|
u8 |
8-bit unsigned integer |
u16 |
16-bit unsigned integer |
u32 |
32-bit unsigned integer |
u64 |
64-bit unsigned integer |
i8 |
8-bit signed integer |
i16 |
16-bit signed integer |
i32 |
32-bit signed integer |
i64 |
64-bit signed integer |
bytes |
Raw byte sequence |
bits8 |
8-bit container for bitfields |
bits16 |
16-bit container for bitfields |
bits32 |
32-bit container for bitfields |
bits64 |
64-bit container for bitfields |
A basic field definition follows this syntax:
FieldName : Type [@Offset] [endian=be|le] [array=Expr] [len=Expr] [const=Value];
@Offset
: Explicit byte offset (e.g.,@4
forces the field to start at byte 4)endian=be|le
: Override global endianness for this fieldarray=Expr
: Define as array with length from expression (e.g.,array=count
)len=Expr
: Forbytes
type, specify length (e.g.,len=payloadSize
)const=Value
: Mark as constant value (e.g.,const=0xAA
)
// Basic field
Version : u16;
// Field with explicit offset
Length : u32 @8;
// Little-endian field (overriding global big-endian)
Checksum : u16 endian=le;
// Fixed-size array
Flags : u8 array=4;
// Dynamic array (length determined by previous field)
Data : bytes array=DataLength;
// Constant field
Magic : u8 const=0x55;
Bitfields allow packing multiple values into a single integer:
FieldName : bitsN {
SubField1 : Width;
SubField2 : Width;
// Additional subfields
};
bitsN
must be one ofbits8
,bits16
,bits32
,bits64
Width
specifies the number of bits for each subfield- Total width of all subfields must equal N
- Subfields are packed left-to-right (LSB-first)
Status : bits16 {
Valid : 1; // 1 bit
Mode : 3; // 3 bits (total: 4)
ErrorCode : 5; // 5 bits (total: 9)
Reserved : 7; // 7 bits (total: 16)
};
Expressions can be used for offsets, array lengths, and length attributes, supporting:
- Previous field names (e.g.,
array=Count
) - Integers (e.g.,
len=1024
) - Arithmetic operations:
+
,-
,*
(left-to-right evaluation)
HeaderSize : u8 const=12;
Payload : bytes len=TotalSize - HeaderSize;
ItemCount : u8;
Items : u16 array=ItemCount * 2;
Buffer : bytes array=(Header.Length + 3) / 4; // Round up to 4-byte boundary
Structs can contain other structs as fields:
protocol NestedProtocol {
struct Header {
u16 Version;
u32 Length;
}
struct Message {
Header : Header; // Nested struct
u8 Type;
bytes Payload len=Header.Length - 5;
}
}
go install github.com/hootrhino/pdlc/cmd/pdlc@latest
pdlc --in protocol.pdl --lang go,c --out ./generated
--in
: Input PDL file path (required)--lang
: Comma-separated list of target languages (go
,c
)--out
: Output directory (default: current directory)
Generates ./out/go/proto/<name>_gen.go
containing:
- Struct definitions matching the PDL
Encode() ([]byte, error)
method for serializationDecode([]byte) error
method for deserialization- Bitfield helper methods (
Get<Field><Subfield>()
andSet<Field><Subfield>()
)
Generates ./out/c/<name>.h
containing:
- Packed struct definitions
- Bitfield access macros (
GET_<STRUCT>_<FIELD>_<SUBFIELD>()
andSET_*
) - Endianness conversion functions where needed
protocol Modbus {
endian = be;
struct Request {
TransactionID : u16;
ProtocolID : u16 const=0;
Length : u16;
UnitID : u8;
FunctionCode : u8;
struct Data {
StartAddress : u16;
Quantity : u16;
} Data;
}
struct Flags : bits16 {
Response : 1;
Error : 1;
Reserved : 14;
}
}
import "path/to/generated/proto"
func main() {
// Create message
req := &proto.ModbusRequest{
TransactionID: 0x1234,
Length: 6,
UnitID: 1,
FunctionCode: 3,
Data: proto.ModbusRequestData{
StartAddress: 0,
Quantity: 10,
},
}
// Encode
data, err := req.Encode()
if err != nil {
// Handle error
}
// Decode
decoded := &proto.ModbusRequest{}
if err := decoded.Decode(data); err != nil {
// Handle error
}
// Use bitfield helpers
flags := &proto.ModbusFlags{}
flags.SetFlagsResponse(true)
if flags.GetFlagsError() {
// Handle error
}
}
The generated code includes test helpers. You can verify protocol handling with:
go test ./generated/go/proto -v
- Endianness handling (both global and per-field)
- Type-safe serialization/deserialization
- Bitfield manipulation without manual bitwise operations
- Automatic boundary checks
- Support for variable-length fields
- Consistency between Go and C implementations
- No floating-point types (extend the DSL if needed)
- Arithmetic expressions have left-to-right precedence only
- No conditional fields (all fields are always present)
- Maximum bitfield container size is 64 bits
To add new features:
- Extend the AST definitions in
internal/pdl/ast.go
- Update the parser in
internal/pdl/parser.go
- Add code generation logic in
internal/gen/golang.go
and/orinternal/gen/cgen.go
- Add tests for new functionality
MIT License