mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-08-24 16:11:43 +00:00
libghostty: expand ABI type metadata
The type metadata export only described extern struct layouts, leaving embedders to mirror enum values and tagged union relationships. Describe every public C type in a versioned manifest with target and build metadata. Keep union field renames alongside their source tagged unions so the manifest uses public C names without changing Zig value layouts.
This commit is contained in:
@@ -156,7 +156,7 @@
|
||||
|
||||
// Look up a field's offset and DataView setter from the type layout JSON.
|
||||
function fieldInfo(structName, fieldName) {
|
||||
const field = typeLayout[structName].fields[fieldName];
|
||||
const field = typeLayout.types[structName].fields[fieldName];
|
||||
return field;
|
||||
}
|
||||
|
||||
@@ -166,9 +166,16 @@
|
||||
switch (field.type) {
|
||||
case 'u8': case 'bool': view.setUint8(field.offset, value); break;
|
||||
case 'u16': view.setUint16(field.offset, value, true); break;
|
||||
case 'u32': case 'enum': view.setUint32(field.offset, value, true); break;
|
||||
case 'u32': view.setUint32(field.offset, value, true); break;
|
||||
case 'u64': view.setBigUint64(field.offset, BigInt(value), true); break;
|
||||
default: throw new Error(`Unsupported field type: ${field.type}`);
|
||||
default: {
|
||||
const type = typeLayout.types[field.type];
|
||||
if (type?.kind === 'enum' && type.underlying === 'i32') {
|
||||
view.setInt32(field.offset, value, true);
|
||||
break;
|
||||
}
|
||||
throw new Error(`Unsupported field type: ${field.type}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,15 +193,14 @@
|
||||
.replace(/\\\\/g, '\\');
|
||||
}
|
||||
|
||||
// GHOSTTY_FORMATTER_FORMAT_PLAIN = 0
|
||||
const GHOSTTY_FORMATTER_FORMAT_PLAIN = 0;
|
||||
// GHOSTTY_SUCCESS = 0
|
||||
const GHOSTTY_SUCCESS = 0;
|
||||
|
||||
function run() {
|
||||
const outputDiv = document.getElementById('output');
|
||||
|
||||
try {
|
||||
const GHOSTTY_FORMATTER_FORMAT_PLAIN =
|
||||
typeLayout.types.GhosttyFormatterFormat.values.PLAIN;
|
||||
const GHOSTTY_SUCCESS =
|
||||
typeLayout.types.GhosttyResult.values.SUCCESS;
|
||||
const cols = parseInt(document.getElementById('cols').value, 10);
|
||||
const rows = parseInt(document.getElementById('rows').value, 10);
|
||||
const vtText = parseEscapes(document.getElementById('vtInput').value);
|
||||
@@ -224,7 +230,7 @@
|
||||
wasmInstance.exports.ghostty_wasm_free_u8_array(dataPtr, vtBytes.length);
|
||||
|
||||
// Create a plain-text formatter
|
||||
const FMT_OPTS_SIZE = typeLayout['GhosttyFormatterTerminalOptions'].size;
|
||||
const FMT_OPTS_SIZE = typeLayout.types.GhosttyFormatterTerminalOptions.size;
|
||||
const fmtOptsPtr = wasmInstance.exports.ghostty_wasm_alloc_u8_array(FMT_OPTS_SIZE);
|
||||
new Uint8Array(getBuffer(), fmtOptsPtr, FMT_OPTS_SIZE).fill(0);
|
||||
const fmtOptsView = new DataView(getBuffer(), fmtOptsPtr, FMT_OPTS_SIZE);
|
||||
@@ -235,12 +241,12 @@
|
||||
|
||||
// Set the nested sized-struct `size` fields for extra and extra.screen
|
||||
const extraOffset = fieldInfo('GhosttyFormatterTerminalOptions', 'extra').offset;
|
||||
const extraSize = typeLayout['GhosttyFormatterTerminalExtra'].size;
|
||||
const extraSize = typeLayout.types.GhosttyFormatterTerminalExtra.size;
|
||||
const extraSizeField = fieldInfo('GhosttyFormatterTerminalExtra', 'size');
|
||||
fmtOptsView.setUint32(extraOffset + extraSizeField.offset, extraSize, true);
|
||||
|
||||
const screenOffset = fieldInfo('GhosttyFormatterTerminalExtra', 'screen').offset;
|
||||
const screenSize = typeLayout['GhosttyFormatterScreenExtra'].size;
|
||||
const screenSize = typeLayout.types.GhosttyFormatterScreenExtra.size;
|
||||
const screenSizeField = fieldInfo('GhosttyFormatterScreenExtra', 'size');
|
||||
fmtOptsView.setUint32(extraOffset + screenOffset + screenSizeField.offset, screenSize, true);
|
||||
|
||||
|
||||
@@ -63,6 +63,9 @@ typedef enum GHOSTTY_ENUM_TYPED {
|
||||
GHOSTTY_OSC_COMMAND_CONEMU_XTERM_EMULATION = 20,
|
||||
GHOSTTY_OSC_COMMAND_CONEMU_COMMENT = 21,
|
||||
GHOSTTY_OSC_COMMAND_KITTY_TEXT_SIZING = 22,
|
||||
GHOSTTY_OSC_COMMAND_KITTY_CLIPBOARD_PROTOCOL = 23,
|
||||
GHOSTTY_OSC_COMMAND_KITTY_DND_PROTOCOL = 24,
|
||||
GHOSTTY_OSC_COMMAND_CONTEXT_SIGNAL = 25,
|
||||
GHOSTTY_OSC_COMMAND_TYPE_MAX_VALUE = GHOSTTY_ENUM_MAX_VALUE,
|
||||
} GhosttyOscCommandType;
|
||||
|
||||
|
||||
@@ -332,29 +332,40 @@ typedef struct {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Return a pointer to a null-terminated JSON string describing the
|
||||
* layout of every C API struct for the current target.
|
||||
* Return the versioned libghostty-vt C type manifest for the current target.
|
||||
*
|
||||
* This is primarily useful for language bindings that can't easily
|
||||
* set C struct fields and need to do so via byte offsets. For example,
|
||||
* WebAssembly modules can't share struct definitions with the host.
|
||||
* The manifest defines all the public types available in the linked
|
||||
* build. The types contain their layouts, enum values, union fields, and more.
|
||||
*
|
||||
* Language bindings, such as WebAssembly hosts, should obtain offsets,
|
||||
* sizes, alignments, array shapes, enum constants, and tagged-union arms from
|
||||
* this manifest rather than hardcoding them. Consumers should reject unknown
|
||||
* schema versions and verify the descriptors they require at initialization.
|
||||
*
|
||||
* Example (abbreviated):
|
||||
* @code{.json}
|
||||
* {
|
||||
* "GhosttyMouseEncoderSize": {
|
||||
* "size": 40,
|
||||
* "align": 8,
|
||||
* "fields": {
|
||||
* "size": { "offset": 0, "size": 8, "type": "u64" },
|
||||
* "screen_width": { "offset": 8, "size": 4, "type": "u32" },
|
||||
* "screen_height": { "offset": 12, "size": 4, "type": "u32" },
|
||||
* "cell_width": { "offset": 16, "size": 4, "type": "u32" },
|
||||
* "cell_height": { "offset": 20, "size": 4, "type": "u32" },
|
||||
* "padding_top": { "offset": 24, "size": 4, "type": "u32" },
|
||||
* "padding_bottom": { "offset": 28, "size": 4, "type": "u32" },
|
||||
* "padding_right": { "offset": 32, "size": 4, "type": "u32" },
|
||||
* "padding_left": { "offset": 36, "size": 4, "type": "u32" }
|
||||
* "schema": 1,
|
||||
* "abi": {
|
||||
* "target": "wasm32", "os": "freestanding", "environment": "none",
|
||||
* "pointer_size": 4, "usize_size": 4, "endian": "little"
|
||||
* },
|
||||
* "types": {
|
||||
* "GhosttyRenderStateData": {
|
||||
* "kind": "enum", "size": 4, "align": 4,
|
||||
* "underlying": "i32", "prefix": "GHOSTTY_RENDER_STATE_DATA_",
|
||||
* "values": { "INVALID": 0, "DIRTY": 3, "MAX_VALUE": 2147483647 }
|
||||
* },
|
||||
* "GhosttyStyleColor": {
|
||||
* "kind": "struct", "size": 16, "align": 8,
|
||||
* "fields": {
|
||||
* "tag": { "offset": 0, "size": 4,
|
||||
* "type": "GhosttyStyleColorTag" },
|
||||
* "value": { "offset": 8, "size": 8,
|
||||
* "type": "GhosttyStyleColorValue", "tag": "tag",
|
||||
* "arms": { "NONE": null, "PALETTE": "palette",
|
||||
* "RGB": "rgb" } }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
|
||||
@@ -15,14 +15,18 @@ const Target = @import("target.zig").Target;
|
||||
/// 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.
|
||||
/// `options.padding` is a type that is always added to the C union with the key
|
||||
/// `_padding`. It should have the size and alignment needed to pad the C union
|
||||
/// to the expected size and should never change to ensure ABI compatibility.
|
||||
///
|
||||
/// Each tag has an optional field in `options.field_renames` that may rename its
|
||||
/// C value field. Multiple tags may map to the same field when their C value
|
||||
/// types are identical. Tags with no payload are omitted. This is used
|
||||
/// for metadata and should match the C headers directly.
|
||||
pub fn TaggedUnion(
|
||||
comptime target: Target,
|
||||
comptime Union: type,
|
||||
comptime Padding: type,
|
||||
comptime options: TaggedUnionOptions(Union),
|
||||
) type {
|
||||
return struct {
|
||||
comptime {
|
||||
@@ -30,7 +34,7 @@ pub fn TaggedUnion(
|
||||
.zig => {},
|
||||
|
||||
// For ABI compatibility, we expect that this is our union size.
|
||||
.c => if (@sizeOf(CValue) != @sizeOf(Padding)) {
|
||||
.c => if (@sizeOf(CValue) != @sizeOf(options.padding)) {
|
||||
@compileLog(@sizeOf(CValue));
|
||||
@compileError("TaggedUnion CValue size does not match expected fixed size");
|
||||
},
|
||||
@@ -49,11 +53,25 @@ pub fn TaggedUnion(
|
||||
.c => extern struct {
|
||||
tag: Tag,
|
||||
value: CValue,
|
||||
|
||||
/// Returns the public C value-union field name for `tag`, or
|
||||
/// null when the tag has no value field. This is metadata only;
|
||||
/// it does not rename fields in `CValue`.
|
||||
pub fn cFieldRename(comptime tag: Tag) ?[]const u8 {
|
||||
const tag_name = @tagName(tag);
|
||||
const value = @field(@unionInit(Union, tag_name, undefined), tag_name);
|
||||
|
||||
if (@field(options.field_renames, tag_name)) |name| return name;
|
||||
if (@sizeOf(@TypeOf(value)) == 0) return null;
|
||||
|
||||
return tag_name;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/// The C ABI compatible union value type.
|
||||
pub const CValue = cvalue: {
|
||||
@setEvalBranchQuota(10_000);
|
||||
switch (target) {
|
||||
.zig => break :cvalue extern struct {},
|
||||
.c => {},
|
||||
@@ -83,8 +101,8 @@ pub fn TaggedUnion(
|
||||
}
|
||||
|
||||
names[tag_fields.len] = "_padding";
|
||||
types[tag_fields.len] = Padding;
|
||||
attrs[tag_fields.len] = .{ .@"align" = @alignOf(Padding) };
|
||||
types[tag_fields.len] = options.padding;
|
||||
attrs[tag_fields.len] = .{ .@"align" = @alignOf(options.padding) };
|
||||
|
||||
break :cvalue @Union(.@"extern", null, &names, &types, &attrs);
|
||||
};
|
||||
@@ -125,6 +143,30 @@ pub fn TaggedUnion(
|
||||
};
|
||||
}
|
||||
|
||||
/// Options for generating the C representation of a tagged union.
|
||||
pub fn TaggedUnionOptions(comptime Union: type) type {
|
||||
const Tag = @typeInfo(Union).@"union".tag_type.?;
|
||||
const tag_fields = @typeInfo(Tag).@"enum".fields;
|
||||
const FieldRenames: type = field_renames: {
|
||||
const default_rename: ?[]const u8 = null;
|
||||
|
||||
var names: [tag_fields.len][]const u8 = undefined;
|
||||
var types: [tag_fields.len]type = undefined;
|
||||
var attrs: [tag_fields.len]std.builtin.Type.StructField.Attributes = undefined;
|
||||
|
||||
for (tag_fields, 0..) |field, i| {
|
||||
names[i] = field.name;
|
||||
types[i] = ?[]const u8;
|
||||
attrs[i] = .{ .default_value_ptr = &default_rename };
|
||||
}
|
||||
|
||||
break :field_renames @Struct(.auto, null, &names, &types, &attrs);
|
||||
};
|
||||
return struct {
|
||||
padding: type,
|
||||
field_renames: FieldRenames = .{},
|
||||
};
|
||||
}
|
||||
test "TaggedUnion: matching size" {
|
||||
const Tag = enum(c_int) { a, b };
|
||||
const U = TaggedUnion(
|
||||
@@ -133,7 +175,7 @@ test "TaggedUnion: matching size" {
|
||||
a: u32,
|
||||
b: u64,
|
||||
},
|
||||
u64,
|
||||
.{ .padding = u64 },
|
||||
);
|
||||
|
||||
try testing.expectEqual(8, @sizeOf(U.CValue));
|
||||
@@ -146,7 +188,7 @@ test "TaggedUnion: padded size" {
|
||||
union(Tag) {
|
||||
a: u32,
|
||||
},
|
||||
u64,
|
||||
.{ .padding = u64 },
|
||||
);
|
||||
|
||||
try testing.expectEqual(8, @sizeOf(U.CValue));
|
||||
@@ -157,9 +199,39 @@ test "TaggedUnion: c conversion" {
|
||||
const U = TaggedUnion(.c, union(Tag) {
|
||||
a: u32,
|
||||
b: u64,
|
||||
}, u64);
|
||||
}, .{ .padding = u64 });
|
||||
|
||||
const c = U.cval(.{ .a = 42 });
|
||||
try testing.expectEqual(Tag.a, c.tag);
|
||||
try testing.expectEqual(42, c.value.a);
|
||||
}
|
||||
|
||||
test "TaggedUnion: custom C value fields" {
|
||||
const Tag = enum(c_int) { a, b, none };
|
||||
const Union = union(Tag) {
|
||||
a: u32,
|
||||
b: u32,
|
||||
none,
|
||||
};
|
||||
const U = TaggedUnion(.c, Union, .{
|
||||
.padding = u64,
|
||||
.field_renames = .{
|
||||
.a = "value",
|
||||
.b = "value",
|
||||
},
|
||||
});
|
||||
|
||||
try testing.expect(!@hasField(U.CValue, "value"));
|
||||
try testing.expect(@hasField(U.CValue, "a"));
|
||||
try testing.expect(@hasField(U.CValue, "b"));
|
||||
try testing.expect(@hasField(U.CValue, "none"));
|
||||
try testing.expectEqualStrings("value", U.C.cFieldRename(.a).?);
|
||||
try testing.expect(U.C.cFieldRename(.none) == null);
|
||||
|
||||
const a = U.cval(.{ .a = 42 });
|
||||
try testing.expectEqual(Tag.a, a.tag);
|
||||
try testing.expectEqual(42, a.value.a);
|
||||
|
||||
const none = U.cval(.none);
|
||||
try testing.expectEqual(Tag.none, none.tag);
|
||||
}
|
||||
|
||||
@@ -2590,7 +2590,7 @@ pub const ScrollViewport = union(Tag) {
|
||||
@This(),
|
||||
// Padding: largest variant is isize (8 bytes on 64-bit).
|
||||
// Use [2]u64 (16 bytes) for future expansion.
|
||||
[2]u64,
|
||||
.{ .padding = [2]u64 },
|
||||
);
|
||||
pub const C = c_union.C;
|
||||
pub const CValue = c_union.CValue;
|
||||
|
||||
@@ -12,6 +12,9 @@ pub const Parser = ?*osc.Parser;
|
||||
/// C: GhosttyOscCommand
|
||||
pub const Command = ?*osc.Command;
|
||||
|
||||
/// C: GhosttyOscCommandType
|
||||
pub const CommandType = osc.Command.Key;
|
||||
|
||||
pub fn new(
|
||||
alloc_: ?*const CAllocator,
|
||||
result: *Parser,
|
||||
@@ -44,7 +47,7 @@ pub fn end(parser_: Parser, terminator: u8) callconv(lib.calling_conv) Command {
|
||||
return parser_.?.end(terminator);
|
||||
}
|
||||
|
||||
pub fn commandType(command_: Command) callconv(lib.calling_conv) osc.Command.Key {
|
||||
pub fn commandType(command_: Command) callconv(lib.calling_conv) CommandType {
|
||||
const command = command_ orelse return .invalid;
|
||||
return command.*;
|
||||
}
|
||||
|
||||
@@ -182,7 +182,7 @@ pub const UnknownSequence = union(Tag) {
|
||||
// A future borrowed CSI payload may need parameter, separator, and
|
||||
// intermediate arrays. Reserve 128 bytes so that representation and
|
||||
// other structured sequence types can be added without an ABI break.
|
||||
[16]u64,
|
||||
.{ .padding = [16]u64 },
|
||||
);
|
||||
pub const C = c_union.C;
|
||||
pub const CValue = c_union.CValue;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -71,7 +71,15 @@ pub const Point = union(Tag) {
|
||||
@This(),
|
||||
// Padding: largest variant is Coordinate (u16 + u32 = 6 bytes).
|
||||
// Use [2]u64 (16 bytes) for future expansion.
|
||||
[2]u64,
|
||||
.{
|
||||
.padding = [2]u64,
|
||||
.field_renames = .{
|
||||
.active = "coordinate",
|
||||
.viewport = "coordinate",
|
||||
.screen = "coordinate",
|
||||
.history = "coordinate",
|
||||
},
|
||||
},
|
||||
);
|
||||
pub const C = c_union.C;
|
||||
pub const CValue = c_union.CValue;
|
||||
|
||||
@@ -161,7 +161,18 @@ pub const Attribute = union(Tag) {
|
||||
// Largest variant is Unknown.C: 2 pointers + 2 usize = 32 bytes on 64-bit.
|
||||
// We use [8]u64 (64 bytes) to allow room for future expansion while
|
||||
// maintaining ABI compatibility.
|
||||
[8]u64,
|
||||
.{
|
||||
.padding = [8]u64,
|
||||
.field_renames = .{
|
||||
.@"256_underline_color" = "underline_color_256",
|
||||
.@"8_bg" = "bg_8",
|
||||
.@"8_fg" = "fg_8",
|
||||
.@"8_bright_bg" = "bright_bg_8",
|
||||
.@"8_bright_fg" = "bright_fg_8",
|
||||
.@"256_bg" = "bg_256",
|
||||
.@"256_fg" = "fg_256",
|
||||
},
|
||||
},
|
||||
);
|
||||
pub const Value = c_union.Value;
|
||||
pub const C = c_union.C;
|
||||
|
||||
@@ -236,7 +236,7 @@ pub const Action = union(Key) {
|
||||
@This(),
|
||||
// TODO: Before shipping an ABI-compatible libghostty, verify this.
|
||||
// This was just arbitrarily chosen for now.
|
||||
[16]u64,
|
||||
.{ .padding = [16]u64 },
|
||||
);
|
||||
pub const Tag = c_union.Tag;
|
||||
pub const Value = c_union.Value;
|
||||
|
||||
Reference in New Issue
Block a user