Less verbose way to always serialize packed structs as little endian

I’m trying to use a binary protocol in zig. I have defined the structure of the binary protocol largely as packed structs. Is there a less verbose way to make sure I always send the data as little endian?

test "send command" {
    const Command = packed struct(u16) {
        flag: bool,
        reserved: u7 = 0,
        num: u8,
    };

    const my_command = Command{
        .flag = false,
        .num = 7,
    };
    var command_bytes: [2]u8 = undefined;

    var fbs = std.io.fixedBufferStream(command_bytes);
    fbs.writer().writeInt(
        u16,
        @bitCast(my_command),
        std.builtin.Endian.little,
    );

    send_command(&command_bytes);
}

Currently my send_command is like socket send, it accepts a slice of bytes to send.

Some difficulties:

  1. The packed structs will always be a multiple of 8 bits long, but I cannot use std.mem.asBytes since that will give me padding bytes.
  2. I need to send many different types of packed structs easily.
  3. I need to always serialize as little endian, regardless of host endianness.
  4. I don’t want endian handling littered all over my codebase.

I came up with this helper function:

const native_endian = @import("builtin").target.cpu.arch.endian();

fn packed_struct_to_bytes_little(comptime T: type, packed_struct: T) [@divExact(@bitSizeOf(T), 8)]u8 {
    comptime std.debug.assert(@typeInfo(T).Struct.layout == .@"packed"); // must be a packed struct
    var bytes: [@divExact(@bitSizeOf(T), 8)]u8 = undefined;

    if (native_endian == .little) {
        bytes = @bitCast(packed_struct);
    } else {
        std.mem.writePackedInt(
            @typeInfo(T).Struct.backing_integer.?,
            &bytes,
            0,
            @bitCast(packed_struct),
            .little,
        );
    }
    return bytes;
}

Not entirely sure if I’m doing this correctly or if there is an easier way.

But if you’re writing to a buffer using writeInt, you’re going to write the padding as well.

I think what you want is this:

pub fn send(cmd: anytype) void{
  const Cmd = @TypeOf(cmd);
  const Backing = @typeInfo(Cmd).Struct.backing_integer.?;
  const realBits = @typeInfo(Backing).Int.bits;
  const realBytes = @divExact(realBits, 8);

  const native_endian = @import("builtin").target.cpu.arch.endian();
  switch(native_endian){
    .little => { 
      const bytes = std.mem.asBytes(&cmd);
      sendBytes(bytes[0..realBytes]);
    },
    .big => {
      const temp = @byteSwap(cmd);
      const bytes = std.mem.asBytes(&temp);
      const padding = @sizeOf(Cmd) - realBytes;
      sendBytes(bytes[padding..]);
    },
  }
}
2 Likes

I am using writePackedInt which shouldn’t write padding, I think.

Also, @byteswap only accepts integers or integer vector types.

Right. You can bitcast it to the backing integer.