diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-04-04 19:22:04 +0300 |
|---|---|---|
| committer | Can Celik <ogulcancelik@gmail.com> | 2026-04-05 01:23:06 +0300 |
| commit | 41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch) | |
| tree | 3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/lib | |
| parent | 55c138263e190a1e79d4b997bfb9397306c0ec1f (diff) | |
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/lib')
| -rw-r--r-- | vendor/libghostty-vt/src/lib/allocator.zig | 264 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/lib/allocator/convenience.zig | 59 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/lib/enum.zig | 145 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/lib/main.zig | 19 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/lib/string.zig | 15 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/lib/struct.zig | 105 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/lib/target.zig | 6 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/lib/types.zig | 13 | ||||
| -rw-r--r-- | vendor/libghostty-vt/src/lib/union.zig | 171 |
9 files changed, 797 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/lib/allocator.zig b/vendor/libghostty-vt/src/lib/allocator.zig new file mode 100644 index 00000000..fc56033a --- /dev/null +++ b/vendor/libghostty-vt/src/lib/allocator.zig @@ -0,0 +1,264 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const testing = std.testing; + +/// Convenience functions +pub const convenience = @import("allocator/convenience.zig"); + +/// Useful alias since they're required to create Zig allocators +pub const ZigVTable = std.mem.Allocator.VTable; + +/// The VTable required by the C interface. +/// C: GhosttyAllocatorVtable +pub const VTable = extern struct { + alloc: *const fn (*anyopaque, len: usize, alignment: u8, ret_addr: usize) callconv(.c) ?[*]u8, + resize: *const fn (*anyopaque, memory: [*]u8, memory_len: usize, alignment: u8, new_len: usize, ret_addr: usize) callconv(.c) bool, + remap: *const fn (*anyopaque, memory: [*]u8, memory_len: usize, alignment: u8, new_len: usize, ret_addr: usize) callconv(.c) ?[*]u8, + free: *const fn (*anyopaque, memory: [*]u8, memory_len: usize, alignment: u8, ret_addr: usize) callconv(.c) void, +}; + +/// Returns an allocator to use for the given possibly-null C allocator, +/// ensuring some allocator is always returned. +pub fn default(c_alloc_: ?*const Allocator) std.mem.Allocator { + // If we're given an allocator, use it. + if (c_alloc_) |c_alloc| return c_alloc.zig(); + + // Tests always use the test allocator so we can detect leaks. + if (comptime builtin.is_test) return testing.allocator; + + // If we have libc, use that. We prefer libc if we have it because + // its generally fast but also lets the embedder easily override + // malloc/free with custom allocators like mimalloc or something. + if (comptime builtin.link_libc) return std.heap.c_allocator; + + // Wasm + if (comptime builtin.target.cpu.arch.isWasm()) return std.heap.wasm_allocator; + + // No libc, use the preferred allocator for releases which is the + // Zig SMP allocator. + return std.heap.smp_allocator; +} + +/// The Allocator interface for custom memory allocation strategies +/// within C libghostty APIs. +/// +/// This -- purposely -- matches the Zig allocator interface. We do this +/// for two reasons: (1) Zig's allocator interface is well proven in +/// the real world to be flexible and useful, and (2) it allows us to +/// easily convert C allocators to Zig allocators and vice versa, since +/// we're written in Zig. +/// +/// C: GhosttyAllocator +pub const Allocator = extern struct { + ctx: *anyopaque, + vtable: *const VTable, + + /// vtable for the Zig allocator interface to map our extern + /// allocator to Zig's allocator interface. + pub const zig_vtable: ZigVTable = .{ + .alloc = alloc, + .resize = resize, + .remap = remap, + .free = free, + }; + + /// Create a C allocator from a Zig allocator. This requires that + /// the Zig allocator be pointer-stable for the lifetime of the + /// C allocator. + pub fn fromZig(zig_alloc: *const std.mem.Allocator) Allocator { + return .{ + .ctx = @ptrCast(@constCast(zig_alloc)), + .vtable = &ZigAllocator.vtable, + }; + } + + /// Create a Zig allocator from this C allocator. This requires + /// a pointer to a Zig allocator vtable that we can populate with + /// our callbacks. + pub fn zig(self: *const Allocator) std.mem.Allocator { + return .{ + .ptr = @ptrCast(@constCast(self)), + .vtable = &zig_vtable, + }; + } + + fn alloc( + ctx: *anyopaque, + len: usize, + alignment: std.mem.Alignment, + ra: usize, + ) ?[*]u8 { + const self: *Allocator = @ptrCast(@alignCast(ctx)); + return self.vtable.alloc( + self.ctx, + len, + @intFromEnum(alignment), + ra, + ); + } + + fn resize( + ctx: *anyopaque, + old_mem: []u8, + alignment: std.mem.Alignment, + new_len: usize, + ra: usize, + ) bool { + const self: *Allocator = @ptrCast(@alignCast(ctx)); + return self.vtable.resize( + self.ctx, + old_mem.ptr, + old_mem.len, + @intFromEnum(alignment), + new_len, + ra, + ); + } + + fn remap( + ctx: *anyopaque, + old_mem: []u8, + alignment: std.mem.Alignment, + new_len: usize, + ra: usize, + ) ?[*]u8 { + const self: *Allocator = @ptrCast(@alignCast(ctx)); + return self.vtable.remap( + self.ctx, + old_mem.ptr, + old_mem.len, + @intFromEnum(alignment), + new_len, + ra, + ); + } + + fn free( + ctx: *anyopaque, + old_mem: []u8, + alignment: std.mem.Alignment, + ra: usize, + ) void { + const self: *Allocator = @ptrCast(@alignCast(ctx)); + self.vtable.free( + self.ctx, + old_mem.ptr, + old_mem.len, + @intFromEnum(alignment), + ra, + ); + } +}; + +/// An allocator implementation that wraps a Zig allocator so that +/// it can be exposed to C. +const ZigAllocator = struct { + const vtable: VTable = .{ + .alloc = alloc, + .resize = resize, + .remap = remap, + .free = free, + }; + + fn alloc( + ctx: *anyopaque, + len: usize, + alignment: u8, + ra: usize, + ) callconv(.c) ?[*]u8 { + const zig_alloc: *const std.mem.Allocator = @ptrCast(@alignCast(ctx)); + return zig_alloc.vtable.alloc( + zig_alloc.ptr, + len, + @enumFromInt(alignment), + ra, + ); + } + + fn resize( + ctx: *anyopaque, + memory: [*]u8, + memory_len: usize, + alignment: u8, + new_len: usize, + ra: usize, + ) callconv(.c) bool { + const zig_alloc: *const std.mem.Allocator = @ptrCast(@alignCast(ctx)); + return zig_alloc.vtable.resize( + zig_alloc.ptr, + memory[0..memory_len], + @enumFromInt(alignment), + new_len, + ra, + ); + } + + fn remap( + ctx: *anyopaque, + memory: [*]u8, + memory_len: usize, + alignment: u8, + new_len: usize, + ra: usize, + ) callconv(.c) ?[*]u8 { + const zig_alloc: *const std.mem.Allocator = @ptrCast(@alignCast(ctx)); + return zig_alloc.vtable.remap( + zig_alloc.ptr, + memory[0..memory_len], + @enumFromInt(alignment), + new_len, + ra, + ); + } + + fn free( + ctx: *anyopaque, + memory: [*]u8, + memory_len: usize, + alignment: u8, + ra: usize, + ) callconv(.c) void { + const zig_alloc: *const std.mem.Allocator = @ptrCast(@alignCast(ctx)); + return zig_alloc.vtable.free( + zig_alloc.ptr, + memory[0..memory_len], + @enumFromInt(alignment), + ra, + ); + } +}; + +/// libc Allocator, requires linking libc +pub const c_allocator: Allocator = .fromZig(&std.heap.c_allocator); + +/// Allocator that can be sent to the C API that does full +/// leak checking within Zig tests. This should only be used from +/// Zig tests. +pub const test_allocator: Allocator = b: { + if (!builtin.is_test) @compileError("test_allocator can only be used in tests"); + break :b .fromZig(&testing.allocator); +}; + +test "c allocator" { + if (!comptime builtin.link_libc) return error.SkipZigTest; + + const alloc = c_allocator.zig(); + const str = try alloc.alloc(u8, 10); + defer alloc.free(str); + try testing.expectEqual(10, str.len); +} + +test "fba allocator" { + var buf: [1024]u8 = undefined; + var fba: std.heap.FixedBufferAllocator = .init(&buf); + const zig_alloc = fba.allocator(); + + // Convert the Zig allocator to a C interface + const c_alloc: Allocator = .fromZig(&zig_alloc); + + // Convert back to Zig so we can test it. + const alloc = c_alloc.zig(); + const str = try alloc.alloc(u8, 10); + defer alloc.free(str); + try testing.expectEqual(10, str.len); +} diff --git a/vendor/libghostty-vt/src/lib/allocator/convenience.zig b/vendor/libghostty-vt/src/lib/allocator/convenience.zig new file mode 100644 index 00000000..0f5f88d2 --- /dev/null +++ b/vendor/libghostty-vt/src/lib/allocator/convenience.zig @@ -0,0 +1,59 @@ +//! This contains convenience functions for allocating various types. +//! +//! The primary use case for this is Wasm builds. Ghostty relies a lot on +//! pointers to various types for ABI compatibility and creating those pointers +//! in Wasm is tedious. This file contains a purely additive set of functions +//! that can be exposed to the Wasm module without changing the API from the +//! C library. +//! +//! Given these are convenience methods, they always use the default allocator. +//! If a caller is using a custom allocator, they have the expertise to +//! allocate these types manually using their custom allocator. + +// Get our default allocator at comptime since it is known. +const default = @import("../allocator.zig").default; +const alloc = default(null); + +pub const Opaque = *anyopaque; + +pub fn allocOpaque() callconv(.c) ?*Opaque { + return alloc.create(*anyopaque) catch return null; +} + +pub fn freeOpaque(ptr: ?*Opaque) callconv(.c) void { + if (ptr) |p| alloc.destroy(p); +} + +pub fn allocU8Array(len: usize) callconv(.c) ?[*]u8 { + const slice = alloc.alloc(u8, len) catch return null; + return slice.ptr; +} + +pub fn freeU8Array(ptr: ?[*]u8, len: usize) callconv(.c) void { + if (ptr) |p| alloc.free(p[0..len]); +} + +pub fn allocU16Array(len: usize) callconv(.c) ?[*]u16 { + const slice = alloc.alloc(u16, len) catch return null; + return slice.ptr; +} + +pub fn freeU16Array(ptr: ?[*]u16, len: usize) callconv(.c) void { + if (ptr) |p| alloc.free(p[0..len]); +} + +pub fn allocU8() callconv(.c) ?*u8 { + return alloc.create(u8) catch return null; +} + +pub fn freeU8(ptr: ?*u8) callconv(.c) void { + if (ptr) |p| alloc.destroy(p); +} + +pub fn allocUsize() callconv(.c) ?*usize { + return alloc.create(usize) catch return null; +} + +pub fn freeUsize(ptr: ?*usize) callconv(.c) void { + if (ptr) |p| alloc.destroy(p); +} diff --git a/vendor/libghostty-vt/src/lib/enum.zig b/vendor/libghostty-vt/src/lib/enum.zig new file mode 100644 index 00000000..76e05fa9 --- /dev/null +++ b/vendor/libghostty-vt/src/lib/enum.zig @@ -0,0 +1,145 @@ +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 +/// backing type. +/// +/// In all cases, the enum keys will be created in the order given. +/// For C ABI, this means that the order MUST NOT be changed in order +/// to preserve ABI compatibility. You can set a key to null to +/// remove it from the Zig enum while keeping the "hole" in the C enum +/// to preserve ABI compatibility. +/// +/// C detection is up to the caller, since there are multiple ways +/// to do that. We rely on the `target` parameter to determine whether we +/// should create a C compatible enum or a Zig enum. +/// +/// For the Zig enum, the enum value is not guaranteed to be stable, so +/// it shouldn't be relied for things like serialization. +pub fn Enum( + target: Target, + keys: []const ?[:0]const u8, +) type { + var fields: [keys.len]std.builtin.Type.EnumField = undefined; + var fields_i: usize = 0; + var holes: usize = 0; + for (keys) |key_| { + const key: [:0]const u8 = key_ orelse { + switch (target) { + // For Zig we don't track holes because the enum value + // isn't guaranteed to be stable and we want to use the + // smallest possible backing type. + .zig => {}, + + // For C we must track holes to preserve ABI compatibility + // with subsequent values. + .c => holes += 1, + } + continue; + }; + + fields[fields_i] = .{ + .name = key, + .value = fields_i + holes, + }; + fields_i += 1; + } + + // Assigned to var so that the type name is nicer in stack traces. + const Result = @Type(.{ .@"enum" = .{ + .tag_type = switch (target) { + .c => c_int, + .zig => std.math.IntFittingRange(0, fields_i - 1), + }, + .fields = fields[0..fields_i], + .decls = &.{}, + .is_exhaustive = true, + } }); + return Result; +} + +test "zig" { + const testing = std.testing; + const T = Enum(.zig, &.{ "a", "b", "c", "d" }); + const info = @typeInfo(T).@"enum"; + try testing.expectEqual(u2, info.tag_type); +} + +test "c" { + const testing = std.testing; + const T = Enum(.c, &.{ "a", "b", "c", "d" }); + const info = @typeInfo(T).@"enum"; + try testing.expectEqual(c_int, info.tag_type); +} + +test "abi by removing a key" { + const testing = std.testing; + // C + { + const T = Enum(.c, &.{ "a", "b", null, "d" }); + const info = @typeInfo(T).@"enum"; + try testing.expectEqual(c_int, info.tag_type); + try testing.expectEqual(3, @intFromEnum(T.d)); + } + + // Zig + { + const T = Enum(.zig, &.{ "a", "b", null, "d" }); + const info = @typeInfo(T).@"enum"; + try testing.expectEqual(u2, info.tag_type); + try testing.expectEqual(2, @intFromEnum(T.d)); + } +} + +/// Verify that for every key in enum T, there is a matching declaration in +/// `ghostty.h` with the correct value. This should only ever be called inside a `test` +/// because the `ghostty.h` module is only available then. +pub fn checkGhosttyHEnum( + comptime T: type, + comptime prefix: []const u8, +) !void { + const info = @typeInfo(T); + + try std.testing.expect(info == .@"enum"); + try std.testing.expect(info.@"enum".tag_type == c_int); + try std.testing.expect(info.@"enum".is_exhaustive == true); + + @setEvalBranchQuota(100_000); + + const c = @import("ghostty.h"); + + var set: std.EnumSet(T) = .initFull(); + + const enum_fields = info.@"enum".fields; + + inline for (enum_fields) |field| { + const expected_name: *const [prefix.len + field.name.len]u8 = comptime e: { + var buf: [prefix.len + field.name.len]u8 = undefined; + @memcpy(buf[0..prefix.len], prefix); + for (buf[prefix.len..], field.name) |*d, s| { + d.* = std.ascii.toUpper(s); + } + break :e &buf; + }; + + if (@hasDecl(c, expected_name)) { + std.testing.expectEqual(field.value, @field(c, expected_name)) catch |e| { + std.log.err(@typeName(T) ++ " key " ++ field.name ++ " does not have the same backing int as " ++ expected_name, .{}); + return e; + }; + + set.remove(@enumFromInt(field.value)); + } + } + + std.testing.expect(set.count() == 0) catch |e| { + var it = set.iterator(); + while (it.next()) |v| { + var buf: [128]u8 = undefined; + const upper_string = std.ascii.upperString(&buf, @tagName(v)); + std.log.err("ghostty.h is missing value for {s}{s}", .{ prefix, upper_string }); + } + return e; + }; +} diff --git a/vendor/libghostty-vt/src/lib/main.zig b/vendor/libghostty-vt/src/lib/main.zig new file mode 100644 index 00000000..05ebe9bd --- /dev/null +++ b/vendor/libghostty-vt/src/lib/main.zig @@ -0,0 +1,19 @@ +const std = @import("std"); +const enumpkg = @import("enum.zig"); +const structpkg = @import("struct.zig"); +const types = @import("types.zig"); +const unionpkg = @import("union.zig"); + +pub const allocator = @import("allocator.zig"); +pub const Enum = enumpkg.Enum; +pub const checkGhosttyHEnum = enumpkg.checkGhosttyHEnum; +pub const String = types.String; +pub const Struct = structpkg.Struct; +pub const structSizedFieldFits = structpkg.sizedFieldFits; +pub const Target = @import("target.zig").Target; +pub const TaggedUnion = unionpkg.TaggedUnion; +pub const cutPrefix = @import("string.zig").cutPrefix; + +test { + std.testing.refAllDecls(@This()); +} diff --git a/vendor/libghostty-vt/src/lib/string.zig b/vendor/libghostty-vt/src/lib/string.zig new file mode 100644 index 00000000..795823c2 --- /dev/null +++ b/vendor/libghostty-vt/src/lib/string.zig @@ -0,0 +1,15 @@ +const std = @import("std"); + +// This is a copy of std.mem.cutPrefix from 0.16. Once Ghostty has been ported +// to 0.16 this can be removed. + +/// If slice starts with prefix, returns the rest of slice starting at +/// prefix.len. +pub fn cutPrefix(comptime T: type, slice: []const T, prefix: []const T) ?[]const T { + return if (std.mem.startsWith(T, slice, prefix)) slice[prefix.len..] else null; +} + +test cutPrefix { + try std.testing.expectEqualStrings("foo", cutPrefix(u8, "--example=foo", "--example=").?); + try std.testing.expectEqual(null, cutPrefix(u8, "--example=foo", "-example=")); +} diff --git a/vendor/libghostty-vt/src/lib/struct.zig b/vendor/libghostty-vt/src/lib/struct.zig new file mode 100644 index 00000000..bbf07ece --- /dev/null +++ b/vendor/libghostty-vt/src/lib/struct.zig @@ -0,0 +1,105 @@ +const std = @import("std"); +const testing = std.testing; +const Target = @import("target.zig").Target; + +/// Create a struct type that is C ABI compatible from a Zig struct type. +/// +/// When the target is `.zig`, the original struct type is returned as-is. +/// When the target is `.c`, the struct is recreated with an `extern` layout, +/// ensuring a stable, C-compatible memory layout. +/// +/// This handles packed structs by resolving zero alignments to the natural +/// alignment of each field's type, since extern structs require explicit +/// alignment. This means packed struct fields like `bool` will take up +/// their full size (1 byte) rather than being bit-packed. +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 = if (field.alignment > 0) field.alignment else @alignOf(field.type), + }; + } + + break :c @Type(.{ .@"struct" = .{ + .layout = .@"extern", + .fields = &fields, + .decls = &.{}, + .is_tuple = info.is_tuple, + } }); + }, + }; +} + +/// Returns true if a struct of type `T` with size `size` can set +/// field `field` (if it fits within the size). This is used for ABI +/// compatibility for structs that have an explicit size field. +pub fn sizedFieldFits( + comptime T: type, + size: usize, + comptime field: []const u8, +) bool { + const offset = @offsetOf(T, field); + const field_size = @sizeOf(@FieldType(T, field)); + return size >= offset + field_size; +} + +test "sizedFieldFits boundary checks" { + const Sized = extern struct { + size: usize, + a: u8, + b: u32, + }; + + const size_required = @offsetOf(Sized, "size") + @sizeOf(@FieldType(Sized, "size")); + const a_required = @offsetOf(Sized, "a") + @sizeOf(@FieldType(Sized, "a")); + const b_required = @offsetOf(Sized, "b") + @sizeOf(@FieldType(Sized, "b")); + + try testing.expect(sizedFieldFits(Sized, size_required, "size")); + try testing.expect(!sizedFieldFits(Sized, size_required - 1, "size")); + + try testing.expect(sizedFieldFits(Sized, a_required, "a")); + try testing.expect(!sizedFieldFits(Sized, a_required - 1, "a")); + + try testing.expect(sizedFieldFits(Sized, b_required, "b")); + try testing.expect(!sizedFieldFits(Sized, b_required - 1, "b")); +} + +test "sizedFieldFits respects alignment padding" { + const Sized = extern struct { + size: usize, + a: u8, + b: u32, + }; + + const up_to_padding = @offsetOf(Sized, "b"); + try testing.expect(sizedFieldFits(Sized, up_to_padding, "a")); + try testing.expect(!sizedFieldFits(Sized, up_to_padding, "b")); +} + +test "packed struct converts to extern with full-size bools" { + const Packed = packed struct { + flag1: bool, + flag2: bool, + value: u8, + }; + + const C = Struct(.c, Packed); + const info = @typeInfo(C).@"struct"; + + try testing.expectEqual(.@"extern", info.layout); + try testing.expectEqual(@as(usize, 1), @sizeOf(@FieldType(C, "flag1"))); + try testing.expectEqual(@as(usize, 1), @sizeOf(@FieldType(C, "flag2"))); + try testing.expectEqual(@as(usize, 1), @sizeOf(@FieldType(C, "value"))); + try testing.expectEqual(@as(usize, 3), @sizeOf(C)); +} diff --git a/vendor/libghostty-vt/src/lib/target.zig b/vendor/libghostty-vt/src/lib/target.zig new file mode 100644 index 00000000..8d7a7fb8 --- /dev/null +++ b/vendor/libghostty-vt/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/vendor/libghostty-vt/src/lib/types.zig b/vendor/libghostty-vt/src/lib/types.zig new file mode 100644 index 00000000..758540d1 --- /dev/null +++ b/vendor/libghostty-vt/src/lib/types.zig @@ -0,0 +1,13 @@ +pub const String = extern struct { + ptr: [*]const u8, + len: usize, + + pub fn init(zig: anytype) String { + return switch (@TypeOf(zig)) { + []u8, []const u8 => .{ + .ptr = zig.ptr, + .len = zig.len, + }, + }; + } +}; diff --git a/vendor/libghostty-vt/src/lib/union.zig b/vendor/libghostty-vt/src/lib/union.zig new file mode 100644 index 00000000..924d0e86 --- /dev/null +++ b/vendor/libghostty-vt/src/lib/union.zig @@ -0,0 +1,171 @@ +const std = @import("std"); +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")) break :value 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 { + @setEvalBranchQuota(10000); + 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); +} |
