Reinterpreting unaligned memory

I’m trying to reinterpret unaligned memory to avoid allocation:

const std = @import("std");
const builtin = @import("builtin");

fn deserializeInPlace(buf: []u8) ![]align(1) const u16 {
    if (buf.len % 2 != 0) return error.WrongLen;
    const slice: []align(1) u16 = std.mem.bytesAsSlice(u16, buf);
    switch (builtin.cpu.arch.endian()) {
        .little => {},
        .big => {
            // NOTE: this doesnt work due to alignment
            // std.mem.byteSwapAllElements(u16, slice)
            // test5.zig:9:50: note: pointer alignment '1' cannot cast into pointer alignment '2'
            // note: parameter type declared here
            // pub fn byteSwapAllElements(comptime Elem: type, slice: []Elem) void {
            //                                                        ^~~~~~
            for (slice) |*elem| {
                elem.* = @byteSwap(elem.*);
            }
        },
    }
    return slice;
}

test {
    var bytes: [4]u8 = .{ 0x01, 0x02, 0x03, 0x04 };
    const nums = try deserializeInPlace(&bytes);
    const expected: []const u16 align(1) = &.{ 0x0201, 0x0403 };
    try std.testing.expectEqualSlices(u16, expected, nums);
}

$ zig test test5.zig 
anyzig: zig version '0.15.1' pulled from '/home/jeff/repos/untracked/build.zig.zon'
anyzig: appdata '/home/jeff/.local/share/anyzig'
anyzig: zig '0.15.1' already exists at '/home/jeff/.cache/zig/p/N-V-__8AAN5NhBR0oTsvnwjPdeNiiDLtEsfXRHd1fv-R3TOv'
test5.zig:28:54: error: expected type '[]const u16', found '[]align(1) const u16'
    try std.testing.expectEqualSlices(u16, expected, nums);
                                                     ^~~~
