mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-06 07:38:21 +00:00
146 lines
4.7 KiB
Zig
146 lines
4.7 KiB
Zig
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(1000000);
|
|
|
|
const c = @import("ghostty.h");
|
|
|
|
var set: std.EnumSet(T) = .initFull();
|
|
|
|
const c_decls = @typeInfo(c).@"struct".decls;
|
|
const enum_fields = info.@"enum".fields;
|
|
|
|
inline for (enum_fields) |field| {
|
|
const upper_name = comptime u: {
|
|
var buf: [128]u8 = undefined;
|
|
break :u std.ascii.upperString(&buf, field.name);
|
|
};
|
|
|
|
inline for (c_decls) |decl| {
|
|
if (!comptime std.mem.startsWith(u8, decl.name, prefix)) continue;
|
|
|
|
const suffix = decl.name[prefix.len..];
|
|
|
|
if (!comptime std.mem.eql(u8, suffix, upper_name)) continue;
|
|
|
|
std.testing.expectEqual(field.value, @field(c, decl.name)) catch |e| {
|
|
std.log.err(@typeName(T) ++ " key " ++ field.name ++ " does not have the same backing int as " ++ decl.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;
|
|
};
|
|
}
|