libghostty: export benchmark CLI API

This commit is contained in:
Mitchell Hashimoto
2025-07-09 11:33:00 -07:00
parent 01b2545d1d
commit 20bb71c627
9 changed files with 114 additions and 31 deletions

View File

@ -932,6 +932,9 @@ bool ghostty_inspector_metal_shutdown(ghostty_inspector_t);
// Don't use these unless you know what you're doing. // Don't use these unless you know what you're doing.
void ghostty_set_window_background_blur(ghostty_app_t, void*); void ghostty_set_window_background_blur(ghostty_app_t, void*);
// Benchmark API, if available.
bool ghostty_benchmark_cli(const char*, const char*);
#ifdef __cplusplus #ifdef __cplusplus
} }
#endif #endif

View File

@ -13,11 +13,24 @@ const Log = logpkg.Log;
pub fn init() void { pub fn init() void {
if (__dso_handle != null) return; if (__dso_handle != null) return;
const root = @import("root");
const sym = if (@hasDecl(root, "main"))
root.main
else
comptime first: {
for (@typeInfo(root).@"struct".decls) |decl_info| {
const decl = @field(root, decl_info.name);
if (@typeInfo(@TypeOf(decl)) == .@"fn") break :first decl;
}
@compileError("No functions found in root module");
};
// Since __dso_handle is not automatically populated by the linker, // Since __dso_handle is not automatically populated by the linker,
// we populate it by looking up the main function's module address // we populate it by looking up the main function's module address
// which should be a mach-o header. // which should be a mach-o header.
var info: DlInfo = undefined; var info: DlInfo = undefined;
const result = dladdr(@import("root").main, &info); const result = dladdr(sym, &info);
assert(result != 0); assert(result != 0);
__dso_handle = @ptrCast(@alignCast(info.dli_fbase)); __dso_handle = @ptrCast(@alignCast(info.dli_fbase));
} }

View File

@ -45,7 +45,7 @@ pub fn run(
const signpost: if (builtin.target.os.tag.isDarwin()) struct { const signpost: if (builtin.target.os.tag.isDarwin()) struct {
log: *macos.os.Log, log: *macos.os.Log,
id: macos.os.signpost.Id, id: macos.os.signpost.Id,
} else void = if (comptime builtin.os.tag == .macos) macos: { } else void = if (builtin.target.os.tag.isDarwin()) darwin: {
macos.os.signpost.init(); macos.os.signpost.init();
const log = macos.os.Log.create( const log = macos.os.Log.create(
build_config.bundle_id, build_config.bundle_id,
@ -53,9 +53,9 @@ pub fn run(
); );
const id = macos.os.signpost.Id.forPointer(log, self.ptr); const id = macos.os.signpost.Id.forPointer(log, self.ptr);
macos.os.signpost.intervalBegin(log, id, signpost_name); macos.os.signpost.intervalBegin(log, id, signpost_name);
break :macos .{ .log = log, .id = id }; break :darwin .{ .log = log, .id = id };
} else {}; } else {};
defer if (comptime builtin.os.tag == .macos) { defer if (comptime builtin.target.os.tag.isDarwin()) {
macos.os.signpost.intervalEnd( macos.os.signpost.intervalEnd(
signpost.log, signpost.log,
signpost.id, signpost.id,

34
src/benchmark/CApi.zig Normal file
View File

@ -0,0 +1,34 @@
const std = @import("std");
const cli = @import("cli.zig");
const state = &@import("../global.zig").state;
const log = std.log.scoped(.benchmark);
/// Run the Ghostty benchmark CLI with the given action and arguments.
export fn ghostty_benchmark_cli(
action_name_: [*:0]const u8,
args: [*:0]const u8,
) bool {
const action_name = std.mem.sliceTo(action_name_, 0);
const action: cli.Action = std.meta.stringToEnum(
cli.Action,
action_name,
) orelse {
log.warn("unknown action={s}", .{action_name});
return false;
};
cli.mainAction(
state.alloc,
action,
.{ .string = std.mem.sliceTo(args, 0) },
) catch |err| {
log.warn("failed to run action={s} err={}", .{
@tagName(action),
err,
});
return false;
};
return true;
}

View File

@ -4,7 +4,7 @@ const cli = @import("../cli.zig");
/// The available actions for the CLI. This is the list of available /// The available actions for the CLI. This is the list of available
/// benchmarks. /// benchmarks.
const Action = enum { pub const Action = enum {
@"terminal-stream", @"terminal-stream",
/// Returns the struct associated with the action. The struct /// Returns the struct associated with the action. The struct
@ -24,31 +24,57 @@ const Action = enum {
/// An entrypoint for the benchmark CLI. /// An entrypoint for the benchmark CLI.
pub fn main() !void { pub fn main() !void {
// TODO: Better terminal output throughout this, use libvaxis.
const alloc = std.heap.c_allocator; const alloc = std.heap.c_allocator;
const action_ = try cli.action.detectArgs(Action, alloc); const action_ = try cli.action.detectArgs(Action, alloc);
const action = action_ orelse return error.NoAction; const action = action_ orelse return error.NoAction;
try mainAction(alloc, action, .cli);
// We need a comptime action to get the struct type and do the
// rest.
return switch (action) {
inline else => |comptime_action| {
const BenchmarkImpl = Action.Struct(comptime_action);
try mainAction(BenchmarkImpl, alloc);
},
};
} }
fn mainAction(comptime BenchmarkImpl: type, alloc: Allocator) !void { /// Arguments that can be passed to the benchmark.
pub const Args = union(enum) {
/// The arguments passed to the CLI via argc/argv.
cli,
/// Simple string arguments, parsed via std.process.ArgIteratorGeneral.
string: []const u8,
};
pub fn mainAction(
alloc: Allocator,
action: Action,
args: Args,
) !void {
switch (action) {
inline else => |comptime_action| {
const BenchmarkImpl = Action.Struct(comptime_action);
try mainActionImpl(BenchmarkImpl, alloc, args);
},
}
}
fn mainActionImpl(
comptime BenchmarkImpl: type,
alloc: Allocator,
args: Args,
) !void {
// First, parse our CLI options. // First, parse our CLI options.
const Options = BenchmarkImpl.Options; const Options = BenchmarkImpl.Options;
var opts: Options = .{}; var opts: Options = .{};
defer if (@hasDecl(Options, "deinit")) opts.deinit(); defer if (@hasDecl(Options, "deinit")) opts.deinit();
{ switch (args) {
.cli => {
var iter = try cli.args.argsIterator(alloc); var iter = try cli.args.argsIterator(alloc);
defer iter.deinit(); defer iter.deinit();
try cli.args.parse(Options, alloc, &opts, &iter); try cli.args.parse(Options, alloc, &opts, &iter);
},
.string => |str| {
var iter = try std.process.ArgIteratorGeneral(.{}).init(
alloc,
str,
);
defer iter.deinit();
try cli.args.parse(Options, alloc, &opts, &iter);
},
} }
// Create our implementation // Create our implementation

View File

@ -1,5 +1,6 @@
pub const cli = @import("cli.zig"); pub const cli = @import("cli.zig");
pub const Benchmark = @import("Benchmark.zig"); pub const Benchmark = @import("Benchmark.zig");
pub const CApi = @import("CApi.zig");
pub const TerminalStream = @import("TerminalStream.zig"); pub const TerminalStream = @import("TerminalStream.zig");
test { test {

View File

@ -41,7 +41,7 @@ pub const BackgroundImageFit = Config.BackgroundImageFit;
pub const LinkPreviews = Config.LinkPreviews; pub const LinkPreviews = Config.LinkPreviews;
// Alternate APIs // Alternate APIs
pub const CAPI = @import("config/CAPI.zig"); pub const CApi = @import("config/CApi.zig");
pub const Wasm = if (!builtin.target.cpu.arch.isWasm()) struct {} else @import("config/Wasm.zig"); pub const Wasm = if (!builtin.target.cpu.arch.isWasm()) struct {} else @import("config/Wasm.zig");
test { test {

View File

@ -33,10 +33,16 @@ pub const std_options = main.std_options;
comptime { comptime {
// These structs need to be referenced so the `export` functions // These structs need to be referenced so the `export` functions
// are truly exported by the C API lib. // are truly exported by the C API lib.
_ = @import("config.zig").CAPI;
if (@hasDecl(apprt.runtime, "CAPI")) { // Our config API
_ = apprt.runtime.CAPI; _ = @import("config.zig").CApi;
}
// Any apprt-specific C API, mainly libghostty for apprt.embedded.
if (@hasDecl(apprt.runtime, "CAPI")) _ = apprt.runtime.CAPI;
// Our benchmark API. We probably want to gate this on a build
// config in the future but for now we always just export it.
_ = @import("benchmark/main.zig").CApi;
} }
/// ghostty_info_s /// ghostty_info_s
@ -72,7 +78,7 @@ pub const String = extern struct {
}; };
/// Initialize ghostty global state. /// Initialize ghostty global state.
export fn ghostty_init(argc: usize, argv: [*][*:0]u8) c_int { pub export fn ghostty_init(argc: usize, argv: [*][*:0]u8) c_int {
assert(builtin.link_libc); assert(builtin.link_libc);
std.os.argv = argv[0..argc]; std.os.argv = argv[0..argc];
@ -86,7 +92,7 @@ export fn ghostty_init(argc: usize, argv: [*][*:0]u8) c_int {
/// Runs an action if it is specified. If there is no action this returns /// Runs an action if it is specified. If there is no action this returns
/// false. If there is an action then this doesn't return. /// false. If there is an action then this doesn't return.
export fn ghostty_cli_try_action() void { pub export fn ghostty_cli_try_action() void {
const action = state.action orelse return; const action = state.action orelse return;
std.log.info("executing CLI action={}", .{action}); std.log.info("executing CLI action={}", .{action});
posix.exit(action.run(state.alloc) catch |err| { posix.exit(action.run(state.alloc) catch |err| {
@ -98,7 +104,7 @@ export fn ghostty_cli_try_action() void {
} }
/// Return metadata about Ghostty, such as version, build mode, etc. /// Return metadata about Ghostty, such as version, build mode, etc.
export fn ghostty_info() Info { pub export fn ghostty_info() Info {
return .{ return .{
.mode = switch (builtin.mode) { .mode = switch (builtin.mode) {
.Debug => .debug, .Debug => .debug,
@ -117,11 +123,11 @@ export fn ghostty_info() Info {
/// the function call. /// the function call.
/// ///
/// This should only be used for singular strings maintained by Ghostty. /// This should only be used for singular strings maintained by Ghostty.
export fn ghostty_translate(msgid: [*:0]const u8) [*:0]const u8 { pub export fn ghostty_translate(msgid: [*:0]const u8) [*:0]const u8 {
return internal_os.i18n._(msgid); return internal_os.i18n._(msgid);
} }
/// Free a string allocated by Ghostty. /// Free a string allocated by Ghostty.
export fn ghostty_string_free(str: String) void { pub export fn ghostty_string_free(str: String) void {
state.alloc.free(str.ptr.?[0..str.len]); state.alloc.free(str.ptr.?[0..str.len]);
} }