From 9c2dfb863642f0685b5f2307c6ffedbcad9429c1 Mon Sep 17 00:00:00 2001 From: Jae B Date: Sun, 27 Jul 2025 13:07:48 +1000 Subject: [PATCH 1/7] start on supporting new logger interface --- src/android/android.zig | 278 ++++++++++++++++++++++++---------------- 1 file changed, 165 insertions(+), 113 deletions(-) diff --git a/src/android/android.zig b/src/android/android.zig index c8abbd8..9a82360 100644 --- a/src/android/android.zig +++ b/src/android/android.zig @@ -13,6 +13,18 @@ const android_builtin = struct { pub const package_name: [:0]const u8 = ab.package_name; }; +/// Default to the "package" attribute defined in AndroidManifest.xml +/// +/// If tag isn't set when calling "__android_log_write" then it *usually* defaults to the current +/// package name, ie. "com.zig.minimal" +/// +/// However if running via a seperate thread, then it seems to use that threads +/// tag, which means if you log after running code through sdl_main, it won't print +/// logs with the package name. +/// +/// To workaround this, we bake the package name into the Zig binaries. +const log_tag: [:0]const u8 = android_builtin.package_name; + /// Writes the constant string text to the log, with priority prio and tag tag. /// Returns: 1 if the message was written to the log, or -EPERM if it was not; see __android_log_is_loggable(). /// Source: https://developer.android.com/ndk/reference/group/logging @@ -51,62 +63,49 @@ pub const Level = enum(u8) { }; /// Alternate log function implementation that calls __android_log_write so that you can see the logging via "adb logcat" -pub fn logFn( - comptime message_level: std.log.Level, - comptime scope: if (builtin.zig_version.major == 0 and builtin.zig_version.minor == 13) - // Support Zig 0.13.0 - @Type(.EnumLiteral) - else - // Support Zig 0.14.0-dev - @Type(.enum_literal), - comptime format: []const u8, - args: anytype, -) void { - // NOTE(jae): 2024-09-11 - // Zig has a colon ": " or "): " for scoped but Android logs just do that after being flushed - // So we don't do that here. - const prefix2 = if (scope == .default) "" else "(" ++ @tagName(scope) ++ ")"; // "): "; - var androidLogWriter = comptime LogWriter{ - .level = switch (message_level) { - // => .ANDROID_LOG_VERBOSE, // No mapping - .debug => .debug, // android.ANDROID_LOG_DEBUG = 3, - .info => .info, // android.ANDROID_LOG_INFO = 4, - .warn => .warn, // android.ANDROID_LOG_WARN = 5, - .err => .err, // android.ANDROID_LOG_WARN = 6, - }, - }; - const writer = androidLogWriter.writer(); - - nosuspend { - writer.print(prefix2 ++ format ++ "\n", args) catch return; - androidLogWriter.flush(); - } -} - -/// LogWriter was was taken basically as is from: https://github.com/ikskuh/ZigAndroidTemplate -const LogWriter = struct { - /// Default to the "package" attribute defined in AndroidManifest.xml - /// - /// If tag isn't set when calling "__android_log_write" then it *usually* defaults to the current - /// package name, ie. "com.zig.minimal" - /// - /// However if running via a seperate thread, then it seems to use that threads - /// tag, which means if you log after running code through sdl_main, it won't print - /// logs with the package name. - /// - /// To workaround this, we bake the package name into the Zig binaries. - const tag: [:0]const u8 = android_builtin.package_name; +pub const logFn = if (builtin.zig_version.major == 0 and builtin.zig_version.minor <= 14) + LogWriter_Zig014.logFn +else + AndroidLog.logFn; +/// LogWriter_Zig014 was was taken basically as is from: https://github.com/ikskuh/ZigAndroidTemplate +/// +/// Deprecated: To be removed when Zig 0.15.x is stable +const LogWriter_Zig014 = struct { level: Level, line_buffer: [8192]u8 = undefined, line_len: usize = 0, const Error = error{}; - const Writer = if (builtin.zig_version.major == 0 and builtin.zig_version.minor == 14) - std.io.Writer(*@This(), Error, write) - else - std.io.GenericWriter(*@This(), Error, write); + const Writer = std.io.Writer(*@This(), Error, write); + + fn logFn( + comptime message_level: std.log.Level, + comptime scope: @Type(.enum_literal), + comptime format: []const u8, + args: anytype, + ) void { + // NOTE(jae): 2024-09-11 + // Zig has a colon ": " or "): " for scoped but Android logs just do that after being flushed + // So we don't do that here. + const prefix2 = if (scope == .default) "" else "(" ++ @tagName(scope) ++ ")"; // "): "; + var androidLogWriter = comptime @This(){ + .level = switch (message_level) { + // => .ANDROID_LOG_VERBOSE, // No mapping + .debug => .debug, // android.ANDROID_LOG_DEBUG = 3, + .info => .info, // android.ANDROID_LOG_INFO = 4, + .warn => .warn, // android.ANDROID_LOG_WARN = 5, + .err => .err, // android.ANDROID_LOG_WARN = 6, + }, + }; + const logger = androidLogWriter.writer(); + + nosuspend { + logger.print(prefix2 ++ format ++ "\n", args) catch return; + androidLogWriter.flush(); + } + } fn write(self: *@This(), buffer: []const u8) Error!usize { for (buffer) |char| { @@ -130,7 +129,7 @@ const LogWriter = struct { if (self.line_len > 0) { std.debug.assert(self.line_len < self.line_buffer.len - 1); self.line_buffer[self.line_len] = 0; - if (tag.len == 0) { + if (log_tag.len == 0) { _ = __android_log_write( @intFromEnum(self.level), null, @@ -139,7 +138,7 @@ const LogWriter = struct { } else { _ = __android_log_write( @intFromEnum(self.level), - tag.ptr, + log_tag.ptr, &self.line_buffer, ); } @@ -152,6 +151,78 @@ const LogWriter = struct { } }; +/// AndroidLog is a Writer interface that logs out to Android via "__android_log_write" calls +const AndroidLog = struct { + level: Level, + writer: std.Io.Writer, + + const vtable: std.Io.Writer.VTable = .{ + .drain = @This().drain, + }; + + fn init(level: Level, buffer: []u8) AndroidLog { + return .{ + .level = level, + .writer = .{ + .buffer = buffer, + .vtable = &vtable, + }, + }; + } + + fn logFn( + comptime message_level: std.log.Level, + comptime scope: @Type(.enum_literal), + comptime format: []const u8, + args: anytype, + ) void { + // NOTE(jae): 2024-09-11 + // Zig has a colon ": " or "): " for scoped but Android logs just do that after being flushed + // So we don't do that here. + const prefix2 = if (scope == .default) "" else "(" ++ @tagName(scope) ++ ")"; // "): "; + + const android_log_level: Level = switch (message_level) { + // => .ANDROID_LOG_VERBOSE, // No mapping + .debug => .debug, // android.ANDROID_LOG_DEBUG = 3, + .info => .info, // android.ANDROID_LOG_INFO = 4, + .warn => .warn, // android.ANDROID_LOG_WARN = 5, + .err => .err, // android.ANDROID_LOG_WARN = 6, + }; + var buffer: [8192]u8 = undefined; + var logger = AndroidLog.init(android_log_level, &buffer); + + nosuspend { + logger.writer.print(prefix2 ++ format ++ "\n", args) catch return; + logger.writer.flush() catch return; + } + } + + fn write(logger: *AndroidLog, text: []const u8) void { + _ = __android_log_write(@intFromEnum(logger.level), comptime if (log_tag.len == 0) null else log_tag.ptr, text.ptr); + } + + fn drain(w: *std.Io.Writer, data: []const []const u8, splat: usize) std.io.Writer.Error!usize { + const logger: *AndroidLog = @alignCast(@fieldParentPtr("writer", w)); + const slice = data[0 .. data.len - 1]; + // TODO: What do we do with the 'pattern'? + const pattern = data[slice.len]; + var written: usize = pattern.len * splat; + for (slice) |bytes| { + var bytes_to_log = bytes; + while (std.mem.indexOfScalar(u8, bytes_to_log, '\n')) |newline_pos| { + const line = bytes_to_log[0..newline_pos]; + bytes_to_log = bytes_to_log[newline_pos..]; + logger.write(line); + } + if (bytes_to_log.len == 0) continue; + logger.write(bytes_to_log); + written += bytes_to_log.len; + } + w.end = 0; + return written; + } +}; + /// Panic is a copy-paste of the panic logic from Zig but replaces usages of getStdErr with our own writer /// /// Example output (Zig 0.13.0): @@ -175,8 +246,8 @@ const Panic = struct { fn panic(message: []const u8, ret_addr: ?usize) noreturn { @branchHint(.cold); - if (!is_zig_014_or_less) @compileError("Android Panic needs to be updated to the newer io.Writer vtable implementation to work in Zig 0.15.0+"); if (comptime !builtin.abi.isAndroid()) @compileError("do not use Android panic for non-Android builds"); + // if (!is_zig_014_or_less) @compileError("Android Panic needs to be updated to the newer io.Writer vtable implementation to work in Zig 0.15.0+"); const first_trace_addr = ret_addr orelse @returnAddress(); panicImpl(first_trace_addr, message); } @@ -195,13 +266,10 @@ const Panic = struct { } } - const native_os = builtin.os.tag; - const updateSegfaultHandler = std.debug.updateSegfaultHandler; - fn resetSegfaultHandler() void { // NOTE(jae): 2024-09-22 // Not applicable for Android as it runs on the OS tag Linux - // if (native_os == .windows) { + // if (builtin.os.tag == .windows) { // if (windows_segfault_handle) |handle| { // assert(windows.kernel32.RemoveVectoredExceptionHandler(handle) != 0); // windows_segfault_handle = null; @@ -218,28 +286,30 @@ const Panic = struct { posix.sigemptyset(), .flags = 0, }; - // To avoid a double-panic, do nothing if an error happens here. - if (builtin.zig_version.major == 0 and builtin.zig_version.minor == 13) { - // Legacy 0.13.0 - updateSegfaultHandler(&act) catch {}; - } else { - // 0.14.0-dev+ - updateSegfaultHandler(&act); - } + std.debug.updateSegfaultHandler(&act); } const io = struct { - var writer = LogWriter{ - .level = .fatal, - }; - - inline fn getStdErr() *LogWriter { - return &writer; + var log_buffer: [8192]u8 = undefined; + var writer = if (is_zig_014_or_less) + LogWriter_Zig014{ + .level = .fatal, + } + else + AndroidLog.init(.fatal, &log_buffer); + + inline fn getAndroidLogWriter() if (is_zig_014_or_less) + std.io.GenericWriter(*LogWriter_Zig014, LogWriter_Zig014.Error, LogWriter_Zig014.write) + else + *std.Io.Writer { + if (is_zig_014_or_less) + return writer.writer() + else + return &writer.writer; } }; const posix = std.posix; - const enable_segfault_handler = std.options.enable_segfault_handler; /// Panic is a copy-paste of the panic logic from Zig but replaces usages of getStdErr with our own writer /// @@ -248,7 +318,7 @@ const Panic = struct { fn panicImpl(first_trace_addr: ?usize, msg: []const u8) noreturn { @branchHint(.cold); - if (enable_segfault_handler) { + if (std.options.enable_segfault_handler) { // If a segfault happens while panicking, we want it to actually segfault, not trigger // the handler. resetSegfaultHandler(); @@ -266,7 +336,7 @@ const Panic = struct { panic_mutex.lock(); defer panic_mutex.unlock(); - const stderr = io.getStdErr().writer(); + const stderr = io.getAndroidLogWriter(); if (builtin.single_threaded) { stderr.print("panic: ", .{}) catch posix.abort(); } else { @@ -286,8 +356,7 @@ const Panic = struct { // A panic happened while trying to print a previous panic message, // we're still holding the mutex but that's fine as we're going to // call abort() - const stderr = io.getStdErr().writer(); - stderr.print("Panicked during a panic. Aborting.\n", .{}) catch posix.abort(); + android_fatal_log("Panicked during a panic. Aborting."); }, else => { // Panicked while printing "Panicked during a panic." @@ -297,69 +366,52 @@ const Panic = struct { posix.abort(); } - const getSelfDebugInfo = std.debug.getSelfDebugInfo; - const writeStackTrace = std.debug.writeStackTrace; - - // Used for 0.13.0 compatibility, technically this allocator is completely unused by "writeStackTrace" - fn getDebugInfoAllocator() std.mem.Allocator { - return std.heap.page_allocator; - } - fn dumpStackTrace(stack_trace: std.builtin.StackTrace) void { nosuspend { - const stderr = io.getStdErr().writer(); if (comptime builtin.target.cpu.arch.isWasm()) { - if (native_os == .wasi) { - stderr.print("Unable to dump stack trace: not implemented for Wasm\n", .{}) catch return; - } - return; + @compileError("cannot use Android logger with Wasm"); } if (builtin.strip_debug_info) { - stderr.print("Unable to dump stack trace: debug info stripped\n", .{}) catch return; - return; + android_fatal_log("Unable to dump stack trace: debug info stripped"); } - const debug_info = getSelfDebugInfo() catch |err| { + const stderr = io.getAndroidLogWriter(); + const debug_info = std.debug.getSelfDebugInfo() catch |err| { stderr.print("Unable to dump stack trace: Unable to open debug info: {s}\n", .{@errorName(err)}) catch return; return; }; - if (builtin.zig_version.major == 0 and builtin.zig_version.minor == 13) { - // Legacy 0.13.0 - writeStackTrace(stack_trace, stderr, getDebugInfoAllocator(), debug_info, .no_color) catch |err| { - stderr.print("Unable to dump stack trace: {s}\n", .{@errorName(err)}) catch return; - return; - }; - } else { - // 0.14.0-dev+ - writeStackTrace(stack_trace, stderr, debug_info, .no_color) catch |err| { - stderr.print("Unable to dump stack trace: {s}\n", .{@errorName(err)}) catch return; - return; - }; - } + std.debug.writeStackTrace(stack_trace, stderr, debug_info, .no_color) catch |err| { + stderr.print("Unable to dump stack trace: {s}\n", .{@errorName(err)}) catch return; + return; + }; } } - const writeCurrentStackTrace = std.debug.writeCurrentStackTrace; fn dumpCurrentStackTrace(start_addr: ?usize) void { nosuspend { - const stderr = io.getStdErr().writer(); if (comptime builtin.target.cpu.arch.isWasm()) { - if (native_os == .wasi) { - stderr.print("Unable to dump stack trace: not implemented for Wasm\n", .{}) catch return; - } - return; + @compileError("cannot use Android logger with Wasm"); } if (builtin.strip_debug_info) { - stderr.print("Unable to dump stack trace: debug info stripped\n", .{}) catch return; + android_fatal_log("Unable to dump stack trace: debug info stripped"); return; } - const debug_info = getSelfDebugInfo() catch |err| { + const stderr = io.getAndroidLogWriter(); + const debug_info = std.debug.getSelfDebugInfo() catch |err| { stderr.print("Unable to dump stack trace: Unable to open debug info: {s}\n", .{@errorName(err)}) catch return; return; }; - writeCurrentStackTrace(stderr, debug_info, .no_color, start_addr) catch |err| { + std.debug.writeCurrentStackTrace(stderr, debug_info, .no_color, start_addr) catch |err| { stderr.print("Unable to dump stack trace: {s}\n", .{@errorName(err)}) catch return; return; }; } } }; + +fn android_fatal_log(message: [:0]const u8) void { + _ = __android_log_write( + @intFromEnum(Level.fatal), + comptime if (log_tag.len == 0) null else log_tag.ptr, + message, + ); +} From d606595b03a6c2a3c2b119a1a5431e5ce28c4321 Mon Sep 17 00:00:00 2001 From: Jae B Date: Sun, 27 Jul 2025 13:26:19 +1000 Subject: [PATCH 2/7] more work --- src/android/android.zig | 74 +++++++++++++++++++++++++++-------------- 1 file changed, 49 insertions(+), 25 deletions(-) diff --git a/src/android/android.zig b/src/android/android.zig index 9a82360..f290d5a 100644 --- a/src/android/android.zig +++ b/src/android/android.zig @@ -156,6 +156,9 @@ const AndroidLog = struct { level: Level, writer: std.Io.Writer, + /// line is a reusable buffer used to ensure the log data has a NULL terminating byte + line_buffer: [8192]u8, + const vtable: std.Io.Writer.VTable = .{ .drain = @This().drain, }; @@ -163,6 +166,7 @@ const AndroidLog = struct { fn init(level: Level, buffer: []u8) AndroidLog { return .{ .level = level, + .line_buffer = undefined, .writer = .{ .buffer = buffer, .vtable = &vtable, @@ -170,6 +174,51 @@ const AndroidLog = struct { }; } + /// write_line invokes '__android_log_write' and writes text to a line + fn write_line(logger: *AndroidLog, text: []const u8) void { + @memcpy(&logger.line_buffer, text); + logger.line_buffer[text.len] = 0; + const line_buffer = logger.line_buffer[0..text.len]; + + _ = __android_log_write(@intFromEnum(logger.level), comptime if (log_tag.len == 0) null else log_tag.ptr, line_buffer.ptr); + } + + fn drain(w: *std.Io.Writer, data: []const []const u8, splat: usize) std.io.Writer.Error!usize { + const logger: *AndroidLog = @alignCast(@fieldParentPtr("writer", w)); + const slice = data[0 .. data.len - 1]; + var written: usize = 0; + for (slice) |bytes| { + var bytes_to_log = bytes; + while (std.mem.indexOfScalar(u8, bytes_to_log, '\n')) |newline_pos| { + const line = bytes_to_log[0..newline_pos]; + bytes_to_log = bytes_to_log[newline_pos..]; + logger.write_line(line); + } + if (bytes_to_log.len == 0) continue; + logger.write_line(bytes_to_log); + written += bytes_to_log.len; + } + const pattern = data[data.len - 1]; + written += pattern.len * splat; + switch (pattern.len) { + 0 => {}, + 1 => { + @panic("TODO: support 'pattern' for 1 length item (splat logging for Android)"); + // @memset(buffer[w.end..][0..splat], pattern[0]); + // w.end += splat; + }, + else => { + @panic("TODO: support 'pattern' for multiple length item (splat logging for Android)"); + // for (0..splat) |_| { + // @memcpy(buffer[w.end..][0..pattern.len], pattern); + // w.end += pattern.len; + // } + }, + } + w.end = 0; + return written; + } + fn logFn( comptime message_level: std.log.Level, comptime scope: @Type(.enum_literal), @@ -196,31 +245,6 @@ const AndroidLog = struct { logger.writer.flush() catch return; } } - - fn write(logger: *AndroidLog, text: []const u8) void { - _ = __android_log_write(@intFromEnum(logger.level), comptime if (log_tag.len == 0) null else log_tag.ptr, text.ptr); - } - - fn drain(w: *std.Io.Writer, data: []const []const u8, splat: usize) std.io.Writer.Error!usize { - const logger: *AndroidLog = @alignCast(@fieldParentPtr("writer", w)); - const slice = data[0 .. data.len - 1]; - // TODO: What do we do with the 'pattern'? - const pattern = data[slice.len]; - var written: usize = pattern.len * splat; - for (slice) |bytes| { - var bytes_to_log = bytes; - while (std.mem.indexOfScalar(u8, bytes_to_log, '\n')) |newline_pos| { - const line = bytes_to_log[0..newline_pos]; - bytes_to_log = bytes_to_log[newline_pos..]; - logger.write(line); - } - if (bytes_to_log.len == 0) continue; - logger.write(bytes_to_log); - written += bytes_to_log.len; - } - w.end = 0; - return written; - } }; /// Panic is a copy-paste of the panic logic from Zig but replaces usages of getStdErr with our own writer From 7c1451f244327bd30429700ecc21d0d3821f58a1 Mon Sep 17 00:00:00 2001 From: Jae B Date: Sun, 27 Jul 2025 14:13:46 +1000 Subject: [PATCH 3/7] more work --- src/android/android.zig | 130 ++++++++++++++++++++++++++++------------ 1 file changed, 92 insertions(+), 38 deletions(-) diff --git a/src/android/android.zig b/src/android/android.zig index f290d5a..747fc0c 100644 --- a/src/android/android.zig +++ b/src/android/android.zig @@ -30,6 +30,12 @@ const log_tag: [:0]const u8 = android_builtin.package_name; /// Source: https://developer.android.com/ndk/reference/group/logging extern "log" fn __android_log_write(prio: c_int, tag: [*c]const u8, text: [*c]const u8) c_int; +/// Writes a formatted string to the log, with priority prio and tag tag. +/// The details of formatting are the same as for printf(3) +/// Returns: 1 if the message was written to the log, or -EPERM if it was not; see __android_log_is_loggable(). +/// Source: https://man7.org/linux/man-pages/man3/printf.3.html +extern "log" fn __android_log_print(prio: c_int, tag: [*c]const u8, text: [*c]const u8, ...) c_int; + /// Alternate panic implementation that calls __android_log_write so that you can see the logging via "adb logcat" pub const panic = std.debug.FullPanic(Panic.panic); @@ -156,17 +162,14 @@ const AndroidLog = struct { level: Level, writer: std.Io.Writer, - /// line is a reusable buffer used to ensure the log data has a NULL terminating byte - line_buffer: [8192]u8, - const vtable: std.Io.Writer.VTable = .{ .drain = @This().drain, + .flush = @This().flush, }; fn init(level: Level, buffer: []u8) AndroidLog { return .{ .level = level, - .line_buffer = undefined, .writer = .{ .buffer = buffer, .vtable = &vtable, @@ -175,12 +178,35 @@ const AndroidLog = struct { } /// write_line invokes '__android_log_write' and writes text to a line - fn write_line(logger: *AndroidLog, text: []const u8) void { - @memcpy(&logger.line_buffer, text); - logger.line_buffer[text.len] = 0; - const line_buffer = logger.line_buffer[0..text.len]; + fn write_line(_: *AndroidLog, text: []const u8) void { + _ = __android_log_print( + @intFromEnum(Level.fatal), + comptime if (log_tag.len == 0) null else log_tag.ptr, + "%.*s", + text.len, + text.ptr, + ); + } + + /// Repeatedly calls `VTable.drain` until `end` is zero. + pub fn flush(w: *std.Io.Writer) std.io.Writer.Error!void { + const drainFn = w.vtable.drain; + while (w.end != 0) _ = try drainFn(w, &.{w.buffer[0..w.end]}, 1); + } - _ = __android_log_write(@intFromEnum(logger.level), comptime if (log_tag.len == 0) null else log_tag.ptr, line_buffer.ptr); + fn log_buffer(logger: *AndroidLog, buffer: []const u8) std.io.Writer.Error!usize { + var written: usize = 0; + var bytes_to_log = buffer; + while (std.mem.indexOfScalar(u8, bytes_to_log, '\n')) |newline_pos| { + const line = bytes_to_log[0..newline_pos]; + bytes_to_log = bytes_to_log[newline_pos..]; + logger.write_line(line); + written += line.len; + } + if (bytes_to_log.len == 0) return written; + logger.write_line(bytes_to_log); + written += bytes_to_log.len; + return written; } fn drain(w: *std.Io.Writer, data: []const []const u8, splat: usize) std.io.Writer.Error!usize { @@ -188,31 +214,19 @@ const AndroidLog = struct { const slice = data[0 .. data.len - 1]; var written: usize = 0; for (slice) |bytes| { - var bytes_to_log = bytes; - while (std.mem.indexOfScalar(u8, bytes_to_log, '\n')) |newline_pos| { - const line = bytes_to_log[0..newline_pos]; - bytes_to_log = bytes_to_log[newline_pos..]; - logger.write_line(line); - } - if (bytes_to_log.len == 0) continue; - logger.write_line(bytes_to_log); - written += bytes_to_log.len; + written += try logger.log_buffer(bytes); } const pattern = data[data.len - 1]; written += pattern.len * splat; switch (pattern.len) { 0 => {}, 1 => { - @panic("TODO: support 'pattern' for 1 length item (splat logging for Android)"); - // @memset(buffer[w.end..][0..splat], pattern[0]); - // w.end += splat; + written += try logger.log_buffer(pattern); }, else => { - @panic("TODO: support 'pattern' for multiple length item (splat logging for Android)"); - // for (0..splat) |_| { - // @memcpy(buffer[w.end..][0..pattern.len], pattern); - // w.end += pattern.len; - // } + for (0..splat) |_| { + written += try logger.log_buffer(pattern); + } }, } w.end = 0; @@ -225,6 +239,24 @@ const AndroidLog = struct { comptime format: []const u8, args: anytype, ) void { + // If there are no arguments for the logging, just call Android log directly + const ArgsType = @TypeOf(args); + const args_type_info = @typeInfo(ArgsType); + if (args_type_info != .@"struct") { + @compileError("expected tuple or struct argument, found " ++ @typeName(ArgsType)); + } + const fields_info = args_type_info.@"struct".fields; + if (fields_info.len == 0) { + _ = __android_log_print( + @intFromEnum(Level.fatal), + comptime if (log_tag.len == 0) null else log_tag.ptr, + "%.*s", + format.len, + format.ptr, + ); + return; + } + // NOTE(jae): 2024-09-11 // Zig has a colon ": " or "): " for scoped but Android logs just do that after being flushed // So we don't do that here. @@ -359,15 +391,25 @@ const Panic = struct { { panic_mutex.lock(); defer panic_mutex.unlock(); - - const stderr = io.getAndroidLogWriter(); if (builtin.single_threaded) { - stderr.print("panic: ", .{}) catch posix.abort(); + _ = __android_log_print( + @intFromEnum(Level.fatal), + comptime if (log_tag.len == 0) null else log_tag.ptr, + "panic: %.*s", + msg.len, + msg.ptr, + ); } else { - const current_thread_id = std.Thread.getCurrentId(); - stderr.print("thread {} panic: ", .{current_thread_id}) catch posix.abort(); + const current_thread_id: u32 = std.Thread.getCurrentId(); + _ = __android_log_print( + @intFromEnum(Level.fatal), + comptime if (log_tag.len == 0) null else log_tag.ptr, + "thread %d panic: %.*s", + current_thread_id, + msg.len, + msg.ptr, + ); } - stderr.print("{s}\n", .{msg}) catch posix.abort(); if (@errorReturnTrace()) |t| dumpStackTrace(t.*); dumpCurrentStackTrace(first_trace_addr); } @@ -398,13 +440,13 @@ const Panic = struct { if (builtin.strip_debug_info) { android_fatal_log("Unable to dump stack trace: debug info stripped"); } - const stderr = io.getAndroidLogWriter(); const debug_info = std.debug.getSelfDebugInfo() catch |err| { - stderr.print("Unable to dump stack trace: Unable to open debug info: {s}\n", .{@errorName(err)}) catch return; + android_fatal_print_c_string("Unable to dump stack trace: Unable to open debug info: %s", @errorName(err)); return; }; + const stderr = io.getAndroidLogWriter(); std.debug.writeStackTrace(stack_trace, stderr, debug_info, .no_color) catch |err| { - stderr.print("Unable to dump stack trace: {s}\n", .{@errorName(err)}) catch return; + android_fatal_print_c_string("Unable to dump stack trace: %s", @errorName(err)); return; }; } @@ -419,13 +461,13 @@ const Panic = struct { android_fatal_log("Unable to dump stack trace: debug info stripped"); return; } - const stderr = io.getAndroidLogWriter(); const debug_info = std.debug.getSelfDebugInfo() catch |err| { - stderr.print("Unable to dump stack trace: Unable to open debug info: {s}\n", .{@errorName(err)}) catch return; + android_fatal_print_c_string("Unable to dump stack trace: Unable to open debug info: %s", @errorName(err)); return; }; + const stderr = io.getAndroidLogWriter(); std.debug.writeCurrentStackTrace(stderr, debug_info, .no_color, start_addr) catch |err| { - stderr.print("Unable to dump stack trace: {s}\n", .{@errorName(err)}) catch return; + android_fatal_print_c_string("Unable to dump stack trace: %s", @errorName(err)); return; }; } @@ -439,3 +481,15 @@ fn android_fatal_log(message: [:0]const u8) void { message, ); } + +fn android_fatal_print_c_string( + comptime fmt: [:0]const u8, + c_str: [:0]const u8, +) void { + _ = __android_log_print( + @intFromEnum(Level.fatal), + comptime if (log_tag.len == 0) null else log_tag.ptr, + fmt, + c_str.ptr, + ); +} From b1bbb336baca14204b3d37a1ce8c9c56b924a6f9 Mon Sep 17 00:00:00 2001 From: Jae B Date: Sun, 27 Jul 2025 17:14:16 +1000 Subject: [PATCH 4/7] more work --- src/android/android.zig | 77 +++++++++++++++++------------------------ 1 file changed, 31 insertions(+), 46 deletions(-) diff --git a/src/android/android.zig b/src/android/android.zig index 747fc0c..25d7d31 100644 --- a/src/android/android.zig +++ b/src/android/android.zig @@ -164,7 +164,6 @@ const AndroidLog = struct { const vtable: std.Io.Writer.VTable = .{ .drain = @This().drain, - .flush = @This().flush, }; fn init(level: Level, buffer: []u8) AndroidLog { @@ -177,59 +176,53 @@ const AndroidLog = struct { }; } - /// write_line invokes '__android_log_write' and writes text to a line - fn write_line(_: *AndroidLog, text: []const u8) void { - _ = __android_log_print( - @intFromEnum(Level.fatal), - comptime if (log_tag.len == 0) null else log_tag.ptr, - "%.*s", - text.len, - text.ptr, - ); - } - - /// Repeatedly calls `VTable.drain` until `end` is zero. - pub fn flush(w: *std.Io.Writer) std.io.Writer.Error!void { - const drainFn = w.vtable.drain; - while (w.end != 0) _ = try drainFn(w, &.{w.buffer[0..w.end]}, 1); - } - - fn log_buffer(logger: *AndroidLog, buffer: []const u8) std.io.Writer.Error!usize { + fn log_each_newline(logger: *AndroidLog, buffer: []const u8) std.io.Writer.Error!usize { var written: usize = 0; var bytes_to_log = buffer; while (std.mem.indexOfScalar(u8, bytes_to_log, '\n')) |newline_pos| { const line = bytes_to_log[0..newline_pos]; - bytes_to_log = bytes_to_log[newline_pos..]; - logger.write_line(line); + bytes_to_log = bytes_to_log[newline_pos + 1 ..]; + android_log_string(logger.level, line); written += line.len; } if (bytes_to_log.len == 0) return written; - logger.write_line(bytes_to_log); + android_log_string(logger.level, bytes_to_log); written += bytes_to_log.len; return written; } fn drain(w: *std.Io.Writer, data: []const []const u8, splat: usize) std.io.Writer.Error!usize { const logger: *AndroidLog = @alignCast(@fieldParentPtr("writer", w)); - const slice = data[0 .. data.len - 1]; var written: usize = 0; + + // Consume 'buffer[0..end]' first + written += try logger.log_each_newline(w.buffer[0..w.end]); + w.end = 0; + + // NOTE(jae): 2025-07-27 + // The logic below should probably try to collect the buffers / pattern + // below into one buffer first so that newlines are handled as expected but I'm not willing + // to put the effort in. + + // Write additional overflow data + const slice = data[0 .. data.len - 1]; for (slice) |bytes| { - written += try logger.log_buffer(bytes); + written += try logger.log_each_newline(bytes); } + + // The last element of data is repeated as necessary const pattern = data[data.len - 1]; - written += pattern.len * splat; switch (pattern.len) { 0 => {}, 1 => { - written += try logger.log_buffer(pattern); + written += try logger.log_each_newline(pattern); }, else => { for (0..splat) |_| { - written += try logger.log_buffer(pattern); + written += try logger.log_each_newline(pattern); } }, } - w.end = 0; return written; } @@ -239,24 +232,6 @@ const AndroidLog = struct { comptime format: []const u8, args: anytype, ) void { - // If there are no arguments for the logging, just call Android log directly - const ArgsType = @TypeOf(args); - const args_type_info = @typeInfo(ArgsType); - if (args_type_info != .@"struct") { - @compileError("expected tuple or struct argument, found " ++ @typeName(ArgsType)); - } - const fields_info = args_type_info.@"struct".fields; - if (fields_info.len == 0) { - _ = __android_log_print( - @intFromEnum(Level.fatal), - comptime if (log_tag.len == 0) null else log_tag.ptr, - "%.*s", - format.len, - format.ptr, - ); - return; - } - // NOTE(jae): 2024-09-11 // Zig has a colon ": " or "): " for scoped but Android logs just do that after being flushed // So we don't do that here. @@ -493,3 +468,13 @@ fn android_fatal_print_c_string( c_str.ptr, ); } + +fn android_log_string(android_log_level: Level, text: []const u8) void { + _ = __android_log_print( + @intFromEnum(android_log_level), + comptime if (log_tag.len == 0) null else log_tag.ptr, + "%.*s", + text.len, + text.ptr, + ); +} From e2632443d696fe7ce81b9462d2234e7194df2f78 Mon Sep 17 00:00:00 2001 From: Jae B Date: Sun, 27 Jul 2025 17:32:05 +1000 Subject: [PATCH 5/7] more work --- src/android/android.zig | 62 ++++++++++++++++++++++++++++++++++------- 1 file changed, 52 insertions(+), 10 deletions(-) diff --git a/src/android/android.zig b/src/android/android.zig index 25d7d31..8a27ff8 100644 --- a/src/android/android.zig +++ b/src/android/android.zig @@ -232,6 +232,26 @@ const AndroidLog = struct { comptime format: []const u8, args: anytype, ) void { + // If there are no arguments or '{}' patterns in the logging, just call Android log directly + const ArgsType = @TypeOf(args); + const args_type_info = @typeInfo(ArgsType); + if (args_type_info != .@"struct") { + @compileError("expected tuple or struct argument, found " ++ @typeName(ArgsType)); + } + const fields_info = args_type_info.@"struct".fields; + if (fields_info.len == 0 and + comptime std.mem.indexOfScalar(u8, format, '{') == null) + { + _ = __android_log_print( + @intFromEnum(Level.fatal), + comptime if (log_tag.len == 0) null else log_tag.ptr, + "%.*s", + format.len, + format.ptr, + ); + return; + } + // NOTE(jae): 2024-09-11 // Zig has a colon ": " or "): " for scoped but Android logs just do that after being flushed // So we don't do that here. @@ -321,22 +341,42 @@ const Panic = struct { } const io = struct { - var log_buffer: [8192]u8 = undefined; - var writer = if (is_zig_014_or_less) + /// Collect data in writer buffer and flush to Android logs per newline + var android_log_writer_buffer: [8192]u8 = undefined; + + /// The primary motivation for recursive mutex here is so that a panic while + /// android log writer mutex is held still dumps the stack trace and other debug + /// information. + var android_log_writer_mutex = std.Thread.Mutex.Recursive.init; + + var android_panic_log_writer = if (is_zig_014_or_less) LogWriter_Zig014{ .level = .fatal, } else - AndroidLog.init(.fatal, &log_buffer); + AndroidLog.init(.fatal, &android_log_writer_buffer); - inline fn getAndroidLogWriter() if (is_zig_014_or_less) + fn lockAndroidLogWriter() if (is_zig_014_or_less) std.io.GenericWriter(*LogWriter_Zig014, LogWriter_Zig014.Error, LogWriter_Zig014.write) else *std.Io.Writer { - if (is_zig_014_or_less) - return writer.writer() - else - return &writer.writer; + android_log_writer_mutex.lock(); + if (is_zig_014_or_less) { + android_panic_log_writer.flush(); + return android_panic_log_writer.writer(); + } else { + android_panic_log_writer.writer.flush() catch {}; + return &android_panic_log_writer.writer; + } + } + + fn unlockAndroidLogWriter() void { + if (is_zig_014_or_less) { + android_panic_log_writer.flush(); + } else { + android_panic_log_writer.writer.flush() catch {}; + } + android_log_writer_mutex.unlock(); } }; @@ -419,7 +459,8 @@ const Panic = struct { android_fatal_print_c_string("Unable to dump stack trace: Unable to open debug info: %s", @errorName(err)); return; }; - const stderr = io.getAndroidLogWriter(); + const stderr = io.lockAndroidLogWriter(); + defer io.unlockAndroidLogWriter(); std.debug.writeStackTrace(stack_trace, stderr, debug_info, .no_color) catch |err| { android_fatal_print_c_string("Unable to dump stack trace: %s", @errorName(err)); return; @@ -440,7 +481,8 @@ const Panic = struct { android_fatal_print_c_string("Unable to dump stack trace: Unable to open debug info: %s", @errorName(err)); return; }; - const stderr = io.getAndroidLogWriter(); + const stderr = io.lockAndroidLogWriter(); + defer io.unlockAndroidLogWriter(); std.debug.writeCurrentStackTrace(stderr, debug_info, .no_color, start_addr) catch |err| { android_fatal_print_c_string("Unable to dump stack trace: %s", @errorName(err)); return; From 3ee91eabf22a9af1475b8dd7f60914a4212a9fc3 Mon Sep 17 00:00:00 2001 From: Jae B Date: Sun, 27 Jul 2025 17:36:20 +1000 Subject: [PATCH 6/7] remove panic_mutex --- src/android/android.zig | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/android/android.zig b/src/android/android.zig index 8a27ff8..a6e91f2 100644 --- a/src/android/android.zig +++ b/src/android/android.zig @@ -286,9 +286,6 @@ const Panic = struct { /// The counter is incremented/decremented atomically. var panicking = std.atomic.Value(u8).init(0); - // Locked to avoid interleaving panic messages from multiple threads. - var panic_mutex = std.Thread.Mutex{}; - /// Counts how many times the panic handler is invoked by this thread. /// This is used to catch and handle panics triggered by the panic handler. threadlocal var panic_stage: usize = 0; @@ -404,8 +401,6 @@ const Panic = struct { // Make sure to release the mutex when done { - panic_mutex.lock(); - defer panic_mutex.unlock(); if (builtin.single_threaded) { _ = __android_log_print( @intFromEnum(Level.fatal), From 06602dcd834e26c0e7c5459025323f1551775dec Mon Sep 17 00:00:00 2001 From: Jae B Date: Sun, 27 Jul 2025 17:56:02 +1000 Subject: [PATCH 7/7] more --- src/android/android.zig | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/android/android.zig b/src/android/android.zig index a6e91f2..d2601ea 100644 --- a/src/android/android.zig +++ b/src/android/android.zig @@ -421,7 +421,13 @@ const Panic = struct { ); } if (@errorReturnTrace()) |t| dumpStackTrace(t.*); - dumpCurrentStackTrace(first_trace_addr); + if (is_zig_014_or_less) { + dumpCurrentStackTrace_014(first_trace_addr); + } else { + const stderr = io.lockAndroidLogWriter(); + defer io.unlockAndroidLogWriter(); + std.debug.dumpCurrentStackTraceToWriter(first_trace_addr orelse @returnAddress(), stderr) catch {}; + } } waitForOtherThreadToFinishPanicking(); @@ -463,7 +469,8 @@ const Panic = struct { } } - fn dumpCurrentStackTrace(start_addr: ?usize) void { + /// Deprecated: Only used for current Zig 0.14.1 stable builds, + fn dumpCurrentStackTrace_014(start_addr: ?usize) void { nosuspend { if (comptime builtin.target.cpu.arch.isWasm()) { @compileError("cannot use Android logger with Wasm");