test5.zig:28:54: note: pointer alignment '1' cannot cast into pointer alignment '2'
/home/jeff/.cache/zig/p/N-V-__8AAN5NhBR0oTsvnwjPdeNiiDLtEsfXRHd1fv-R3TOv/lib/std/testing.zig:363:73: note: parameter type declared here
pub fn expectEqualSlices(comptime T: type, expected: []const T, actual: []const T) !void {

But I am having trouble even testing it, it doesn’t seem like unaligned slices are well supported by the standard library. Am I doing something wrong, is there a better way to do this that I am not seeing?

You may be interested in std.mem.readInt which can be used to read unaligned integers from a byte array, with a specified endian-ness.

const std = @import("std");
const builtin = @import("builtin");
const testing = @import("testing");

pub fn main() !void {
    var bytes: [4]u8 = .{ 0x01, 0x02, 0x03, 0x04 };
    const nums = try deserializeInPlace(&bytes);
    std.debug.print("{any}", .{nums});
}

fn deserializeInPlace(buf: []u8) ![]const u16 {
    if (buf.len % 2 != 0) return error.WrongLen;
    const slice: []u16 = std.mem.bytesAsSlice(u16, @as([]u16, @ptrCast(@alignCast(buf))));
    switch (builtin.cpu.arch.endian()) {
        .little => {},
        .big => {
            // NOTE: this doesnt work due to alignment
            // std.mem.byteSwapAllElements(u16, slice)
            // test5.zig:9:50: note: pointer alignment '1' cannot cast into pointer alignment '2'
            // note: parameter type declared here
            // pub fn byteSwapAllElements(comptime Elem: type, slice: []Elem) void {
            //                                                        ^~~~~~
            for (slice) |*elem| {
                elem.* = @byteSwap(elem.*);
            }
        },
    }
    return slice;
}

test {
    const expected: []const u16 = &.{ 0x0201, 0x0403 };
    var bytes: [4]u8 = .{ 0x01, 0x02, 0x03, 0x04 };
    const nums = try deserializeInPlace(&bytes);
    try std.testing.expectEqualSlices(u16, expected, nums);
}
󰣇 Documents/test/testing ❯ zig run ./src/main.zig                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         zig    00:07 
{ 513 }%        
󰣇 Documents/test/testing ❯ zig test ./src/main.zig                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        zig    00:07 
slices differ. first difference occurs at index 1 (0x1)

============ expected this output: =============  len: 2 (0x2)

[0]: 513
[1]: 1027

============= instead found this: ==============  len: 1 (0x1)

[0]: 513

================================================

1/1 main.test_0...FAIL (TestExpectedEqual)
/home/james/zig/0.15.1/files/lib/std/testing.zig:376:5: 0x102ef37 in expectEqualSlices__anon_1576 (std.zig)
    return error.TestExpectedEqual;
    ^
/home/james/Documents/test/testing/src/main.zig:35:5: 0x102f131 in test_0 (main.zig)
    try std.testing.expectEqualSlices(u16, expected, nums);
    ^
0 passed; 0 skipped; 1 failed.
error: the following test command failed with exit code 1:
.zig-cache/o/2d038284fd934167d906265621c1d8b8/test --seed=0xf19e1a55

Here is the answer to pass the test case:

const std = @import("std");
const builtin = @import("builtin");
const testing = @import("testing");

pub fn main() !void {
    var bytes: [4]u8 = .{ 0x01, 0x02, 0x03, 0x04 };
    const nums = try deserializeInPlace(&bytes);
    std.debug.print("{any}", .{nums});
}

fn deserializeInPlace(buf: []u8) ![]u16 {
    if (buf.len % 2 != 0) return error.WrongLen;
    std.debug.print("{any}", .{buf});
    const slice = std.mem.bytesAsSlice(u16, buf);
    switch (builtin.cpu.arch.endian()) {
        .little => {},
        .big => {
            // NOTE: this doesnt work due to alignment
            // std.mem.byteSwapAllElements(u16, slice)
            // test5.zig:9:50: note: pointer alignment '1' cannot cast into pointer alignment '2'
            // note: parameter type declared here
            // pub fn byteSwapAllElements(comptime Elem: type, slice: []Elem) void {
            //                                                        ^~~~~~
            for (slice) |*elem| {
                elem.* = @byteSwap(elem.*);
            }
        },
    }
    return @alignCast(slice);
}

test {
    const expected: []const u16 = &.{ 0x0201, 0x0403 };
    var bytes: [4]u8 = .{ 0x01, 0x02, 0x03, 0x04 };
    const nums = try deserializeInPlace(&bytes);
    try std.testing.expectEqualSlices(u16, expected, nums);
}

Welcome to ziggit!

The implementation needs to read from align(1) memory. Your implementation assumes that the input buffer is align(2), you can illustrate the failure with this:


test {
    var bytes: [5]u8 = .{ 0x00, 0x01, 0x02, 0x03, 0x04 };
    const nums = try deserializeInPlace(bytes[1..]);
    const expected: []const u16 align(1) = &.{ 0x0201, 0x0403 };
    try std.testing.expectEqual(nums[0], expected[0]);
    try std.testing.expectEqual(nums[1], expected[1]);
}
{ 1, 2, 3, 4 }thread 1439595 panic: incorrect alignment
/home/jeff/repos/untracked/test5.zig:22:12: 0x102c1a6 in deserializeInPlace (test5.zig)
    return @alignCast(slice);
           ^
/home/jeff/repos/untracked/test5.zig:27:40: 0x102c3e6 in test_0 (test5.zig)
    const nums = try deserializeInPlace(bytes[1..]);

You have two options, either require the input data to be overaligned or have the result be underaligned. Having both have their natural alignments will require copying to a new buffer. Both have their pros and cons so whichever is better is up to you.

try std.testing.expectEqualSlices(u16, expected, nums);

can be rewritten as

for (expected, nums) |e, n| {
    try std.testing.expectEqual(e, n);
}
2 Likes