Skip to content

do not allow enum/packed struct/packed union in extern positions if backing type is implicit #24714

@mlugg

Description

@mlugg

This proposal assumes #19754; in particular, it assumes that packed unions have a backing integer which can be explicitly specified, much like packed structs do.

Background

Consider this snippet:

/// This long definition has 256 fields so that the inferred integer tag type
/// is `u8`. We need to do that by having 256 fields because in Zig you can't
/// set enum tag values (`foo = 255`) without an explicitly specified backing
/// integer, which we are intentionally avoiding here.
const Enum = enum {
    // zig fmt: off
    _00, _01, _02, _03, _04, _05, _06, _07, _08, _09, _0a, _0b, _0c, _0d, _0e, _0f,
    _10, _11, _12, _13, _14, _15, _16, _17, _18, _19, _1a, _1b, _1c, _1d, _1e, _1f,
    _20, _21, _22, _23, _24, _25, _26, _27, _28, _29, _2a, _2b, _2c, _2d, _2e, _2f,
    _30, _31, _32, _33, _34, _35, _36, _37, _38, _39, _3a, _3b, _3c, _3d, _3e, _3f,
    _40, _41, _42, _43, _44, _45, _46, _47, _48, _49, _4a, _4b, _4c, _4d, _4e, _4f,
    _50, _51, _52, _53, _54, _55, _56, _57, _58, _59, _5a, _5b, _5c, _5d, _5e, _5f,
    _60, _61, _62, _63, _64, _65, _66, _67, _68, _69, _6a, _6b, _6c, _6d, _6e, _6f,
    _70, _71, _72, _73, _74, _75, _76, _77, _78, _79, _7a, _7b, _7c, _7d, _7e, _7f,
    _80, _81, _82, _83, _84, _85, _86, _87, _88, _89, _8a, _8b, _8c, _8d, _8e, _8f,
    _90, _91, _92, _93, _94, _95, _96, _97, _98, _99, _9a, _9b, _9c, _9d, _9e, _9f,
    _a0, _a1, _a2, _a3, _a4, _a5, _a6, _a7, _a8, _a9, _aa, _ab, _ac, _ad, _ae, _af,
    _b0, _b1, _b2, _b3, _b4, _b5, _b6, _b7, _b8, _b9, _ba, _bb, _bc, _bd, _be, _bf,
    _c0, _c1, _c2, _c3, _c4, _c5, _c6, _c7, _c8, _c9, _ca, _cb, _cc, _cd, _ce, _cf,
    _d0, _d1, _d2, _d3, _d4, _d5, _d6, _d7, _d8, _d9, _da, _db, _dc, _dd, _de, _df,
    _e0, _e1, _e2, _e3, _e4, _e5, _e6, _e7, _e8, _e9, _ea, _eb, _ec, _ed, _ee, _ef,
    _f0, _f1, _f2, _f3, _f4, _f5, _f6, _f7, _f8, _f9, _fa, _fb, _fc, _fd, _fe, _ff,
    // zig fmt: on
};

/// The inferred backing type is `u8`.
const PackedStruct = packed struct { x: u8 };

/// The inferred backing type is `u8`.
const PackedUnion = packed union { x: u8 };

comptime {
    assert(@typeInfo(Enum).@"enum".tag_type == u8);
    assert(@typeInfo(PackedStruct).@"struct".backing_integer.? == u8);
    // `std.builtin.Type.Union.backing_integer` doesn't currently exist, but it
    // will under the accepted proposal #19754
    //assert(@typeInfo(PackedUnion).@"union".backing_integer.? == u8);
}

const assert = @import("std").debug.assert;

This code is fine, and the assertions pass.

Here's the thing, though. These three types all have an inferred unsigned integer backing type based on their bit count (all u8). That type changes based on the type's fields, so is at best pretty implicit. In addition, the user has not explicitly specified that the backing type should be unsigned, and the signedness of the backing type has ABI implications (C calling conventions may differ in how they pass signed and unsigned types). As such, it's kind of problematic that this code compiles with the above definitions:

export fn foo(a: Enum, b: PackedStruct, c: PackedUnion) void {
    bar(a, b, c);
}
extern fn bar(a: Enum, b: PackedStruct, c: PackedUnion) void;

The ABI behavior here is implicit. Even worse, unsigned and signed types have the same parameter passing convention in most calling conventions, so if this code is wrong, the bug is unlikely to be noticed. Users coming from C may also expect to be able to turn a C enum { ... } into a Zig enum { ... }, but the layout is significantly different (c_int vs a small uint), so passing such a type over ABI boundaries is a potential footgun for new users in particular.

Proposal

enums, packed structs, and packed unions should only be allowed in extern contexts if the backing type was explicitly specified.

So, the previous snippet should emit compile errors which look something like this:

error: parameter of type 'Enum' not allowed in function with calling convention 'c'
note: enum must have explicit backing type to determine its ABI representation

Packability

Another thing worth briefly discussing is whether these types are packable, meaning they are allowed as fields of packed structs and packed unions. The status quo behavior is that enums with implicit tag types are not packable, while packed structs and packed unions with implicit backing types are packable. This behavior might feel inconsistent at first, but it probably makes sense: the number of bits of an enum with an implicit integer tag type is not obvious (you'd need to count the fields!), while the number of bits of a packed struct or packed union is fairly obvious even with an implicit backing type: you just add up its fields. As such, status quo semantics here are probably reasonable. If we did want to consider any change to them, that would be a separate proposal.

Metadata

Metadata

Assignees

No one assigned

    Labels

    acceptedThis proposal is planned.breakingImplementing this issue could cause existing code to no longer compile or have different behavior.proposalThis issue suggests modifications. If it also has the "accepted" label then it is planned.

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions