I’m attempting to write a zig library / application for a binary protocol (EtherCAT) that is transmitted on standard ethernet hardware.
The protocol makes heavy use of bit fields and is real-time critical (sometimes sending frames at 20 kHz), which makes me think zig may be the most enjoyable language for this project.
Frames are sent into a ring of connected subdevices. Subdevices contain ASIC chips that modify the frames in flight, writing and reading data on the frame and recalculating the checksum on the fly, and then the frame is returned to the main device (running the application) where the results are read.
I’m struggling with designing my API. Because this is a real-time application, heap allocations should generally only occur at startup, and if possible, the majority of the memory should be stack-allocated (to avoid page faults). Most applications of the library will fit within a typical stack (<1 MB) and may not require any heap allocations at all.
The primary challenge I am facing is serialization and de-serialization of the binary protocol. The protocol makes heavy use of bit fields to optimize utilization of the network bandwidth. For example:
/// Datagram Header
///
/// Ref: IEC 61158-4-12:2019 5.4.1.2
pub const DatagramHeader = packed struct {
/// service command, APRD etc.
command: Command,
/// used my maindevice to identify duplicate or lost datagrams
idx: u8,
/// auto-increment, configured station, or logical address
/// when position addressing
address: u32,
/// length of following data, in bytes
length: u11,
/// reserved, 0
reserved: u3 = 0,
/// true when frame has circulated at least once, else false
circulating: bool,
/// multiple datagrams, true when more datagrams follow, else false
next: bool,
/// EtherCAT event request register of all subdevices combined with
/// a logical OR. Two byte bitmask (IEC 61131-3 WORD)
irq: u16,
};
I guess my main questions are:
- How to handle endianness of my hosts? I first reached for packed structs to best represent the binary protocol, but I don’t think they actually provide much use because I will need to do endianness handling on serialization and de-serialization anyway, so they might as well be regular structs.
- Are serialization and de-serialization methods generally struct methods?
- For serialization, I assume I should take a target byte slice as a parameter to serialize into and make heavy use of the bit writer or other writers from the standard library.
- For de-serialization, I assume I will need to take an allocator as a parameter, but the frame structure is rather nested, with slices to structs containing slices to other structs, so it may be difficult for callers to free the returned data. Should I just put a comment that callers should pass an arena allocator that they should free themselves?
- I would like to support multiple frames in flight, which means I will need to send frames and have a place pre-allocated to receive the frame back into. I guess the send frame API will need to accept both the frame struct to send as well as a pointer to where the frame should be deserialized into upon the frame returning, some microseconds later? Ideally multiple threads should be able to send and receive frames, and the access to the shared resource that is the network interface should have mutexes etc inside it to handle this.