Skip to content

hootrhino/pdlc

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 

Repository files navigation

pdlc – Protocol Description Language Compiler

A tool for describing binary protocols with a simple DSL (PDL) and generating type-safe serialization/deserialization code for Go and C.

Overview

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() and Decode() 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.

DSL Syntax Reference

Basic Structure

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
}

Data Types

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

Field Definitions

A basic field definition follows this syntax:

FieldName : Type [@Offset] [endian=be|le] [array=Expr] [len=Expr] [const=Value];

Attributes:

  • @Offset: Explicit byte offset (e.g., @4 forces the field to start at byte 4)
  • endian=be|le: Override global endianness for this field
  • array=Expr: Define as array with length from expression (e.g., array=count)
  • len=Expr: For bytes type, specify length (e.g., len=payloadSize)
  • const=Value: Mark as constant value (e.g., const=0xAA)

Examples:

// 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;

Bitfield Containers

Bitfields allow packing multiple values into a single integer:

FieldName : bitsN {
    SubField1 : Width;
    SubField2 : Width;
    // Additional subfields
};
  • bitsN must be one of bits8, 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)

Example:

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

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)

Examples:

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

Nested Structs

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;
    }
}

Usage

Installation

go install github.com/hootrhino/pdlc/cmd/pdlc@latest

Generating Code

pdlc --in protocol.pdl --lang go,c --out ./generated

Options:

  • --in: Input PDL file path (required)
  • --lang: Comma-separated list of target languages (go, c)
  • --out: Output directory (default: current directory)

Output

Go

Generates ./out/go/proto/<name>_gen.go containing:

  • Struct definitions matching the PDL
  • Encode() ([]byte, error) method for serialization
  • Decode([]byte) error method for deserialization
  • Bitfield helper methods (Get<Field><Subfield>() and Set<Field><Subfield>())

C

Generates ./out/c/<name>.h containing:

  • Packed struct definitions
  • Bitfield access macros (GET_<STRUCT>_<FIELD>_<SUBFIELD>() and SET_*)
  • Endianness conversion functions where needed

Example

PDL Input (modbus.pdl)

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;
    }
}

Generated Go Usage

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
    }
}

Testing

The generated code includes test helpers. You can verify protocol handling with:

go test ./generated/go/proto -v

Features

  • 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

Limitations

  • 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

Extending the DSL

To add new features:

  1. Extend the AST definitions in internal/pdl/ast.go
  2. Update the parser in internal/pdl/parser.go
  3. Add code generation logic in internal/gen/golang.go and/or internal/gen/cgen.go
  4. Add tests for new functionality

License

MIT License

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages