libghostty: minor C/C++ compatibility fixes (#13801)

Minor things as I was just auditing the state of our headers:

* Make sure all subheaders like `point.h` can be included standalone
* Support Clang/GCC extensions for typed enums if we can detect it
* Add missing structs to the `ghostty_type_json` function
* Fix `GHOSTTY_INIT_SIZED` for C++ mode
This commit is contained in:
Mitchell Hashimoto
2026-08-13 12:56:32 -07:00
committed by GitHub
4 changed files with 104 additions and 20 deletions

View File

@@ -11,6 +11,8 @@
#include <stddef.h>
#include <stdint.h>
#include <ghostty/vt/types.h>
/* DA1 conformance levels (Pp parameter). */
#define GHOSTTY_DA_CONFORMANCE_VT100 1
#define GHOSTTY_DA_CONFORMANCE_VT101 1

View File

@@ -9,6 +9,8 @@
#include <stdint.h>
#include <ghostty/vt/types.h>
#ifdef __cplusplus
extern "C" {
#endif

View File

@@ -39,18 +39,21 @@
* The Zig side backs all C enums with c_int, so the C declarations
* must use int as their underlying type to maintain ABI compatibility.
*
* C23 (detected via __STDC_VERSION__ >= 202311L) supports explicit
* enum underlying types with `enum : int { ... }`. For pre-C23
* compilers, which are free to choose any type that can represent
* all values (C11 §6.7.2.2), we add an INT_MAX sentinel as the last
* entry to force the compiler to use int.
* C++11 and C23 support explicit enum underlying types with
* `enum : int { ... }`. Clang and GCC 13+ also support this syntax as
* an extension in older C language modes, so use it when available.
*
* Other pre-C23 C compilers are free to choose any type that can
* represent all values (C11 §6.7.2.2). For those compilers, we add an
* INT_MAX sentinel as the last entry so the compatible type must be
* able to represent INT_MAX. The exact compatible type and its
* signedness remain implementation-defined in this fallback.
*
* INT_MAX is used rather than a fixed constant like 0xFFFFFFFF
* because enum constants must have type int (which is signed).
* Values above INT_MAX overflow signed int and are a constraint
* violation in standard C; compilers that accept them interpret them
* as negative values via two's complement, which can collide with
* legitimate negative enum values.
* because enum constants must have type int in pre-C23 C. Values above
* INT_MAX are a constraint violation there; compilers that accept them
* may interpret them as negative values via two's complement, which can
* collide with legitimate negative enum values.
*
* Usage:
* @code
@@ -61,7 +64,18 @@
* } Foo;
* @endcode
*/
#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L
#if defined(__cplusplus) && \
(__cplusplus >= 201103L || (defined(_MSC_VER) && _MSC_VER >= 1700))
#define GHOSTTY_ENUM_TYPED : int
#elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 202311L
#define GHOSTTY_ENUM_TYPED : int
#elif defined(__clang__)
#if __has_extension(c_fixed_enum)
#define GHOSTTY_ENUM_TYPED : int
#else
#define GHOSTTY_ENUM_TYPED
#endif
#elif defined(__GNUC__) && __GNUC__ >= 13
#define GHOSTTY_ENUM_TYPED : int
#else
#define GHOSTTY_ENUM_TYPED
@@ -305,8 +319,17 @@ typedef struct {
* opts.trim = true;
* @endcode
*/
#ifdef __cplusplus
#define GHOSTTY_INIT_SIZED(type) \
([]() noexcept { \
type value{}; \
value.size = sizeof(value); \
return value; \
}())
#else
#define GHOSTTY_INIT_SIZED(type) \
((type){ .size = sizeof(type) })
#endif
/**
* Return a pointer to a null-terminated JSON string describing the

View File

@@ -7,6 +7,7 @@
const std = @import("std");
const lib = @import("../lib.zig");
const color = @import("../color.zig");
const sgr = @import("../sgr.zig");
const color_c = @import("color.zig");
const mouse_event = @import("mouse_event.zig");
const point = @import("../point.zig");
@@ -18,9 +19,11 @@ const selection = @import("selection.zig");
const selection_gesture = @import("selection_gesture.zig");
const render = @import("render.zig");
const style_c = @import("style.zig");
const kitty_graphics = @import("kitty_graphics.zig");
const mouse_encode = @import("mouse_encode.zig");
const grid_ref = @import("grid_ref.zig");
const io = @import("io.zig");
const sys = @import("sys.zig");
/// C: GhosttySurfacePosition
pub const SurfacePosition = extern struct {
@@ -38,6 +41,8 @@ pub const Codepoints = extern struct {
pub const structs: std.StaticStringMap(StructInfo) = structs: {
@setEvalBranchQuota(10_000);
break :structs .initComptime(.{
.{ "GhosttyAllocator", StructInfo.init(lib.alloc.Allocator) },
.{ "GhosttyAllocatorVtable", StructInfo.init(lib.alloc.VTable) },
.{ "GhosttyBuffer", StructInfo.init(lib.Buffer) },
.{ "GhosttyClipboardContent", StructInfo.init(terminal.ClipboardContent) },
.{ "GhosttyClipboardWrite", StructInfo.init(terminal.ClipboardWrite) },
@@ -54,22 +59,28 @@ pub const structs: std.StaticStringMap(StructInfo) = structs: {
.{ "GhosttyTerminalSelectWordOptions", StructInfo.init(selection.SelectWordOptions) },
.{ "GhosttyTerminalSelectWordBetweenOptions", StructInfo.init(selection.SelectWordBetweenOptions) },
.{ "GhosttyTerminalSelectLineOptions", StructInfo.init(selection.SelectLineOptions) },
.{ "GhosttyTerminalSelectionFormatOptions", StructInfo.init(selection.FormatOptions) },
.{ "GhosttyFormatterTerminalExtra", StructInfo.init(formatter.TerminalOptions.Extra) },
.{ "GhosttyFormatterScreenExtra", StructInfo.init(formatter.ScreenOptions.Extra) },
.{ "GhosttyGridRef", StructInfo.init(grid_ref.CGridRef) },
.{ "GhosttyKittyGraphicsPlacementRenderInfo", StructInfo.init(kitty_graphics.PlacementRenderInfo) },
.{ "GhosttyMouseEncoderSize", StructInfo.init(mouse_encode.Size) },
.{ "GhosttyMousePosition", StructInfo.init(mouse_event.Position) },
.{ "GhosttyPoint", StructInfo.init(point.Point.C) },
.{ "GhosttyPointCoordinate", StructInfo.init(point.Coordinate) },
.{ "GhosttyReader", StructInfo.init(io.Reader) },
.{ "GhosttyRenderStateColors", StructInfo.init(render.Colors) },
.{ "GhosttyRenderStateRowSelection", StructInfo.init(render.RowSelection) },
.{ "GhosttySelectionGestureBehaviors", StructInfo.init(selection_gesture.Behaviors) },
.{ "GhosttySelectionGestureGeometry", StructInfo.init(selection_gesture.Geometry) },
.{ "GhosttySgrAttribute", StructInfo.init(sgr.Attribute.C) },
.{ "GhosttySgrUnknown", StructInfo.init(sgr.Attribute.Unknown.C) },
.{ "GhosttySizeReportSize", StructInfo.init(size_report.Size) },
.{ "GhosttyString", StructInfo.init(lib.String) },
.{ "GhosttySurfacePosition", StructInfo.init(SurfacePosition) },
.{ "GhosttyStyle", StructInfo.init(style_c.Style) },
.{ "GhosttyStyleColor", StructInfo.init(style_c.Color) },
.{ "GhosttySysImage", StructInfo.init(sys.Image) },
.{ "GhosttyTerminalDesktopNotification", StructInfo.init(terminal.DesktopNotification) },
.{ "GhosttyTerminalModeConfig", StructInfo.init(terminal.ModeConfig) },
.{ "GhosttyTerminalProgressReport", StructInfo.init(terminal.ProgressReport) },
@@ -220,15 +231,61 @@ test "json parses" {
const root = parsed.value.object;
// Verify we have all expected structs
try std.testing.expect(root.contains("GhosttyClipboardContent"));
try std.testing.expect(root.contains("GhosttyClipboardWrite"));
try std.testing.expect(root.contains("GhosttyFormatterTerminalOptions"));
try std.testing.expect(root.contains("GhosttyTerminalModeConfig"));
try std.testing.expect(root.contains("GhosttyTerminalUnknownSequence"));
try std.testing.expect(root.contains("GhosttyTerminalUnknownStringSequence"));
try std.testing.expect(root.contains("GhosttyReader"));
try std.testing.expect(root.contains("GhosttyWriter"));
// Verify we have every public struct declared by the C API headers.
const expected_structs = [_][]const u8{
"GhosttyAllocator",
"GhosttyAllocatorVtable",
"GhosttyBuffer",
"GhosttyClipboardContent",
"GhosttyClipboardWrite",
"GhosttyCodepoints",
"GhosttyColorPaletteMask",
"GhosttyColorRgb",
"GhosttyColorX11Entry",
"GhosttyDeviceAttributes",
"GhosttyDeviceAttributesPrimary",
"GhosttyDeviceAttributesSecondary",
"GhosttyDeviceAttributesTertiary",
"GhosttyFormatterScreenExtra",
"GhosttyFormatterTerminalExtra",
"GhosttyFormatterTerminalOptions",
"GhosttyGridRef",
"GhosttyKittyGraphicsPlacementRenderInfo",
"GhosttyMouseEncoderSize",
"GhosttyMousePosition",
"GhosttyPoint",
"GhosttyPointCoordinate",
"GhosttyReader",
"GhosttyRenderStateColors",
"GhosttyRenderStateRowSelection",
"GhosttySelection",
"GhosttySelectionGestureBehaviors",
"GhosttySelectionGestureGeometry",
"GhosttySgrAttribute",
"GhosttySgrUnknown",
"GhosttySizeReportSize",
"GhosttyString",
"GhosttyStyle",
"GhosttyStyleColor",
"GhosttySurfacePosition",
"GhosttySysImage",
"GhosttyTerminalDesktopNotification",
"GhosttyTerminalModeConfig",
"GhosttyTerminalProgressReport",
"GhosttyTerminalScrollbar",
"GhosttyTerminalScrollViewport",
"GhosttyTerminalSelectLineOptions",
"GhosttyTerminalSelectWordBetweenOptions",
"GhosttyTerminalSelectWordOptions",
"GhosttyTerminalSelectionFormatOptions",
"GhosttyTerminalUnknownSequence",
"GhosttyTerminalUnknownStringSequence",
"GhosttyWriter",
};
try std.testing.expectEqual(expected_structs.len, root.count());
for (expected_structs) |name| {
try std.testing.expect(root.contains(name));
}
const clipboard_content = root.get("GhosttyClipboardContent").?.object;
const clipboard_content_fields = clipboard_content.get("fields").?.object;