lib: add a `TaggedUnion` helper to create C ABI compatible tagged unions

pull/9342/head
Mitchell Hashimoto 2025-10-23 14:03:34 -07:00
parent fb5b8d7968
commit 099dcbe04d
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
6 changed files with 214 additions and 6 deletions

View File

@ -1,4 +1,5 @@
const std = @import("std");
const Target = @import("target.zig").Target;
/// Create an enum type with the given keys that is C ABI compatible
/// if we're targeting C, otherwise a Zig enum with smallest possible
@ -58,11 +59,6 @@ pub fn Enum(
return Result;
}
pub const Target = union(enum) {
c,
zig,
};
test "zig" {
const testing = std.testing;
const T = Enum(.zig, &.{ "a", "b", "c", "d" });

View File

@ -1,9 +1,12 @@
const std = @import("std");
const enumpkg = @import("enum.zig");
const unionpkg = @import("union.zig");
pub const allocator = @import("allocator.zig");
pub const Enum = enumpkg.Enum;
pub const EnumTarget = enumpkg.Target;
pub const Struct = @import("struct.zig").Struct;
pub const Target = @import("target.zig").Target;
pub const TaggedUnion = unionpkg.TaggedUnion;
test {
std.testing.refAllDecls(@This());

31
src/lib/struct.zig Normal file
View File

@ -0,0 +1,31 @@
const std = @import("std");
const Target = @import("target.zig").Target;
pub fn Struct(
comptime target: Target,
comptime Zig: type,
) type {
return switch (target) {
.zig => Zig,
.c => c: {
const info = @typeInfo(Zig).@"struct";
var fields: [info.fields.len]std.builtin.Type.StructField = undefined;
for (info.fields, 0..) |field, i| {
fields[i] = .{
.name = field.name,
.type = field.type,
.default_value_ptr = field.default_value_ptr,
.is_comptime = field.is_comptime,
.alignment = field.alignment,
};
}
break :c @Type(.{ .@"struct" = .{
.layout = .@"extern",
.fields = &fields,
.decls = &.{},
.is_tuple = info.is_tuple,
} });
},
};
}

6
src/lib/target.zig Normal file
View File

@ -0,0 +1,6 @@
/// The target for ABI generation. The detection of this is left to the
/// caller since there are multiple ways to do that.
pub const Target = union(enum) {
c,
zig,
};

171
src/lib/union.zig Normal file
View File

@ -0,0 +1,171 @@
const std = @import("std");
const assert = std.debug.assert;
const testing = std.testing;
const Target = @import("target.zig").Target;
/// Create a tagged union type that supports a C ABI and maintains
/// C ABI compatibility when adding new tags. This returns a set of types
/// and functions to augment the given Union type, not create a wholly new
/// union type.
///
/// The C ABI compatible types and functions are only available when the
/// target produces C values.
///
/// The `Union` type should be a standard Zig tagged union. The tag type
/// should be explicit (i.e. not `union(enum)`) and the tag type should
/// be an enum created with the `Enum` function in this library, so that
/// automatic C ABI compatibility is ensured.
///
/// The `Padding` type is a type that is always added to the C union
/// with the key `_padding`. This should be set to a type that has the size
/// and alignment needed to pad the C union to the expected size. This
/// should never change to ensure ABI compatibility.
pub fn TaggedUnion(
comptime target: Target,
comptime Union: type,
comptime Padding: type,
) type {
return struct {
comptime {
switch (target) {
.zig => {},
// For ABI compatibility, we expect that this is our union size.
.c => if (@sizeOf(CValue) != @sizeOf(Padding)) {
@compileLog(@sizeOf(CValue));
@compileError("TaggedUnion CValue size does not match expected fixed size");
},
}
}
/// The tag type.
pub const Tag = @typeInfo(Union).@"union".tag_type.?;
/// The Zig union.
pub const Zig = Union;
/// The C ABI compatible tagged union type.
pub const C = switch (target) {
.zig => struct {},
.c => extern struct {
tag: Tag,
value: CValue,
},
};
/// The C ABI compatible union value type.
pub const CValue = cvalue: {
switch (target) {
.zig => break :cvalue extern struct {},
.c => {},
}
const tag_fields = @typeInfo(Tag).@"enum".fields;
var union_fields: [tag_fields.len + 1]std.builtin.Type.UnionField = undefined;
for (tag_fields, 0..) |field, i| {
const action = @unionInit(Union, field.name, undefined);
const Type = t: {
const Type = @TypeOf(@field(action, field.name));
// Types can provide custom types for their CValue.
switch (@typeInfo(Type)) {
.@"enum", .@"struct", .@"union" => if (@hasDecl(Type, "C")) break :t Type.C,
else => {},
}
break :t Type;
};
union_fields[i] = .{
.name = field.name,
.type = Type,
.alignment = @alignOf(Type),
};
}
union_fields[tag_fields.len] = .{
.name = "_padding",
.type = Padding,
.alignment = @alignOf(Padding),
};
break :cvalue @Type(.{ .@"union" = .{
.layout = .@"extern",
.tag_type = null,
.fields = &union_fields,
.decls = &.{},
} });
};
/// Convert to C union.
pub fn cval(self: Union) C {
const value: CValue = switch (self) {
inline else => |v, tag| @unionInit(
CValue,
@tagName(tag),
value: {
switch (@typeInfo(@TypeOf(v))) {
.@"enum", .@"struct", .@"union" => if (@hasDecl(@TypeOf(v), "cval")) v.cval(),
else => {},
}
break :value v;
},
),
};
return .{
.tag = @as(Tag, self),
.value = value,
};
}
/// Returns the value type for the given tag.
pub fn Value(comptime tag: Tag) type {
inline for (@typeInfo(Union).@"union".fields) |field| {
const field_tag = @field(Tag, field.name);
if (field_tag == tag) return field.type;
}
unreachable;
}
};
}
test "TaggedUnion: matching size" {
const Tag = enum(c_int) { a, b };
const U = TaggedUnion(
.c,
union(Tag) {
a: u32,
b: u64,
},
u64,
);
try testing.expectEqual(8, @sizeOf(U.CValue));
}
test "TaggedUnion: padded size" {
const Tag = enum(c_int) { a };
const U = TaggedUnion(
.c,
union(Tag) {
a: u32,
},
u64,
);
try testing.expectEqual(8, @sizeOf(U.CValue));
}
test "TaggedUnion: c conversion" {
const Tag = enum(c_int) { a, b };
const U = TaggedUnion(.c, union(Tag) {
a: u32,
b: u64,
}, u64);
const c = U.cval(.{ .a = 42 });
try testing.expectEqual(Tag.a, c.tag);
try testing.expectEqual(42, c.value.a);
}

View File

@ -193,6 +193,7 @@ test {
_ = @import("crash/main.zig");
_ = @import("datastruct/main.zig");
_ = @import("inspector/main.zig");
_ = @import("lib/main.zig");
_ = @import("terminal/main.zig");
_ = @import("terminfo/main.zig");
_ = @import("simd/main.zig");