packed struct
packed struct
packed struct는 enum과 마찬가지로 정수를 다르게 해석한다는 개념에 기반해요. 모든 packed struct는 backing integer(뒷받침 정수)를 갖는데, 이 값은 필드의 총 비트 수에 따라 암묵적으로 결정되거나 명시적으로 지정돼요. packed struct는 메모리 레이아웃이 잘 정의되어 있다는 특징이 있어요 — 정확히 backing integer와 같은 ABI를 갖죠.
본문
packed struct의 각 필드는 최하위 비트부터 최상위 비트 순으로 배열된 논리적 비트의 연속으로 해석돼요. 허용되는 필드 타입은 다음과 같아요:
- 정수 필드는 그 비트 폭만큼 정확히 비트를 사용해요. 예를 들어
u5는 backing integer의 5비트를 차지하죠. - bool 필드는 정확히 1비트를 사용해요.
- enum 필드는 정수 태그 타입의 비트 폭만큼 정확히 사용해요.
- packed union 필드는 비트 폭이 가장 큰 union 필드의 비트 폭만큼 정확히 사용해요.
packed struct필드는 자기 자신의 backing integer의 비트를 사용해요.
즉, packed struct는 @bitCast나 @ptrCast에 참여해 메모리를 재해석할 수 있어요. 이건 comptime에서도 동작해요:
const std = @import("std");
const expectEqual = std.testing.expectEqual;
const Full = packed struct {
number: u16,
};
const Divided = packed struct {
half1: u8,
quarter3: u4,
quarter4: u4,
};
test "@bitCast between packed structs" {
try doTheTest();
try comptime doTheTest();
}
fn doTheTest() !void {
try expectEqual(2, @sizeOf(Full));
try expectEqual(2, @sizeOf(Divided));
const full = Full{ .number = 0x1234 };
const divided: Divided = @bitCast(full);
try expectEqual(0x34, divided.half1);
try expectEqual(0x2, divided.quarter3);
try expectEqual(0x1, divided.quarter4);
const ordered: [2]u8 = @bitCast(full);
try expectEqual(0x34, ordered[0]);
try expectEqual(0x12, ordered[1]);
}
$ zig test test_packed_structs.zig
1/1 test_packed_structs.test.@bitCast between packed structs...OK
All 1 tests passed.
backing integer는 추론되거나 명시적으로 제공될 수 있어요. 추론되면 부호 없는 정수(ununsigned)가 되고, 명시적으로 제공되면 그 비트 폭이 필드의 총 비트 폭과 정확히 일치하도록 컴파일 타임에 강제돼요:
test "missized packed struct" {
const S = packed struct(u32) { a: u16, b: u8 };
_ = S{ .a = 4, .b = 2 };
}
$ zig test test_missized_packed_struct.zig
/home/ci/work/zig-bootstrap/zig/doc/langref/test_missized_packed_struct.zig:2:22: error: backing integer bit width does not match total bit width of fields
const S = packed struct(u32) { a: u16, b: u8 };
~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/home/ci/work/zig-bootstrap/zig/doc/langref/test_missized_packed_struct.zig:2:29: note: backing integer 'u32' has bit width '32'
const S = packed struct(u32) { a: u16, b: u8 };
^~~
/home/ci/work/zig-bootstrap/zig/doc/langref/test_missized_packed_struct.zig:2:22: note: struct fields have total bit width '24'
referenced by:
test.missized packed struct: /home/ci/work/zig-bootstrap/zig/doc/langref/test_missized_packed_struct.zig:3:17
packed struct는 @backingInt와 @fromBackingInt를 이용해 backing integer로, 또 그 반대로 변환할 수 있어요:
const std = @import("std");
const assert = std.debug.assert;
const expectEqual = std.testing.expectEqual;
const PackedStruct = packed struct(u8) {
lo: u4,
hi: u4,
};
test "convert to and from backing integer" {
const original: PackedStruct = .{ .lo = 0b1100, .hi = 0b0101 };
const backing_int = @backingInt(original);
comptime assert(@TypeOf(backing_int) == u8);
try expectEqual(0b0101_1100, backing_int);
const reconstructed: PackedStruct = @fromBackingInt(backing_int);
try expectEqual(original, reconstructed);
}
$ zig test test_packed_struct_backing_int.zig
1/1 test_packed_struct_backing_int.test.convert to and from backing integer...OK
All 1 tests passed.
Zig는 바이트 정렬(byte-aligned)되지 않은 필드의 주소를 취하는 것을 허용해요:
const std = @import("std");
const expectEqual = std.testing.expectEqual;
const BitField = packed struct {
a: u3,
b: u3,
c: u2,
};
var foo = BitField{
.a = 1,
.b = 2,
.c = 3,
};
test "pointer to non-byte-aligned field" {
const ptr = &foo.b;
try expectEqual(2, ptr.*);
}
$ zig test test_pointer_to_non-byte_aligned_field.zig
1/1 test_pointer_to_non-byte_aligned_field.test.pointer to non-byte-aligned field...OK
All 1 tests passed.
하지만 바이트 정렬되지 않은 필드에 대한 포인터는 특별한 성질을 가져서, 일반 포인터가 기대되는 곳에 전달할 수 없어요:
const std = @import("std");
const expectEqual = std.testing.expectEqual;
const BitField = packed struct {
a: u3,
b: u3,
c: u2,
};
var bit_field = BitField{
.a = 1,
.b = 2,
.c = 3,
};
test "pointer to non-byte-aligned field" {
try expectEqual(2, bar(&bit_field.b));
}
fn bar(x: *const u3) u3 {
return x.*;
}
$ zig test test_misaligned_pointer.zig
/home/ci/work/zig-bootstrap/zig/doc/langref/test_misaligned_pointer.zig:17:28: error: expected type '*const u3', found '*align(1:3:1) u3'
try expectEqual(2, bar(&bit_field.b));
^~~~~~~~~~~~
/home/ci/work/zig-bootstrap/zig/doc/langref/test_misaligned_pointer.zig:17:28: note: pointer host size '1' cannot cast into pointer host size '0'
/home/ci/work/zig-bootstrap/zig/doc/langref/test_misaligned_pointer.zig:17:28: note: pointer bit offset '3' cannot cast into pointer bit offset '0'
/home/ci/work/zig-bootstrap/zig/doc/langref/test_misaligned_pointer.zig:20:11: note: parameter type declared here
fn bar(x: *const u3) u3 {
^~~~~~~~~
이 경우에 bar 함수는 호출할 수 없어요. ABI 정렬되지 않은 필드에 대한 포인터가 비트 오프셋을 언급하는 반면, 함수는 ABI 정렬된 포인터를 기대하기 때문이에요.
ABI 정렬되지 않은 필드에 대한 포인터는 자신의 host 정수 안에 있는 다른 필드들과 같은 주소를 공유해요:
const std = @import("std");
const expectEqual = std.testing.expectEqual;
const BitField = packed struct {
a: u3,
b: u3,
c: u2,
};
var bit_field = BitField{
.a = 1,
.b = 2,
.c = 3,
};
test "pointers of sub-byte-aligned fields share addresses" {
try expectEqual(@intFromPtr(&bit_field.a), @intFromPtr(&bit_field.b));
try expectEqual(@intFromPtr(&bit_field.a), @intFromPtr(&bit_field.c));
}
$ zig test test_packed_struct_field_address.zig
1/1 test_packed_struct_field_address.test.pointers of sub-byte-aligned fields share addresses...OK
All 1 tests passed.
이건 @bitOffsetOf와 offsetOf로 관찰할 수 있어요:
const std = @import("std");
const expectEqual = std.testing.expectEqual;
const BitField = packed struct {
a: u3,
b: u3,
c: u2,
};
test "offsets of non-byte-aligned fields" {
comptime {
try expectEqual(0, @bitOffsetOf(BitField, "a"));
try expectEqual(3, @bitOffsetOf(BitField, "b"));
try expectEqual(6, @bitOffsetOf(BitField, "c"));
try expectEqual(0, @offsetOf(BitField, "a"));
try expectEqual(0, @offsetOf(BitField, "b"));
try expectEqual(0, @offsetOf(BitField, "c"));
}
}
$ zig test test_bitOffsetOf_offsetOf.zig
1/1 test_bitOffsetOf_offsetOf.test.offsets of non-byte-aligned fields...OK
All 1 tests passed.
packed struct는 자기 backing integer와 같은 정렬(alignment)을 갖지만, packed struct에 대한 과도 정렬(overaligned) 포인터가 이를 덮어쓸 수 있어요:
const std = @import("std");
const expectEqual = std.testing.expectEqual;
const S = packed struct {
a: u32,
b: u32,
};
test "overaligned pointer to packed struct" {
var foo: S align(4) = .{ .a = 1, .b = 2 };
const ptr: *align(4) S = &foo;
const ptr_to_b = &ptr.b;
try expectEqual(2, ptr_to_b.*);
}
$ zig test test_overaligned_packed_struct.zig
1/1 test_overaligned_packed_struct.test.overaligned pointer to packed struct...OK
All 1 tests passed.
또한 struct 필드의 정렬을 설정하는 것도 가능해요:
const std = @import("std");
const expectEqual = std.testing.expectEqual;
test "aligned struct fields" {
const S = struct {
a: u32 align(2),
b: u32 align(64),
};
var foo = S{ .a = 1, .b = 2 };
try expectEqual(64, @alignOf(S));
try expectEqual(*align(2) u32, @TypeOf(&foo.a));
try expectEqual(*align(64) u32, @TypeOf(&foo.b));
}
$ zig test test_aligned_struct_fields.zig
1/1 test_aligned_struct_fields.test.aligned struct fields...OK
All 1 tests passed.
packed struct를 서로 비교하면 backing integer끼리 비교가 돼요. 그리고 이 비교는 ==와 != 연산자에서만 동작해요:
const std = @import("std");
const expectEqual = std.testing.expectEqual;
test "packed struct equality" {
const S = packed struct {
a: u4,
b: u4,
};
const x: S = .{ .a = 1, .b = 2 };
const y: S = .{ .b = 2, .a = 1 };
try expectEqual(x, y);
}
$ zig test test_packed_struct_equality.zig
1/1 test_packed_struct_equality.test.packed struct equality...OK
All 1 tests passed.
필드 접근과 할당은 backing integer에 대한 비트 시프트(bitshift)의 축약형으로 이해할 수 있어요. 이 연산들은 원자적(atomic)이지 않으므로, 메모리 매핑 입출력(MMIO)과 함께 쓸 때 필드 접근 문법을 쓰지 않도록 주의해야 해요. volatile 포인터에서 필드 접근을 하는 대신, 먼저 완전히 채워진 새 값을 만들고 그 값을 volatile 포인터에 써요:
pub const GpioRegister = packed struct(u8) {
GPIO0: bool,
GPIO1: bool,
GPIO2: bool,
GPIO3: bool,
reserved: u4 = 0,
};
const gpio: *volatile GpioRegister = @ptrFromInt(0x0123);
pub fn writeToGpio(new_states: GpioRegister) void {
// Example of what not to do:
// BAD! gpio.GPIO0 = true; BAD!
// Instead, do this:
gpio.* = new_states;
}