diff --git a/include/ghostty.h b/include/ghostty.h index 312e6595a..bcd88251b 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -932,6 +932,9 @@ bool ghostty_inspector_metal_shutdown(ghostty_inspector_t); // Don't use these unless you know what you're doing. void ghostty_set_window_background_blur(ghostty_app_t, void*); +// Benchmark API, if available. +bool ghostty_benchmark_cli(const char*, const char*); + #ifdef __cplusplus } #endif diff --git a/pkg/macos/os/signpost.zig b/pkg/macos/os/signpost.zig index d7201aa6c..0be6ad4b1 100644 --- a/pkg/macos/os/signpost.zig +++ b/pkg/macos/os/signpost.zig @@ -13,11 +13,24 @@ const Log = logpkg.Log; pub fn init() void { 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, // we populate it by looking up the main function's module address // which should be a mach-o header. var info: DlInfo = undefined; - const result = dladdr(@import("root").main, &info); + const result = dladdr(sym, &info); assert(result != 0); __dso_handle = @ptrCast(@alignCast(info.dli_fbase)); } diff --git a/src/benchmark/Benchmark.zig b/src/benchmark/Benchmark.zig index 523ea6f9a..4128a7adc 100644 --- a/src/benchmark/Benchmark.zig +++ b/src/benchmark/Benchmark.zig @@ -45,7 +45,7 @@ pub fn run( const signpost: if (builtin.target.os.tag.isDarwin()) struct { log: *macos.os.Log, 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(); const log = macos.os.Log.create( build_config.bundle_id, @@ -53,9 +53,9 @@ pub fn run( ); const id = macos.os.signpost.Id.forPointer(log, self.ptr); macos.os.signpost.intervalBegin(log, id, signpost_name); - break :macos .{ .log = log, .id = id }; + break :darwin .{ .log = log, .id = id }; } else {}; - defer if (comptime builtin.os.tag == .macos) { + defer if (comptime builtin.target.os.tag.isDarwin()) { macos.os.signpost.intervalEnd( signpost.log, signpost.id, diff --git a/src/benchmark/CApi.zig b/src/benchmark/CApi.zig new file mode 100644 index 000000000..3bef8b269 --- /dev/null +++ b/src/benchmark/CApi.zig @@ -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; +} diff --git a/src/benchmark/cli.zig b/src/benchmark/cli.zig index 781eafd24..c0b8dcea6 100644 --- a/src/benchmark/cli.zig +++ b/src/benchmark/cli.zig @@ -4,7 +4,7 @@ const cli = @import("../cli.zig"); /// The available actions for the CLI. This is the list of available /// benchmarks. -const Action = enum { +pub const Action = enum { @"terminal-stream", /// Returns the struct associated with the action. The struct @@ -24,31 +24,57 @@ const Action = enum { /// An entrypoint for the benchmark CLI. pub fn main() !void { - // TODO: Better terminal output throughout this, use libvaxis. - const alloc = std.heap.c_allocator; const action_ = try cli.action.detectArgs(Action, alloc); const action = action_ orelse return error.NoAction; - - // 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); - }, - }; + try mainAction(alloc, action, .cli); } -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. const Options = BenchmarkImpl.Options; var opts: Options = .{}; defer if (@hasDecl(Options, "deinit")) opts.deinit(); - { - var iter = try cli.args.argsIterator(alloc); - defer iter.deinit(); - try cli.args.parse(Options, alloc, &opts, &iter); + switch (args) { + .cli => { + var iter = try cli.args.argsIterator(alloc); + defer iter.deinit(); + 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 diff --git a/src/benchmark/main.zig b/src/benchmark/main.zig index 93e489578..010f11805 100644 --- a/src/benchmark/main.zig +++ b/src/benchmark/main.zig @@ -1,5 +1,6 @@ pub const cli = @import("cli.zig"); pub const Benchmark = @import("Benchmark.zig"); +pub const CApi = @import("CApi.zig"); pub const TerminalStream = @import("TerminalStream.zig"); test { diff --git a/src/config.zig b/src/config.zig index efc9fd973..c5bab5877 100644 --- a/src/config.zig +++ b/src/config.zig @@ -41,7 +41,7 @@ pub const BackgroundImageFit = Config.BackgroundImageFit; pub const LinkPreviews = Config.LinkPreviews; // 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"); test { diff --git a/src/config/CAPI.zig b/src/config/CApi.zig similarity index 100% rename from src/config/CAPI.zig rename to src/config/CApi.zig diff --git a/src/main_c.zig b/src/main_c.zig index 2c266cfb5..9a9bcc6d2 100644 --- a/src/main_c.zig +++ b/src/main_c.zig @@ -33,10 +33,16 @@ pub const std_options = main.std_options; comptime { // These structs need to be referenced so the `export` functions // are truly exported by the C API lib. - _ = @import("config.zig").CAPI; - if (@hasDecl(apprt.runtime, "CAPI")) { - _ = apprt.runtime.CAPI; - } + + // Our config API + _ = @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 @@ -72,7 +78,7 @@ pub const String = extern struct { }; /// 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); 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 /// 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; std.log.info("executing CLI action={}", .{action}); 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. -export fn ghostty_info() Info { +pub export fn ghostty_info() Info { return .{ .mode = switch (builtin.mode) { .Debug => .debug, @@ -117,11 +123,11 @@ export fn ghostty_info() Info { /// the function call. /// /// 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); } /// 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]); }