mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-06 07:38:21 +00:00
vt: add ghostty_type_json for struct layout metadata
Add a new C API function that returns a comptime-generated JSON string describing the size, alignment, and field layout of every C API extern struct. This lets FFI consumers (particularly WASM) construct structs by byte offset without hardcoding platform-specific layout. The JSON is built at comptime using std.json.Stringify via a StructInfo type that holds per-struct metadata and implements jsonStringify. A StaticStringMap keyed by C struct name provides lookup by name as well as iteration for the JSON serialization. The function is declared in types.h alongside the other common types and exported as ghostty_type_json.
This commit is contained in:
@@ -83,4 +83,42 @@ typedef struct {
|
||||
#define GHOSTTY_INIT_SIZED(type) \
|
||||
((type){ .size = sizeof(type) })
|
||||
|
||||
/**
|
||||
* Return a pointer to a JSON string describing the layout of every
|
||||
* C API struct 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.
|
||||
*
|
||||
* 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" }
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* @endcode
|
||||
*
|
||||
* The returned pointer is valid for the lifetime of the process.
|
||||
* The length of the string (excluding any null terminator) is
|
||||
* written to @p len.
|
||||
*
|
||||
* @param[out] len Receives the length of the returned string in bytes.
|
||||
* @return Pointer to the JSON string.
|
||||
*/
|
||||
const char *ghostty_type_json(size_t *len);
|
||||
|
||||
#endif /* GHOSTTY_VT_TYPES_H */
|
||||
|
||||
@@ -219,6 +219,7 @@ comptime {
|
||||
@export(&c.grid_ref_graphemes, .{ .name = "ghostty_grid_ref_graphemes" });
|
||||
@export(&c.grid_ref_style, .{ .name = "ghostty_grid_ref_style" });
|
||||
@export(&c.build_info, .{ .name = "ghostty_build_info" });
|
||||
@export(&c.type_json, .{ .name = "ghostty_type_json" });
|
||||
@export(&c.alloc_alloc, .{ .name = "ghostty_alloc" });
|
||||
@export(&c.alloc_free, .{ .name = "ghostty_free" });
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ pub const cell = @import("cell.zig");
|
||||
pub const color = @import("color.zig");
|
||||
pub const focus = @import("focus.zig");
|
||||
pub const formatter = @import("formatter.zig");
|
||||
pub const grid_ref = @import("grid_ref.zig");
|
||||
pub const types = @import("types.zig");
|
||||
pub const modes = @import("modes.zig");
|
||||
pub const osc = @import("osc.zig");
|
||||
pub const render = @import("render.zig");
|
||||
@@ -141,7 +143,8 @@ pub const terminal_mode_set = terminal.mode_set;
|
||||
pub const terminal_get = terminal.get;
|
||||
pub const terminal_grid_ref = terminal.grid_ref;
|
||||
|
||||
const grid_ref = @import("grid_ref.zig");
|
||||
pub const type_json = types.get_json;
|
||||
|
||||
pub const grid_ref_cell = grid_ref.grid_ref_cell;
|
||||
pub const grid_ref_row = grid_ref.grid_ref_row;
|
||||
pub const grid_ref_graphemes = grid_ref.grid_ref_graphemes;
|
||||
@@ -168,6 +171,7 @@ test {
|
||||
_ = size_report;
|
||||
_ = style;
|
||||
_ = terminal;
|
||||
_ = types;
|
||||
|
||||
// We want to make sure we run the tests for the C allocator interface.
|
||||
_ = @import("../../lib/allocator.zig");
|
||||
|
||||
212
src/terminal/c/types.zig
Normal file
212
src/terminal/c/types.zig
Normal file
@@ -0,0 +1,212 @@
|
||||
//! Comptime-generated metadata describing the layout of all C API
|
||||
//! extern structs for the current target.
|
||||
//!
|
||||
//! This is embedded in the binary as a const string and exposed via
|
||||
//! `ghostty_struct_meta` so that WASM (and other FFI) consumers can
|
||||
//! build structs without hardcoding byte offsets.
|
||||
const std = @import("std");
|
||||
const lib = @import("../lib.zig");
|
||||
|
||||
const terminal = @import("terminal.zig");
|
||||
const formatter = @import("formatter.zig");
|
||||
const render = @import("render.zig");
|
||||
const style_c = @import("style.zig");
|
||||
const mouse_encode = @import("mouse_encode.zig");
|
||||
const grid_ref = @import("grid_ref.zig");
|
||||
|
||||
/// All C API structs and their Ghostty C names.
|
||||
pub const structs: std.StaticStringMap(StructInfo) = .initComptime(.{
|
||||
.{ "GhosttyTerminalOptions", StructInfo.init(terminal.Options) },
|
||||
.{ "GhosttyFormatterTerminalOptions", StructInfo.init(formatter.TerminalOptions) },
|
||||
.{ "GhosttyFormatterTerminalExtra", StructInfo.init(formatter.TerminalOptions.Extra) },
|
||||
.{ "GhosttyFormatterScreenExtra", StructInfo.init(formatter.ScreenOptions.Extra) },
|
||||
.{ "GhosttyRenderStateColors", StructInfo.init(render.Colors) },
|
||||
.{ "GhosttyStyle", StructInfo.init(style_c.Style) },
|
||||
.{ "GhosttyStyleColor", StructInfo.init(style_c.Color) },
|
||||
.{ "GhosttyMouseEncoderSize", StructInfo.init(mouse_encode.Size) },
|
||||
.{ "GhosttyGridRef", StructInfo.init(grid_ref.CGridRef) },
|
||||
});
|
||||
|
||||
/// The comptime-generated JSON string of all structs.
|
||||
pub const json: []const u8 = json: {
|
||||
@setEvalBranchQuota(50000);
|
||||
var counter: std.Io.Writer.Discarding = .init(&.{});
|
||||
jsonWriteAll(&counter.writer) catch unreachable;
|
||||
|
||||
var buf: [counter.count]u8 = undefined;
|
||||
var writer: std.Io.Writer = .fixed(&buf);
|
||||
jsonWriteAll(&writer) catch unreachable;
|
||||
const final = buf;
|
||||
break :json final[0..writer.end];
|
||||
};
|
||||
|
||||
/// Returns a pointer to the comptime-generated JSON string describing
|
||||
/// the layout of all C API extern structs, and writes its length to `len`.
|
||||
/// Exported as `ghostty_type_json` for FFI consumers.
|
||||
pub fn get_json(len: *usize) callconv(lib.calling_conv) [*]const u8 {
|
||||
len.* = json.len;
|
||||
return json.ptr;
|
||||
}
|
||||
|
||||
/// Meta information about a struct that we expose to ease writing
|
||||
/// bindings in some languages, particularly WASM where we can't
|
||||
/// easily share struct definitions and need to hardcode byte offsets.
|
||||
pub const StructInfo = struct {
|
||||
name: []const u8,
|
||||
size: usize,
|
||||
@"align": usize,
|
||||
fields: []const FieldInfo,
|
||||
|
||||
pub const FieldInfo = struct {
|
||||
name: []const u8,
|
||||
offset: usize,
|
||||
size: usize,
|
||||
type: []const u8,
|
||||
};
|
||||
|
||||
pub fn init(comptime T: type) StructInfo {
|
||||
comptime {
|
||||
const fields = @typeInfo(T).@"struct".fields;
|
||||
const field_infos: [fields.len]FieldInfo = blk: {
|
||||
var infos: [fields.len]FieldInfo = undefined;
|
||||
for (fields, 0..) |field, i| infos[i] = .{
|
||||
.name = field.name,
|
||||
.offset = @offsetOf(T, field.name),
|
||||
.size = @sizeOf(field.type),
|
||||
.type = typeName(field.type),
|
||||
};
|
||||
break :blk infos;
|
||||
};
|
||||
|
||||
return .{
|
||||
.name = @typeName(T),
|
||||
.size = @sizeOf(T),
|
||||
.@"align" = @alignOf(T),
|
||||
.fields = &field_infos,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn jsonStringify(
|
||||
self: *const StructInfo,
|
||||
jws: anytype,
|
||||
) std.Io.Writer.Error!void {
|
||||
try jws.beginObject();
|
||||
try jws.objectField("size");
|
||||
try jws.write(self.size);
|
||||
try jws.objectField("align");
|
||||
try jws.write(self.@"align");
|
||||
try jws.objectField("fields");
|
||||
try jws.beginObject();
|
||||
for (self.fields) |field| {
|
||||
try jws.objectField(field.name);
|
||||
try jws.beginObject();
|
||||
try jws.objectField("offset");
|
||||
try jws.write(field.offset);
|
||||
try jws.objectField("size");
|
||||
try jws.write(field.size);
|
||||
try jws.objectField("type");
|
||||
try jws.write(field.type);
|
||||
try jws.endObject();
|
||||
}
|
||||
try jws.endObject();
|
||||
try jws.endObject();
|
||||
}
|
||||
};
|
||||
|
||||
fn jsonWriteAll(writer: *std.Io.Writer) std.Io.Writer.Error!void {
|
||||
var jws: std.json.Stringify = .{ .writer = writer };
|
||||
try jws.beginObject();
|
||||
for (structs.keys(), structs.values()) |name, *info| {
|
||||
try jws.objectField(name);
|
||||
try info.jsonStringify(&jws);
|
||||
}
|
||||
try jws.endObject();
|
||||
}
|
||||
|
||||
fn typeName(comptime T: type) []const u8 {
|
||||
return switch (@typeInfo(T)) {
|
||||
.bool => "bool",
|
||||
.int => |info| switch (info.signedness) {
|
||||
.signed => switch (info.bits) {
|
||||
8 => "i8",
|
||||
16 => "i16",
|
||||
32 => "i32",
|
||||
64 => "i64",
|
||||
else => @compileError("unsupported signed int size"),
|
||||
},
|
||||
.unsigned => switch (info.bits) {
|
||||
8 => "u8",
|
||||
16 => "u16",
|
||||
32 => "u32",
|
||||
64 => "u64",
|
||||
else => @compileError("unsupported unsigned int size"),
|
||||
},
|
||||
},
|
||||
.@"enum" => "enum",
|
||||
.@"struct" => "struct",
|
||||
.pointer => "pointer",
|
||||
.array => "array",
|
||||
else => "opaque",
|
||||
};
|
||||
}
|
||||
|
||||
test "json parses" {
|
||||
const parsed = try std.json.parseFromSlice(
|
||||
std.json.Value,
|
||||
std.testing.allocator,
|
||||
json,
|
||||
.{},
|
||||
);
|
||||
defer parsed.deinit();
|
||||
|
||||
const root = parsed.value.object;
|
||||
|
||||
// Verify we have all expected structs
|
||||
try std.testing.expect(root.contains("GhosttyTerminalOptions"));
|
||||
try std.testing.expect(root.contains("GhosttyFormatterTerminalOptions"));
|
||||
|
||||
// Verify GhosttyTerminalOptions fields
|
||||
const term_opts = root.get("GhosttyTerminalOptions").?.object;
|
||||
try std.testing.expect(term_opts.contains("size"));
|
||||
try std.testing.expect(term_opts.contains("align"));
|
||||
try std.testing.expect(term_opts.contains("fields"));
|
||||
|
||||
const fields = term_opts.get("fields").?.object;
|
||||
try std.testing.expect(fields.contains("cols"));
|
||||
try std.testing.expect(fields.contains("rows"));
|
||||
try std.testing.expect(fields.contains("max_scrollback"));
|
||||
|
||||
// Verify field offsets make sense (cols should be at 0)
|
||||
const cols = fields.get("cols").?.object;
|
||||
try std.testing.expectEqual(0, cols.get("offset").?.integer);
|
||||
}
|
||||
|
||||
test "struct sizes are non-zero" {
|
||||
const parsed = try std.json.parseFromSlice(
|
||||
std.json.Value,
|
||||
std.testing.allocator,
|
||||
json,
|
||||
.{},
|
||||
);
|
||||
defer parsed.deinit();
|
||||
|
||||
var it = parsed.value.object.iterator();
|
||||
while (it.next()) |entry| {
|
||||
const struct_info = entry.value_ptr.object;
|
||||
const size = struct_info.get("size").?.integer;
|
||||
try std.testing.expect(size > 0);
|
||||
}
|
||||
}
|
||||
|
||||
test "structs map has all entries" {
|
||||
try std.testing.expect(structs.get("GhosttyTerminalOptions") != null);
|
||||
try std.testing.expect(structs.get("GhosttyFormatterTerminalOptions") != null);
|
||||
try std.testing.expect(structs.get("GhosttyFormatterTerminalExtra") != null);
|
||||
try std.testing.expect(structs.get("GhosttyFormatterScreenExtra") != null);
|
||||
try std.testing.expect(structs.get("GhosttyRenderStateColors") != null);
|
||||
try std.testing.expect(structs.get("GhosttyStyle") != null);
|
||||
try std.testing.expect(structs.get("GhosttyStyleColor") != null);
|
||||
try std.testing.expect(structs.get("GhosttyMouseEncoderSize") != null);
|
||||
try std.testing.expect(structs.get("GhosttyGridRef") != null);
|
||||
}
|
||||
Reference in New Issue
Block a user