diff --git a/src/lib/enum.zig b/src/lib/enum.zig index c3971ebde..6fc759846 100644 --- a/src/lib/enum.zig +++ b/src/lib/enum.zig @@ -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" }); diff --git a/src/lib/main.zig b/src/lib/main.zig index 4ef8dcb2d..cdddade09 100644 --- a/src/lib/main.zig +++ b/src/lib/main.zig @@ -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()); diff --git a/src/lib/struct.zig b/src/lib/struct.zig new file mode 100644 index 000000000..d494da2e6 --- /dev/null +++ b/src/lib/struct.zig @@ -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, + } }); + }, + }; +} diff --git a/src/lib/target.zig b/src/lib/target.zig new file mode 100644 index 000000000..8d7a7fb89 --- /dev/null +++ b/src/lib/target.zig @@ -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, +}; diff --git a/src/lib/union.zig b/src/lib/union.zig new file mode 100644 index 000000000..f19cd3c7f --- /dev/null +++ b/src/lib/union.zig @@ -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); +} diff --git a/src/main_ghostty.zig b/src/main_ghostty.zig index decfc609c..77b7f3ef4 100644 --- a/src/main_ghostty.zig +++ b/src/main_ghostty.zig @@ -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");