Write bits at bit position in buffer

Is there a standard lib function that does the majority of this or do I need to handwrite this whole thing?

/// Write bits of length at bit position in the buffer.
/// Write value as little-endian.
/// Bit position is lowest to highest memory address, least to most significant within a byte.
///
/// writeBitsAtPos(out, bool, 0, N) does nothing
/// writeBitsAtPos(out, bool, 1, N) writes 1 bit at position N
/// writeBitsAtPos(out, bool, 2, N) writes 2 bits, bool at position N, 0 at position N + 1
/// writeBitsAtPos(out, void, 3, N) writes 3 zero bits at position N
/// writeBitsAtPos(out, u8, 5, N) writes the 5 least significant bits of u8 at position N
pub fn writeBitsAtPos(out_buffer: []u8, value: anytype, bitlength: u16, bit_pos: u16) void {
    // TODO: implement
}

You’re looking for std.mem.writePackedInt

// writes 3 zero bits at position N
std.mem.writePackedInt(u3, out, N, 0, .little);

The example usage seems to be not very useful, since you can do the same thing by simply update the specified field in the packed struct.

I was a bit confused when reading the example source code.

1 Like

Yeah, the example is not good; using it to write to a field of a packed struct doesn’t make much sense. Here’s a better use case (a tightly packed array):

// pretend `codepoints` is a slice of u21

const packed_bytes_len = try std.math.divCeil(usize, @bitSizeOf(u21) * codepoints.len, 8);
const packed_bytes = try allocator.alloc(u8, packed_bytes_len);
defer allocator.free(packed_bytes);

for (codepoints, 0..) |codepoint, i| {
    std.mem.writePackedInt(
        u21,
        packed_bytes,
        i * @bitSizeOf(u21),
        codepoint,
        .little,
    );
}

// Now packed_bytes is a tighly packed array of
// `u21` with no padding bits between the elements.
// We can use `readPackedInt` to retrieve elements from it.

(adapted from this real example [in the real code it’s even more useful since it writes the elements out-of-order], a few more examples here since PackedIntArray was removed in favor of the std.mem.{read,write}PackedInt functions)

1 Like

Thanks, it is an interesting example.

But since the array is packed, is the generated code the same as with a normal array?

Not at all, packing ints like that is a tradeoff between compactness and speed. I have a comment explaining why I’m packing that particular data if you’re interested.

@squeek502 Thanks again.

I searched an example about unaligned access to arrays in the Documentation - The Zig Programming Language section, but I was unable to find it after a quick search.

I think an example is important because a curious reader may be confused that access to an unaligned array will not cause a crash (on some cpu).

This (AFAIK) works only because the compiler add extra and expensive code to avoid a crash. Not sure if this was done from early compilers days or recently.

I am not sure what you are talking about, the array is packed through math and bitshifting by std.mem.writePackedInt, so the programmer that uses that function adds extra more expensive code, the compiler has nothing to do with it, it also doesn’t do any unaligned access it just maps the packed bit position to a dense array of bytes instead of one that includes padding.

1 Like

What @Sze said.

Here’s the crux of writePackedIntLittle:

and readPackedIntLittle:

I actually needed std.mem.writeVarPackedInt

/// Stores an integer to packed memory with provided bit_offset, bit_count, and signedness.
/// If negative, the written value is sign-extended.
pub fn writeVarPackedInt(bytes: []u8, bit_offset: usize, bit_count: usize, value: anytype, endian: std.builtin.Endian)

which I customized to my use case:

pub fn writeBitsAtPos(bytes: []u8, bit_offset: u16, bit_count: usize, value: anytype) void {
    return switch (@TypeOf(value)) {
        void => {}, // do nothing
        bool => std.mem.writeVarPackedInt(bytes, bit_offset, bit_count, @as(u1, @intFromBool(value)), .little),
        f32 => std.mem.writeVarPackedInt(bytes, bit_offset, bit_count, @as(u32, @bitCast(value)), .little),
        f64 => std.mem.writeVarPackedInt(bytes, bit_offset, bit_count, @as(u64, @bitCast(value)), .little),
        else => std.mem.writeVarPackedInt(bytes, bit_offset, bit_count, value, .little),
    };
}