Skip to content

make packed struct always use a single backing integer, inferring it if not explicitly provided #10113

Closed
@ikskuh

Description

@ikskuh

So, everyone knows the problems we have with packed structs. Some are implementation problems, but we also have a huge problem with not having a proper specification on how they should work at all, except for "zero padding".

So i want to propose a definition for packed structs that is easy to understand and implement.

Let's consider this code example:

const T = packed struct {
    a: u4,
    b: u4,
};

var bits = [_]u8 { 0x01 };
var t = @bitCast(T, bits);

std.debug.print("{}\n", .{ t });

What do you expect it to print?
Is it T{ .a = 1, .b = 0 }? Then you have assumed a little-endian platform, as on big endian, it will print T{ .a = 0, .b = 1 }.
Check it out on Compiler Explorer.

Personally, i find this confusing, thus i propose:

A packed struct will have a similar semantic as a unsigned integer with the same amount of bits. Each field is considered an unsigned integer of the same amount of bits.

To explain the idea, we have this struct:

const T1 = packed struct { // u8
  a: u4, // occupies bits 0..3
  b: u4, // occupies bits 4..7
};

const T2 = packed struct { // u32
  a: u4, // occupies bits 0..3 in our u32
  b: u3, // occupies bits 4..6 in our u32
  c: u25, // occupies bits 7..31 in our u32
};

So the packed struct implements exactly what we would do in a language without packed structs:

var value: T2 = …;
var a = @truncate( u4, 0x0000000F & (value >> 0));
var b = @truncate( u3, 0x00000007 & (value >> 4));
var c = @truncate(u25, 0x01FFFFFF & (value >> 7));

This means that we have a predictable model for backed structs and in case of T2, we can reason about it:

const integer: u32 = 0x165652B6;
const value = @bitCast(T2, integer);
std.debug.assert(value.a == 6);
std.debug.assert(value.b == 3);
std.debug.assert(value.c == 0x2CACA5);

This will be true for both little and big endian hardware, so it will do what we expect, even if the bytes on disk have a different order.
If a known byte order is required, we can use std.mem.writeIntLittle, @byteSwap and others.

In addition, we can declare a struct as packed struct(u32), which will enforce the struct size to 32 bit, and we will get a compiler error if it isn't the case.

This will also guarantee that this struct is handled as a u32 and can be used as such, similar to enums. This also guarantees that loads and stores will not be sliced into more than one access if possible and allows atomic load/store for such packed structs. (See #5049).

What needs to be clarified:

  • How to handle floats (float endianess is a thing)
  • How to handle pointers in a packed struct
  • How to handle pointers to struct fields

Related issues:

Regards

  • xq

Metadata

Metadata

Assignees

No one assigned

    Labels

    acceptedThis proposal is planned.proposalThis issue suggests modifications. If it also has the "accepted" label then it is planned.

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions