842 lines
35 KiB
Zig
842 lines
35 KiB
Zig
//! Struct layout: where each member of a C-compatible struct lands in memory under a
|
|
//! given ABI, and how much of the struct is padding (FR-3).
|
|
//!
|
|
//! Nothing here knows any language's syntax. A front end (today only
|
|
//! `CStructParser.zig`) turns text into a `Definition`, and `compute` lays that out
|
|
//! under an `Abi`. A Rust `#[repr(C)]` or a Zig `extern struct` front end would
|
|
//! produce the same `Definition` and need nothing below to change.
|
|
//!
|
|
//! An ABI is data, not code: a table giving each scalar kind its size and alignment,
|
|
//! or none where the type does not exist. Adding a profile is adding a table entry
|
|
//! (NFR-4), and every profile names the C compiler targets it claims to reproduce,
|
|
//! which is what the clang check in `engine/test/struct_oracle.zig` holds it to.
|
|
//!
|
|
//! No I/O. `compute` allocates the per-member results and nothing else.
|
|
|
|
const std = @import("std");
|
|
const Allocator = std.mem.Allocator;
|
|
|
|
/// What laying out a definition can fail with.
|
|
pub const Error = error{
|
|
/// The definition has no members. C forbids an empty struct, and GNU C's
|
|
/// zero-size answer is a different language's.
|
|
EmptyStruct,
|
|
/// More members than `max_members`.
|
|
TooManyMembers,
|
|
/// Two members share a name.
|
|
DuplicateMember,
|
|
/// A scalar the ABI does not have: `__int128` on a 32-bit target.
|
|
TypeUnavailable,
|
|
/// An array dimension of zero. A flexible array member is spelled `[]`.
|
|
InvalidArrayLength,
|
|
/// A flexible array member anywhere but last.
|
|
MisplacedFlexibleArray,
|
|
/// An alignment or pack value that is not a power of two.
|
|
InvalidAlignment,
|
|
/// Larger than the ABI can address, or than the arithmetic can hold.
|
|
StructTooLarge,
|
|
OutOfMemory,
|
|
};
|
|
|
|
/// Ceiling on members, so untrusted input cannot ask for unbounded work (NFR-8).
|
|
/// NFR-1 asks that 256 be handled without perceptible delay; this leaves room above
|
|
/// that for generated code without making the duplicate check expensive.
|
|
pub const max_members: usize = 1024;
|
|
|
|
/// The scalar kinds whose size or alignment a profile has to state.
|
|
///
|
|
/// These are C's kinds rather than shapes: `bool` and `char` are both one byte on
|
|
/// every profile here, but they are different questions to ask of an ABI, and a
|
|
/// profile where they differ would need no new member. Spellings are the front end's
|
|
/// business; `uint32_t`, `u32` and `unsigned int` all arrive as `.int`.
|
|
pub const Primitive = enum {
|
|
/// One byte: `char`, `int8_t`, `u8`.
|
|
char,
|
|
/// `_Bool`.
|
|
bool,
|
|
/// Two bytes: `short`, `int16_t`, `char16_t`.
|
|
short,
|
|
/// Four bytes: `int`, `int32_t`, `char32_t`.
|
|
int,
|
|
/// `long`: eight bytes on LP64, four on LLP64 and ILP32. The reason a 64-bit
|
|
/// Windows profile exists at all.
|
|
long,
|
|
/// Eight-byte integers: `long long`, `int64_t`. Aligned to four on i386.
|
|
long_long,
|
|
/// `__int128`. Absent on the 32-bit profiles.
|
|
int128,
|
|
float,
|
|
/// Aligned to four on i386.
|
|
double,
|
|
/// Any data or function pointer, and the types defined to be pointer-sized here
|
|
/// (`size_t`, `ptrdiff_t`, `intptr_t`). Every profile in the table has
|
|
/// `size_t` the width of a pointer, so they share a row.
|
|
pointer,
|
|
/// `wchar_t`: two bytes on Windows, four elsewhere.
|
|
wchar,
|
|
/// An `enum`. Int-sized on every profile here; a bare-metal ARM profile with
|
|
/// short enums is the case that would make this row differ.
|
|
enumeration,
|
|
};
|
|
|
|
/// Size and alignment of one scalar, in bytes.
|
|
pub const Shape = struct {
|
|
size: u8,
|
|
alignment: u8,
|
|
};
|
|
|
|
/// A target's layout rules.
|
|
pub const Abi = struct {
|
|
/// Stable and short: what `--abi` takes and what the clang check names.
|
|
id: []const u8,
|
|
/// What a person reads.
|
|
label: []const u8,
|
|
/// Every scalar kind, with null where the type does not exist. `EnumArray.init`
|
|
/// takes a struct with a field per member, so a profile cannot forget one.
|
|
shapes: std.EnumArray(Primitive, ?Shape),
|
|
/// Which compiler family's rules the attributes follow. No default: a profile
|
|
/// has to say.
|
|
rules: Rules,
|
|
|
|
/// How explicit alignment and packing combine, which is where compilers differ.
|
|
/// Clang reproduces both, choosing by target, and the clang check is what
|
|
/// established the one difference that matters so far.
|
|
pub const Rules = enum {
|
|
/// GCC's, and clang's everywhere but Windows: `#pragma pack` caps every
|
|
/// member's alignment, including one raised by `aligned` or `alignas`.
|
|
gnu,
|
|
/// MSVC's, and clang's on `*-windows-msvc`: `#pragma pack` caps a member's
|
|
/// natural alignment, and an explicit `aligned` or `alignas` is applied after
|
|
/// it and wins. Under `pack(2)`, `char c; alignas(8) int i;` puts `i` at 8,
|
|
/// where the GNU rules put it at 2.
|
|
microsoft,
|
|
};
|
|
|
|
/// The largest object the ABI can describe: `PTRDIFF_MAX`, half the address
|
|
/// space. A C compiler refuses anything bigger, so a layout claiming otherwise
|
|
/// would be one no program could have.
|
|
fn maxObjectSize(self: *const Abi) u64 {
|
|
const pointer_bits: u6 = @intCast(@as(u16, self.shapes.get(.pointer).?.size) * 8 - 1);
|
|
return (@as(u64, 1) << pointer_bits) - 1;
|
|
}
|
|
};
|
|
|
|
fn s(size: u8, alignment: u8) ?Shape {
|
|
return .{ .size = size, .alignment = alignment };
|
|
}
|
|
|
|
/// The profiles, default first. Each is checked against every clang target listed
|
|
/// for it in `build.zig`.
|
|
pub const abis = [_]Abi{
|
|
.{
|
|
.id = "sysv64",
|
|
.label = "64-bit Linux and macOS (x86-64 SysV, AArch64)",
|
|
.rules = .gnu,
|
|
.shapes = .init(.{
|
|
.char = s(1, 1),
|
|
.bool = s(1, 1),
|
|
.short = s(2, 2),
|
|
.int = s(4, 4),
|
|
.long = s(8, 8),
|
|
.long_long = s(8, 8),
|
|
.int128 = s(16, 16),
|
|
.float = s(4, 4),
|
|
.double = s(8, 8),
|
|
.pointer = s(8, 8),
|
|
.wchar = s(4, 4),
|
|
.enumeration = s(4, 4),
|
|
}),
|
|
},
|
|
.{
|
|
.id = "win64",
|
|
.label = "64-bit Windows (x64, ARM64)",
|
|
.rules = .microsoft,
|
|
.shapes = .init(.{
|
|
.char = s(1, 1),
|
|
.bool = s(1, 1),
|
|
.short = s(2, 2),
|
|
.int = s(4, 4),
|
|
.long = s(4, 4),
|
|
.long_long = s(8, 8),
|
|
.int128 = s(16, 16),
|
|
.float = s(4, 4),
|
|
.double = s(8, 8),
|
|
.pointer = s(8, 8),
|
|
.wchar = s(2, 2),
|
|
.enumeration = s(4, 4),
|
|
}),
|
|
},
|
|
.{
|
|
.id = "i386",
|
|
.label = "32-bit x86 Linux (i386 SysV)",
|
|
.rules = .gnu,
|
|
.shapes = .init(.{
|
|
.char = s(1, 1),
|
|
.bool = s(1, 1),
|
|
.short = s(2, 2),
|
|
.int = s(4, 4),
|
|
.long = s(4, 4),
|
|
.long_long = s(8, 4),
|
|
.int128 = null,
|
|
.float = s(4, 4),
|
|
.double = s(8, 4),
|
|
.pointer = s(4, 4),
|
|
.wchar = s(4, 4),
|
|
.enumeration = s(4, 4),
|
|
}),
|
|
},
|
|
.{
|
|
.id = "arm32",
|
|
.label = "32-bit ARM Linux (AAPCS)",
|
|
.rules = .gnu,
|
|
.shapes = .init(.{
|
|
.char = s(1, 1),
|
|
.bool = s(1, 1),
|
|
.short = s(2, 2),
|
|
.int = s(4, 4),
|
|
.long = s(4, 4),
|
|
.long_long = s(8, 8),
|
|
.int128 = null,
|
|
.float = s(4, 4),
|
|
.double = s(8, 8),
|
|
.pointer = s(4, 4),
|
|
.wchar = s(4, 4),
|
|
.enumeration = s(4, 4),
|
|
}),
|
|
},
|
|
};
|
|
|
|
/// The profile used when none is named (FR-3.6).
|
|
pub const default_abi: *const Abi = &abis[0];
|
|
|
|
/// The profile with this id, or null.
|
|
pub fn findAbi(id: []const u8) ?*const Abi {
|
|
for (&abis) |*abi| {
|
|
if (std.mem.eql(u8, abi.id, id)) return abi;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// Where a member was written, as a byte range in the front end's source. Lets a
|
|
/// layout error point at the member it is about.
|
|
pub const Span = struct {
|
|
start: usize,
|
|
len: usize,
|
|
};
|
|
|
|
/// One member, as the front end understood it.
|
|
pub const Member = struct {
|
|
name: []const u8,
|
|
/// The type as the user wrote it, for display only: `uint32_t`, `struct node *`.
|
|
/// Array dimensions are not part of it; they are `dims`.
|
|
type_name: []const u8,
|
|
primitive: Primitive,
|
|
/// Array dimensions, outermost first. Empty for a scalar.
|
|
dims: []const u64 = &.{},
|
|
/// `T name[]` as the last member: no size of its own, but it still aligns.
|
|
flexible: bool = false,
|
|
/// `alignas(N)` or `aligned(N)`: raises the member's alignment, never lowers it.
|
|
aligned: ?u32 = null,
|
|
/// `__attribute__((packed))` on the member: alignment 1.
|
|
@"packed": bool = false,
|
|
span: ?Span = null,
|
|
};
|
|
|
|
/// A struct to lay out. Borrows everything: a front end that allocates owns the
|
|
/// memory and says how it is released.
|
|
pub const Definition = struct {
|
|
/// The struct's tag or typedef name, if it had one.
|
|
name: ?[]const u8 = null,
|
|
/// Whether `name` is a tag (`struct hdr`) or a typedef (`hdr`), which decides how
|
|
/// the struct is referred to.
|
|
name_kind: NameKind = .tag,
|
|
members: []const Member,
|
|
/// `__attribute__((packed))` on the struct: every member aligned to 1, unless the
|
|
/// member asks for more itself.
|
|
@"packed": bool = false,
|
|
/// `#pragma pack(N)` in effect: no member aligned to more than N.
|
|
pack: ?u32 = null,
|
|
/// `aligned(N)` on the struct: the struct's alignment is at least N.
|
|
aligned: ?u32 = null,
|
|
|
|
pub const NameKind = enum { tag, typedef };
|
|
};
|
|
|
|
/// Where one member landed. Parallel to `Definition.members`.
|
|
pub const Placement = struct {
|
|
offset: u64,
|
|
size: u64,
|
|
/// The alignment the member was placed at, after every attribute and pack.
|
|
alignment: u32,
|
|
/// Bytes inserted between the previous member's end and this one's start.
|
|
padding_before: u64,
|
|
};
|
|
|
|
/// Which member a layout error is about, when it is about one.
|
|
pub const Diagnostic = struct {
|
|
member: ?usize = null,
|
|
};
|
|
|
|
/// A laid-out struct.
|
|
pub const Layout = struct {
|
|
abi: *const Abi,
|
|
/// Borrowed from the definition this came from.
|
|
members: []const Member,
|
|
/// Owned, one per member.
|
|
placements: []Placement,
|
|
size: u64,
|
|
alignment: u32,
|
|
/// Padding after the last member, rounding the size up to the alignment.
|
|
tail_padding: u64,
|
|
/// The sum of the members' own sizes: the struct with no padding at all
|
|
/// (FR-3.10).
|
|
packed_size: u64,
|
|
|
|
pub fn deinit(self: *Layout, allocator: Allocator) void {
|
|
allocator.free(self.placements);
|
|
self.* = undefined;
|
|
}
|
|
|
|
/// Every padding byte, between members and at the tail.
|
|
pub fn padding(self: Layout) u64 {
|
|
return self.size - self.packed_size;
|
|
}
|
|
|
|
/// The struct as consecutive byte ranges, in address order: every member, and
|
|
/// every run of padding between and after them. One list for both a table and a
|
|
/// memory map, so they cannot disagree about what is where. Derived from
|
|
/// `placements` on demand rather than stored as a byte array, because the answer
|
|
/// for a struct holding `char buf[1 << 20]` is three segments, not a million
|
|
/// entries.
|
|
pub fn segments(self: *const Layout) SegmentIterator {
|
|
return .{ .layout = self };
|
|
}
|
|
};
|
|
|
|
/// A run of bytes in a layout.
|
|
pub const Segment = struct {
|
|
offset: u64,
|
|
len: u64,
|
|
kind: Kind,
|
|
|
|
pub const Kind = union(enum) {
|
|
/// Bytes of the member at this index.
|
|
member: usize,
|
|
/// Padding between members, before the member at this index.
|
|
padding: usize,
|
|
/// Padding after the last member.
|
|
tail,
|
|
};
|
|
};
|
|
|
|
/// Yields the segments of a layout. Every member is yielded, a flexible array member
|
|
/// included with a length of zero, because a table lists it; padding is yielded only
|
|
/// where there is some.
|
|
pub const SegmentIterator = struct {
|
|
layout: *const Layout,
|
|
index: usize = 0,
|
|
/// Whether the padding before `placements[index]` has been yielded.
|
|
padding_done: bool = false,
|
|
tail_done: bool = false,
|
|
|
|
pub fn next(self: *SegmentIterator) ?Segment {
|
|
const placements = self.layout.placements;
|
|
while (self.index < placements.len) {
|
|
const placement = placements[self.index];
|
|
if (!self.padding_done) {
|
|
self.padding_done = true;
|
|
if (placement.padding_before > 0) return .{
|
|
.offset = placement.offset - placement.padding_before,
|
|
.len = placement.padding_before,
|
|
.kind = .{ .padding = self.index },
|
|
};
|
|
}
|
|
const index = self.index;
|
|
self.index += 1;
|
|
self.padding_done = false;
|
|
return .{
|
|
.offset = placement.offset,
|
|
.len = placement.size,
|
|
.kind = .{ .member = index },
|
|
};
|
|
}
|
|
if (!self.tail_done) {
|
|
self.tail_done = true;
|
|
const tail = self.layout.tail_padding;
|
|
if (tail > 0) return .{ .offset = self.layout.size - tail, .len = tail, .kind = .tail };
|
|
}
|
|
return null;
|
|
}
|
|
};
|
|
|
|
/// Lay out `definition` under `abi`.
|
|
///
|
|
/// Members are placed in order, each at the next offset that is a multiple of its
|
|
/// alignment, and the struct's size is rounded up to its own alignment, which is the
|
|
/// largest of its members'. A member's alignment starts at its scalar's and is
|
|
/// lowered to 1 by `packed` (on the member or the struct). Then `aligned` raises it
|
|
/// and `#pragma pack` caps it, in the order `abi.rules` says: under the GNU rules
|
|
/// the cap comes last, so it caps an explicit alignment too; under the Microsoft
|
|
/// rules the explicit alignment comes last and wins.
|
|
///
|
|
/// On failure, `diag.member` names the member the error is about, if any.
|
|
pub fn compute(
|
|
allocator: Allocator,
|
|
definition: Definition,
|
|
abi: *const Abi,
|
|
diag: ?*Diagnostic,
|
|
) Error!Layout {
|
|
const members = definition.members;
|
|
if (members.len == 0) return error.EmptyStruct;
|
|
if (members.len > max_members) return error.TooManyMembers;
|
|
|
|
if (definition.pack) |n| try checkAlignment(n);
|
|
if (definition.aligned) |n| try checkAlignment(n);
|
|
|
|
for (members, 0..) |member, i| {
|
|
errdefer if (diag) |d| {
|
|
d.member = i;
|
|
};
|
|
if (member.aligned) |n| try checkAlignment(n);
|
|
for (member.dims) |dim| {
|
|
if (dim == 0) return error.InvalidArrayLength;
|
|
}
|
|
if (member.flexible and i != members.len - 1) return error.MisplacedFlexibleArray;
|
|
for (members[0..i]) |earlier| {
|
|
if (std.mem.eql(u8, earlier.name, member.name)) return error.DuplicateMember;
|
|
}
|
|
}
|
|
|
|
const placements = try allocator.alloc(Placement, members.len);
|
|
errdefer allocator.free(placements);
|
|
|
|
const limit = abi.maxObjectSize();
|
|
var offset: u64 = 0;
|
|
var struct_alignment: u32 = 1;
|
|
var packed_size: u64 = 0;
|
|
for (members, placements, 0..) |member, *placement, i| {
|
|
errdefer if (diag) |d| {
|
|
d.member = i;
|
|
};
|
|
const scalar = abi.shapes.get(member.primitive) orelse return error.TypeUnavailable;
|
|
|
|
var count: u64 = 1;
|
|
for (member.dims) |dim| count = try mul(count, dim);
|
|
const size: u64 = if (member.flexible) 0 else try mul(scalar.size, count);
|
|
|
|
var alignment: u32 = scalar.alignment;
|
|
if (member.@"packed" or definition.@"packed") alignment = 1;
|
|
switch (abi.rules) {
|
|
.gnu => {
|
|
if (member.aligned) |n| alignment = @max(alignment, n);
|
|
if (definition.pack) |n| alignment = @min(alignment, n);
|
|
},
|
|
.microsoft => {
|
|
if (definition.pack) |n| alignment = @min(alignment, n);
|
|
if (member.aligned) |n| alignment = @max(alignment, n);
|
|
},
|
|
}
|
|
|
|
const start = try alignUp(offset, alignment);
|
|
placement.* = .{
|
|
.offset = start,
|
|
.size = size,
|
|
.alignment = alignment,
|
|
.padding_before = start - offset,
|
|
};
|
|
offset = try add(start, size);
|
|
if (offset > limit) return error.StructTooLarge;
|
|
packed_size = try add(packed_size, size);
|
|
struct_alignment = @max(struct_alignment, alignment);
|
|
}
|
|
|
|
if (definition.aligned) |n| struct_alignment = @max(struct_alignment, n);
|
|
const size = try alignUp(offset, struct_alignment);
|
|
if (size > limit) return error.StructTooLarge;
|
|
|
|
return .{
|
|
.abi = abi,
|
|
.members = members,
|
|
.placements = placements,
|
|
.size = size,
|
|
.alignment = struct_alignment,
|
|
.tail_padding = size - offset,
|
|
.packed_size = packed_size,
|
|
};
|
|
}
|
|
|
|
fn checkAlignment(n: u32) Error!void {
|
|
// `isPowerOfTwo` asserts a nonzero argument, and zero is a value a definition
|
|
// can carry.
|
|
if (n == 0 or !std.math.isPowerOfTwo(n)) return error.InvalidAlignment;
|
|
}
|
|
|
|
fn mul(a: u64, b: u64) Error!u64 {
|
|
return std.math.mul(u64, a, b) catch error.StructTooLarge;
|
|
}
|
|
|
|
fn add(a: u64, b: u64) Error!u64 {
|
|
return std.math.add(u64, a, b) catch error.StructTooLarge;
|
|
}
|
|
|
|
/// `offset` rounded up to a multiple of `alignment`, which is a power of two.
|
|
fn alignUp(offset: u64, alignment: u32) Error!u64 {
|
|
const rem = offset % alignment;
|
|
if (rem == 0) return offset;
|
|
return add(offset, alignment - rem);
|
|
}
|
|
|
|
// -- Tests --
|
|
|
|
const testing = std.testing;
|
|
|
|
fn m(name: []const u8, primitive: Primitive) Member {
|
|
return .{ .name = name, .type_name = @tagName(primitive), .primitive = primitive };
|
|
}
|
|
|
|
fn expectPlacement(layout: Layout, index: usize, offset: u64, size: u64, alignment: u32, padding_before: u64) !void {
|
|
const placement = layout.placements[index];
|
|
try testing.expectEqual(offset, placement.offset);
|
|
try testing.expectEqual(size, placement.size);
|
|
try testing.expectEqual(alignment, placement.alignment);
|
|
try testing.expectEqual(padding_before, placement.padding_before);
|
|
}
|
|
|
|
test "the design document's example: one byte of padding in sixteen" {
|
|
// struct { u32 x; u8 flags; u16 id; u64 ts; }
|
|
const members = [_]Member{ m("x", .int), m("flags", .char), m("id", .short), m("ts", .long_long) };
|
|
var layout = try compute(testing.allocator, .{ .members = &members }, default_abi, null);
|
|
defer layout.deinit(testing.allocator);
|
|
|
|
try expectPlacement(layout, 0, 0, 4, 4, 0);
|
|
try expectPlacement(layout, 1, 4, 1, 1, 0);
|
|
try expectPlacement(layout, 2, 6, 2, 2, 1);
|
|
try expectPlacement(layout, 3, 8, 8, 8, 0);
|
|
try testing.expectEqual(@as(u64, 16), layout.size);
|
|
try testing.expectEqual(@as(u32, 8), layout.alignment);
|
|
try testing.expectEqual(@as(u64, 0), layout.tail_padding);
|
|
try testing.expectEqual(@as(u64, 15), layout.packed_size);
|
|
try testing.expectEqual(@as(u64, 1), layout.padding());
|
|
}
|
|
|
|
test "tail padding rounds the size up to the struct's alignment" {
|
|
// struct { u64 a; u8 b; } is 16 bytes, seven of them after b.
|
|
const members = [_]Member{ m("a", .long_long), m("b", .char) };
|
|
var layout = try compute(testing.allocator, .{ .members = &members }, default_abi, null);
|
|
defer layout.deinit(testing.allocator);
|
|
try testing.expectEqual(@as(u64, 16), layout.size);
|
|
try testing.expectEqual(@as(u64, 7), layout.tail_padding);
|
|
try testing.expectEqual(@as(u64, 7), layout.padding());
|
|
}
|
|
|
|
test "what makes each profile its own" {
|
|
const Case = struct { abi: []const u8, size: u64, alignment: u32 };
|
|
// struct { char c; double d; } shows where 8-byte types align.
|
|
const doubles = [_]Member{ m("c", .char), m("d", .double) };
|
|
for ([_]Case{
|
|
.{ .abi = "sysv64", .size = 16, .alignment = 8 },
|
|
.{ .abi = "win64", .size = 16, .alignment = 8 },
|
|
.{ .abi = "i386", .size = 12, .alignment = 4 },
|
|
.{ .abi = "arm32", .size = 16, .alignment = 8 },
|
|
}) |case| {
|
|
var layout = try compute(testing.allocator, .{ .members = &doubles }, findAbi(case.abi).?, null);
|
|
defer layout.deinit(testing.allocator);
|
|
try testing.expectEqual(case.size, layout.size);
|
|
try testing.expectEqual(case.alignment, layout.alignment);
|
|
}
|
|
|
|
// struct { long l; void *p; wchar_t w; } shows the data model.
|
|
const model = [_]Member{ m("l", .long), m("p", .pointer), m("w", .wchar) };
|
|
for ([_]Case{
|
|
.{ .abi = "sysv64", .size = 24, .alignment = 8 },
|
|
.{ .abi = "win64", .size = 24, .alignment = 8 },
|
|
.{ .abi = "i386", .size = 12, .alignment = 4 },
|
|
.{ .abi = "arm32", .size = 12, .alignment = 4 },
|
|
}) |case| {
|
|
var layout = try compute(testing.allocator, .{ .members = &model }, findAbi(case.abi).?, null);
|
|
defer layout.deinit(testing.allocator);
|
|
try testing.expectEqual(case.size, layout.size);
|
|
try testing.expectEqual(case.alignment, layout.alignment);
|
|
}
|
|
|
|
// On Windows `long` is four bytes, so the pointer after it is padded.
|
|
var win = try compute(testing.allocator, .{ .members = &model }, findAbi("win64").?, null);
|
|
defer win.deinit(testing.allocator);
|
|
try expectPlacement(win, 1, 8, 8, 8, 4);
|
|
try expectPlacement(win, 2, 16, 2, 2, 0);
|
|
}
|
|
|
|
test "__int128 is sixteen-aligned on 64-bit and absent on 32-bit" {
|
|
const members = [_]Member{ m("c", .char), m("big", .int128) };
|
|
var layout = try compute(testing.allocator, .{ .members = &members }, default_abi, null);
|
|
defer layout.deinit(testing.allocator);
|
|
try expectPlacement(layout, 1, 16, 16, 16, 15);
|
|
try testing.expectEqual(@as(u64, 32), layout.size);
|
|
|
|
var diag: Diagnostic = .{};
|
|
try testing.expectError(error.TypeUnavailable, compute(testing.allocator, .{ .members = &members }, findAbi("i386").?, &diag));
|
|
try testing.expectEqual(@as(?usize, 1), diag.member);
|
|
}
|
|
|
|
test "arrays multiply the element size and keep the element alignment" {
|
|
// struct { char tag[3]; int grid[2][3]; } : the grid starts at 4 and is 24 bytes.
|
|
const members = [_]Member{
|
|
.{ .name = "tag", .type_name = "char", .primitive = .char, .dims = &.{3} },
|
|
.{ .name = "grid", .type_name = "int", .primitive = .int, .dims = &.{ 2, 3 } },
|
|
};
|
|
var layout = try compute(testing.allocator, .{ .members = &members }, default_abi, null);
|
|
defer layout.deinit(testing.allocator);
|
|
try expectPlacement(layout, 0, 0, 3, 1, 0);
|
|
try expectPlacement(layout, 1, 4, 24, 4, 1);
|
|
try testing.expectEqual(@as(u64, 28), layout.size);
|
|
}
|
|
|
|
test "a flexible array member has no size but still aligns" {
|
|
// struct { short len; double data[]; } : data starts at 8, sizeof is 8.
|
|
const members = [_]Member{
|
|
m("len", .short),
|
|
.{ .name = "data", .type_name = "double", .primitive = .double, .flexible = true },
|
|
};
|
|
var layout = try compute(testing.allocator, .{ .members = &members }, default_abi, null);
|
|
defer layout.deinit(testing.allocator);
|
|
try expectPlacement(layout, 1, 8, 0, 8, 6);
|
|
try testing.expectEqual(@as(u64, 8), layout.size);
|
|
try testing.expectEqual(@as(u64, 2), layout.packed_size);
|
|
}
|
|
|
|
test "packed removes padding, aligned adds it back where asked" {
|
|
const members = [_]Member{ m("c", .char), m("i", .int), m("d", .double) };
|
|
var packed_layout = try compute(testing.allocator, .{ .members = &members, .@"packed" = true }, default_abi, null);
|
|
defer packed_layout.deinit(testing.allocator);
|
|
try testing.expectEqual(@as(u64, 13), packed_layout.size);
|
|
try testing.expectEqual(@as(u32, 1), packed_layout.alignment);
|
|
try testing.expectEqual(@as(u64, 0), packed_layout.padding());
|
|
|
|
// A member's own `aligned` survives the struct being packed.
|
|
const mixed = [_]Member{ m("c", .char), .{ .name = "i", .type_name = "int", .primitive = .int, .aligned = 4 } };
|
|
var aligned_layout = try compute(testing.allocator, .{ .members = &mixed, .@"packed" = true }, default_abi, null);
|
|
defer aligned_layout.deinit(testing.allocator);
|
|
try expectPlacement(aligned_layout, 1, 4, 4, 4, 3);
|
|
try testing.expectEqual(@as(u32, 4), aligned_layout.alignment);
|
|
|
|
// Packing one member leaves the rest natural.
|
|
const one = [_]Member{ m("c", .char), .{ .name = "i", .type_name = "int", .primitive = .int, .@"packed" = true }, m("s", .short) };
|
|
var one_layout = try compute(testing.allocator, .{ .members = &one }, default_abi, null);
|
|
defer one_layout.deinit(testing.allocator);
|
|
try expectPlacement(one_layout, 1, 1, 4, 1, 0);
|
|
try expectPlacement(one_layout, 2, 6, 2, 2, 1);
|
|
try testing.expectEqual(@as(u64, 8), one_layout.size);
|
|
}
|
|
|
|
test "aligned on a member raises it; on the struct it raises the struct" {
|
|
const members = [_]Member{ m("c", .char), .{ .name = "i", .type_name = "int", .primitive = .int, .aligned = 16 } };
|
|
var layout = try compute(testing.allocator, .{ .members = &members }, default_abi, null);
|
|
defer layout.deinit(testing.allocator);
|
|
try expectPlacement(layout, 1, 16, 4, 16, 15);
|
|
try testing.expectEqual(@as(u64, 32), layout.size);
|
|
|
|
// An `aligned` smaller than the natural alignment does nothing.
|
|
const lower = [_]Member{ m("c", .char), .{ .name = "d", .type_name = "double", .primitive = .double, .aligned = 2 } };
|
|
var lower_layout = try compute(testing.allocator, .{ .members = &lower }, default_abi, null);
|
|
defer lower_layout.deinit(testing.allocator);
|
|
try expectPlacement(lower_layout, 1, 8, 8, 8, 7);
|
|
|
|
const plain = [_]Member{ m("a", .int), m("b", .char) };
|
|
var struct_layout = try compute(testing.allocator, .{ .members = &plain, .aligned = 32 }, default_abi, null);
|
|
defer struct_layout.deinit(testing.allocator);
|
|
try testing.expectEqual(@as(u32, 32), struct_layout.alignment);
|
|
try testing.expectEqual(@as(u64, 32), struct_layout.size);
|
|
try testing.expectEqual(@as(u64, 27), struct_layout.tail_padding);
|
|
}
|
|
|
|
test "pragma pack caps every member's alignment" {
|
|
const members = [_]Member{ m("c", .char), m("d", .double), m("s", .short) };
|
|
var layout = try compute(testing.allocator, .{ .members = &members, .pack = 2 }, default_abi, null);
|
|
defer layout.deinit(testing.allocator);
|
|
try expectPlacement(layout, 1, 2, 8, 2, 1);
|
|
try expectPlacement(layout, 2, 10, 2, 2, 0);
|
|
try testing.expectEqual(@as(u64, 12), layout.size);
|
|
try testing.expectEqual(@as(u32, 2), layout.alignment);
|
|
|
|
// A pack larger than anything in the struct changes nothing.
|
|
var wide = try compute(testing.allocator, .{ .members = &members, .pack = 16 }, default_abi, null);
|
|
defer wide.deinit(testing.allocator);
|
|
try testing.expectEqual(@as(u64, 24), wide.size);
|
|
}
|
|
|
|
test "the rules decide whether pragma pack caps an explicit alignment" {
|
|
// struct { char c; alignas(8) int i; } under pack(2). Found by the clang check:
|
|
// the GNU targets put `i` at 2, the windows-msvc ones at 8.
|
|
const members = [_]Member{ m("c", .char), .{ .name = "i", .type_name = "int", .primitive = .int, .aligned = 8 } };
|
|
const def: Definition = .{ .members = &members, .pack = 2 };
|
|
|
|
var gnu = try compute(testing.allocator, def, findAbi("sysv64").?, null);
|
|
defer gnu.deinit(testing.allocator);
|
|
try testing.expectEqual(Abi.Rules.gnu, gnu.abi.rules);
|
|
try expectPlacement(gnu, 1, 2, 4, 2, 1);
|
|
try testing.expectEqual(@as(u64, 6), gnu.size);
|
|
|
|
var microsoft = try compute(testing.allocator, def, findAbi("win64").?, null);
|
|
defer microsoft.deinit(testing.allocator);
|
|
try testing.expectEqual(Abi.Rules.microsoft, microsoft.abi.rules);
|
|
try expectPlacement(microsoft, 1, 8, 4, 8, 7);
|
|
try testing.expectEqual(@as(u64, 16), microsoft.size);
|
|
|
|
// Without an explicit alignment the two agree: pack caps the natural one.
|
|
const plain = [_]Member{ m("c", .char), m("d", .double) };
|
|
for ([_][]const u8{ "sysv64", "win64" }) |id| {
|
|
var layout = try compute(testing.allocator, .{ .members = &plain, .pack = 2 }, findAbi(id).?, null);
|
|
defer layout.deinit(testing.allocator);
|
|
try expectPlacement(layout, 1, 2, 8, 2, 1);
|
|
}
|
|
}
|
|
|
|
test "segments walk the struct in address order, every member included" {
|
|
// struct { char c; int i; short s; char flex[]; } : c, pad, i, s, tail.
|
|
const members = [_]Member{
|
|
m("c", .char),
|
|
m("i", .int),
|
|
m("s", .short),
|
|
.{ .name = "flex", .type_name = "char", .primitive = .char, .flexible = true },
|
|
};
|
|
var layout = try compute(testing.allocator, .{ .members = &members }, default_abi, null);
|
|
defer layout.deinit(testing.allocator);
|
|
try testing.expectEqual(@as(u64, 12), layout.size);
|
|
|
|
const expected = [_]Segment{
|
|
.{ .offset = 0, .len = 1, .kind = .{ .member = 0 } },
|
|
.{ .offset = 1, .len = 3, .kind = .{ .padding = 1 } },
|
|
.{ .offset = 4, .len = 4, .kind = .{ .member = 1 } },
|
|
.{ .offset = 8, .len = 2, .kind = .{ .member = 2 } },
|
|
// The flexible member has no bytes and is still there to be listed.
|
|
.{ .offset = 10, .len = 0, .kind = .{ .member = 3 } },
|
|
.{ .offset = 10, .len = 2, .kind = .tail },
|
|
};
|
|
var it = layout.segments();
|
|
for (expected) |want| {
|
|
const got = it.next() orelse return error.TestUnexpectedResult;
|
|
try testing.expectEqualDeep(want, got);
|
|
}
|
|
try testing.expect(it.next() == null);
|
|
|
|
// The segments tile the struct exactly.
|
|
var covered: u64 = 0;
|
|
var again = layout.segments();
|
|
while (again.next()) |seg| {
|
|
try testing.expectEqual(covered, seg.offset);
|
|
covered += seg.len;
|
|
}
|
|
try testing.expectEqual(layout.size, covered);
|
|
}
|
|
|
|
test "a struct with no padding has one segment per member" {
|
|
const members = [_]Member{ m("a", .int), m("b", .int) };
|
|
var layout = try compute(testing.allocator, .{ .members = &members }, default_abi, null);
|
|
defer layout.deinit(testing.allocator);
|
|
var it = layout.segments();
|
|
var count: usize = 0;
|
|
while (it.next()) |seg| : (count += 1) {
|
|
try testing.expect(seg.kind == .member);
|
|
}
|
|
try testing.expectEqual(@as(usize, 2), count);
|
|
}
|
|
|
|
test "definitions that cannot be laid out say why and where" {
|
|
var diag: Diagnostic = .{};
|
|
|
|
try testing.expectError(error.EmptyStruct, compute(testing.allocator, .{ .members = &.{} }, default_abi, &diag));
|
|
|
|
const dup = [_]Member{ m("a", .int), m("b", .int), m("a", .char) };
|
|
try testing.expectError(error.DuplicateMember, compute(testing.allocator, .{ .members = &dup }, default_abi, &diag));
|
|
try testing.expectEqual(@as(?usize, 2), diag.member);
|
|
|
|
const zero = [_]Member{ m("a", .int), .{ .name = "z", .type_name = "char", .primitive = .char, .dims = &.{ 2, 0 } } };
|
|
diag = .{};
|
|
try testing.expectError(error.InvalidArrayLength, compute(testing.allocator, .{ .members = &zero }, default_abi, &diag));
|
|
try testing.expectEqual(@as(?usize, 1), diag.member);
|
|
|
|
const flex_first = [_]Member{ .{ .name = "f", .type_name = "char", .primitive = .char, .flexible = true }, m("a", .int) };
|
|
diag = .{};
|
|
try testing.expectError(error.MisplacedFlexibleArray, compute(testing.allocator, .{ .members = &flex_first }, default_abi, &diag));
|
|
try testing.expectEqual(@as(?usize, 0), diag.member);
|
|
|
|
const odd = [_]Member{.{ .name = "a", .type_name = "int", .primitive = .int, .aligned = 3 }};
|
|
diag = .{};
|
|
try testing.expectError(error.InvalidAlignment, compute(testing.allocator, .{ .members = &odd }, default_abi, &diag));
|
|
try testing.expectEqual(@as(?usize, 0), diag.member);
|
|
|
|
const plain = [_]Member{m("a", .int)};
|
|
diag = .{};
|
|
try testing.expectError(error.InvalidAlignment, compute(testing.allocator, .{ .members = &plain, .pack = 6 }, default_abi, &diag));
|
|
try testing.expectEqual(@as(?usize, null), diag.member);
|
|
try testing.expectError(error.InvalidAlignment, compute(testing.allocator, .{ .members = &plain, .aligned = 0 }, default_abi, null));
|
|
}
|
|
|
|
test "too many members is refused before any work is done" {
|
|
const many = try testing.allocator.alloc(Member, max_members + 1);
|
|
defer testing.allocator.free(many);
|
|
for (many) |*member| member.* = m("same", .char);
|
|
// Every name is the same, so reaching the duplicate check would be a different
|
|
// error: the count is checked first.
|
|
try testing.expectError(error.TooManyMembers, compute(testing.allocator, .{ .members = many }, default_abi, null));
|
|
|
|
const names = try testing.allocator.alloc([8]u8, max_members);
|
|
defer testing.allocator.free(names);
|
|
for (many[0..max_members], names, 0..) |*member, *name, i| {
|
|
member.* = m(std.fmt.bufPrint(name, "f{d}", .{i}) catch unreachable, .char);
|
|
}
|
|
var layout = try compute(testing.allocator, .{ .members = many[0..max_members] }, default_abi, null);
|
|
defer layout.deinit(testing.allocator);
|
|
try testing.expectEqual(@as(u64, max_members), layout.size);
|
|
}
|
|
|
|
test "a struct larger than the ABI can address is refused" {
|
|
// 2^31 bytes is one past PTRDIFF_MAX on a 32-bit target and fine on a 64-bit one.
|
|
const members = [_]Member{.{ .name = "buf", .type_name = "char", .primitive = .char, .dims = &.{1 << 31} }};
|
|
var diag: Diagnostic = .{};
|
|
try testing.expectError(error.StructTooLarge, compute(testing.allocator, .{ .members = &members }, findAbi("i386").?, &diag));
|
|
try testing.expectEqual(@as(?usize, 0), diag.member);
|
|
var layout = try compute(testing.allocator, .{ .members = &members }, default_abi, null);
|
|
defer layout.deinit(testing.allocator);
|
|
try testing.expectEqual(@as(u64, 1 << 31), layout.size);
|
|
|
|
// Dimensions whose product overflows u64 are the same error, not a wrap.
|
|
const huge = [_]Member{.{ .name = "h", .type_name = "long long", .primitive = .long_long, .dims = &.{ 1 << 40, 1 << 40 } }};
|
|
try testing.expectError(error.StructTooLarge, compute(testing.allocator, .{ .members = &huge }, default_abi, null));
|
|
|
|
// Rounding up to the struct's alignment can push it over too: the members fit
|
|
// in PTRDIFF_MAX, the padded struct does not.
|
|
const edge = [_]Member{
|
|
m("i", .int),
|
|
.{ .name = "buf", .type_name = "char", .primitive = .char, .dims = &.{(1 << 31) - 5} },
|
|
};
|
|
try testing.expectError(error.StructTooLarge, compute(testing.allocator, .{ .members = &edge }, findAbi("i386").?, null));
|
|
}
|
|
|
|
test "every profile has the scalars C guarantees, and pointer-sized limits" {
|
|
for (&abis) |*abi| {
|
|
for ([_]Primitive{ .char, .bool, .short, .int, .long, .long_long, .float, .double, .pointer, .wchar, .enumeration }) |p| {
|
|
const shape = abi.shapes.get(p) orelse return error.TestUnexpectedResult;
|
|
try testing.expect(shape.size > 0);
|
|
try testing.expect(std.math.isPowerOfTwo(shape.alignment));
|
|
try testing.expect(shape.alignment <= shape.size);
|
|
}
|
|
try testing.expectEqual(@as(u8, 1), abi.shapes.get(.char).?.size);
|
|
try testing.expect(abi.shapes.get(.int128) == null or abi.shapes.get(.pointer).?.size == 8);
|
|
try testing.expect(abi.label.len > 0);
|
|
try testing.expect(findAbi(abi.id) == abi);
|
|
}
|
|
try testing.expectEqual(@as(u64, std.math.maxInt(i64)), default_abi.maxObjectSize());
|
|
try testing.expectEqual(@as(u64, std.math.maxInt(i32)), findAbi("arm32").?.maxObjectSize());
|
|
try testing.expect(findAbi("vax") == null);
|
|
}
|
|
|
|
test "compute releases everything on any allocation failure" {
|
|
try testing.checkAllAllocationFailures(testing.allocator, struct {
|
|
fn run(a: Allocator) !void {
|
|
const members = [_]Member{ m("x", .int), m("y", .char) };
|
|
var layout = try compute(a, .{ .members = &members }, default_abi, null);
|
|
layout.deinit(a);
|
|
}
|
|
}.run, .{});
|
|
}
|