mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-08-24 16:11:43 +00:00
libghostty: skip stack traces in release panic handlers
The default Zig panic handler unwinds the stack and symbolicates it, which drags in ~160KB worth of helper machinery. For an embedded library this isn't great because the embedder's environment should be providing this as long as libghostty is compiled with symbols or has a way to symbolize. Change ReleaseFast/ReleaseSmall libghostty-vt builds to use a custom panic handler. Debug/ReleaseSafe keep the full Zig handlers. This shrinks libghostty-vt on macOS by ~160KB (~9%).
This commit is contained in:
@@ -11,6 +11,7 @@ const lib = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const stderr = @import("os/stderr.zig");
|
||||
|
||||
// The public API below reproduces a lot of terminal/main.zig but
|
||||
// is separate because (1) we need our root file to be in `src/`
|
||||
@@ -414,6 +415,53 @@ pub const std_options: std.Options = opts: {
|
||||
break :opts options;
|
||||
};
|
||||
|
||||
/// The panic handler for when this file is the root module.
|
||||
///
|
||||
/// In ReleaseFast and ReleaseSmall builds we print the panic message to
|
||||
/// stderr and trap, but do not attempt to unwind the stack to print a
|
||||
/// stack trace.
|
||||
pub const panic: type = if (builtin.is_test or switch (builtin.mode) {
|
||||
.Debug, .ReleaseSafe => true,
|
||||
.ReleaseFast, .ReleaseSmall => false,
|
||||
})
|
||||
std.debug.FullPanic(std.debug.defaultPanic)
|
||||
else
|
||||
std.debug.FullPanic(tinyPanicImpl);
|
||||
|
||||
/// Prints the panic message to stderr (best-effort) and traps.
|
||||
///
|
||||
/// This intentionally avoids `std.debug.lockStderr`, which routes through
|
||||
/// `std.Options.debug_io` and would keep the entire `std.Io.Threaded`
|
||||
/// vtable alive in the binary which takes up hundreds of KB.
|
||||
///
|
||||
/// This is safe to call from any thread (and even from signal handlers):
|
||||
/// it takes no locks, performs no allocation, and touches no shared
|
||||
/// mutable state. The message is emitted with a single raw write so that
|
||||
/// concurrent stderr output doesn't interleave with it.
|
||||
fn tinyPanicImpl(msg: []const u8, ra: ?usize) noreturn {
|
||||
@branchHint(.cold);
|
||||
_ = ra;
|
||||
|
||||
// 256 bytes is enough for most messages, so try that first
|
||||
// so that we can try to write in a single syscall.
|
||||
var buf: [256]u8 = undefined;
|
||||
if (std.fmt.bufPrint(
|
||||
&buf,
|
||||
"panic: {s}\n",
|
||||
.{msg},
|
||||
)) |line| {
|
||||
stderr.write(line);
|
||||
} else |_| {
|
||||
stderr.write("panic: ");
|
||||
stderr.write(msg);
|
||||
stderr.write("\n");
|
||||
}
|
||||
|
||||
// Trap forces a standard crash that embedder-provided debuggers
|
||||
// or environments can catch.
|
||||
@trap();
|
||||
}
|
||||
|
||||
test {
|
||||
// Zig 0.16.0 has made test logging more strict. Now, *anything* that gets
|
||||
// printed to stderr results in a "failed command" message, even if the
|
||||
|
||||
@@ -28,6 +28,7 @@ pub const xdg = @import("xdg.zig");
|
||||
pub const windows = @import("windows.zig");
|
||||
pub const macos = @import("macos.zig");
|
||||
pub const shell = @import("shell.zig");
|
||||
pub const stderr = @import("stderr.zig");
|
||||
pub const uri = @import("uri.zig");
|
||||
|
||||
// Functions and types
|
||||
@@ -57,6 +58,7 @@ pub const getKernelInfo = kernel_info.getKernelInfo;
|
||||
|
||||
test {
|
||||
_ = file;
|
||||
_ = stderr;
|
||||
_ = i18n;
|
||||
_ = path;
|
||||
_ = uri;
|
||||
|
||||
60
src/os/stderr.zig
Normal file
60
src/os/stderr.zig
Normal file
@@ -0,0 +1,60 @@
|
||||
//! Raw, best-effort stderr writing that bypasses `std.Io` entirely.
|
||||
//!
|
||||
//! `std.debug.lockStderr` (and everything layered on top of it) routes
|
||||
//! through `std.Options.debug_io`, whose default implementation is
|
||||
//! `std.Io.Threaded`. This keeps ~70KB of unreachable code in binaries.
|
||||
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
/// Write bytes to stderr using the most primitive mechanism available
|
||||
/// for the target. This is best-effort: errors are ignored, since this
|
||||
/// is only used for diagnostics (logging and panic messages).
|
||||
///
|
||||
/// Freestanding targets (e.g. wasm) have no stderr, so this is a no-op
|
||||
/// there.
|
||||
pub fn write(bytes: []const u8) void {
|
||||
switch (builtin.os.tag) {
|
||||
.freestanding, .other => {},
|
||||
|
||||
.windows => {
|
||||
const windows = std.os.windows;
|
||||
const handle = windows.peb().ProcessParameters.hStdError;
|
||||
var iosb: windows.IO_STATUS_BLOCK = undefined;
|
||||
_ = windows.ntdll.NtWriteFile(
|
||||
handle,
|
||||
null, // event
|
||||
null, // APC routine
|
||||
null, // APC context
|
||||
&iosb,
|
||||
bytes.ptr,
|
||||
@intCast(bytes.len),
|
||||
null, // byte offset
|
||||
null, // key
|
||||
);
|
||||
},
|
||||
|
||||
else => {
|
||||
const posix = std.posix;
|
||||
var i: usize = 0;
|
||||
while (i < bytes.len) {
|
||||
const rc = posix.system.write(
|
||||
posix.STDERR_FILENO,
|
||||
bytes[i..].ptr,
|
||||
bytes.len - i,
|
||||
);
|
||||
switch (posix.errno(rc)) {
|
||||
.SUCCESS => i += @as(usize, @intCast(rc)),
|
||||
.INTR => continue,
|
||||
else => return,
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
test write {
|
||||
// Smoke test: must not crash. We can't assert on the output without
|
||||
// capturing stderr, which isn't worth the complexity here.
|
||||
write("");
|
||||
}
|
||||
Reference in New Issue
Block a user