Get caller's std.builtin.SourceLocation?

is there a way to mimic C/C++'s
#define PRINT(...) print_impl(__FILE__, __func__, __LINE__, __VA_ARGS__)?

I think you want this builtin? @src:

const std = @import("std");

fn exampleFunction() !void {
    const s = @src();
    std.debug.print("file: {s}\n", .{s.file});
    std.debug.print("fn_name: {s}\n", .{s.fn_name});
    std.debug.print("line: {d}\n", .{s.line});
    std.debug.print("column: {d}\n", .{s.column});
}

pub fn main() !void {
    try exampleFunction();
}
file: sourcelocation.zig
fn_name: exampleFunction
line: 4
column: 15

If you want to create some kind of utility printf debugging thing you need to pass @src() to the function as a parameter with type std.builtin.SourceLocation:

fn print(s: std.builtin.SourceLocation, comptime fmt: []const u8, args: anytype) void {
    std.debug.print(fmt, args);
    std.debug.print("file: {s}\n", .{s.file});
    std.debug.print("fn_name: {s}\n", .{s.fn_name});
    std.debug.print("line: {d}\n", .{s.line});
    std.debug.print("column: {d}\n", .{s.column});
}

pub fn main() !void {
    const foo = 12;
    print(@src(), "foo is: {d}\n", .{foo});
}

Another way would be to use:

3 Likes

A more powerful alternative is to use @returnAddress. This allows you to capture a single usize that represents the callsite of the current function. You can later pass it to std.debug.printSourceAtAddress to output it in the same printing style as a stack trace frame.

A related API is std.debug.Trace.

11 Likes

After following @Sze @andrewrk suggestions came up with 2 solutions(posting here for my future self mainly coz i feel like i will comeback looking for this again in the future and for others who just want to copy paste something which might work instantly).

The snippets assumes you’re only using std.log and only want to print source location for std.log.debug calls(of course those can be changed)

In order to tell the standard library to use our custom long function something like the following is needed at the start of main.zig(not entirely sure where should we really put this “configuration” struct), myLogFn will be doing the heavy lifting.

pub const std_options = .{
    .log_level = .debug,
    .logFn = myLogFn,
};

Hackish solution
this is probably going to work well if you directly use std.log.(info, debug, err) without any wrappers for current std.log implementaion,
this is basically a direct copy paste of std.debug.dumpStackTrace.
so far i understood is, it just checks all the available return addresses on the stack(using native os functions or some builtin stuff), and gets debug symbols assigned to these addresses, so this modified version looks for the return address which’s debug symbol doesn’t contain std/log.zig in it’s file_name(std.debug.SymbolInfo → std.debug.LineInfo) if it encounters ?? or no file_name then it’s going to select that address as the main caller’s return address.

fn writeLineInfoOfAddress(debug_info: *std.debug.SelfInfo, address: usize, out_stream: anytype) bool {
    const module = debug_info.getModuleForAddress(address) catch return false;
    const symbol_info = module.getSymbolAtAddress(debug_info.allocator, address) catch return false;

    defer symbol_info.deinit(debug_info.allocator);

    if (symbol_info.line_info) |li| {
        // if no file name contains "std/log.zig" we are porbably at the actual call location.
        if (std.mem.indexOf(u8, li.file_name, "std" ++ std.fs.path.sep_str ++ "log.zig") != null) return false;
        out_stream.print("[{s}:{d}:{d}] ", .{ li.file_name, li.line, li.column }) catch return false;
        return true;
    }
    return false;
}

pub fn myLogFn(
    comptime message_level: std.log.Level,
    comptime scope: @Type(.EnumLiteral),
    comptime format: []const u8,
    args: anytype,
) void {
    const level_txt = comptime message_level.asText();
    const prefix2 = if (scope == .default) ": " else "(" ++ @tagName(scope) ++ "): ";
    const stderr = std.io.getStdErr().writer();
    var bw = std.io.bufferedWriter(stderr);
    const writer = bw.writer();

    std.debug.lockStdErr();
    defer std.debug.unlockStdErr();
    nosuspend {
        if (message_level == .debug) {
            const debug_info = std.debug.getSelfDebugInfo() catch return;
            var context: std.debug.ThreadContext = undefined;
            const has_context = std.debug.getContext(&context);

            if (builtin.os.tag == .windows) {
                // usually call stack looks like:
                //      std.log.debug(main_call_loc) -> std.log.scoped.debug -> std.log.log -> myLogFn(we're here)
                const HIGHEST_STACK_RET_ADDR = 3;
                var addr_buf: [HIGHEST_STACK_RET_ADDR]usize = undefined;

                const n = std.debug.walkStackWindows(addr_buf[0..], &context);
                const addrs = addr_buf[0..n];
                for (addrs) |addr| {
                    if (writeLineInfoOfAddress(debug_info, addr - 1, writer)) break;
                }
            } else {
                var it = (if (has_context) blk: {
                    break :blk std.debug.StackIterator.initWithContext(null, debug_info, &context) catch null;
                } else null) orelse std.debug.StackIterator.init(null, null);
                defer it.deinit();

                while (it.next()) |return_address| {
                    const address = if (return_address == 0) return_address else return_address - 1;

                    if (writeLineInfoOfAddress(debug_info, address, writer)) break;
                }
            }
        }
        writer.print(level_txt ++ prefix2 ++ format ++ "\n", args) catch return;
        bw.flush() catch return;
    }
}

Example(after putting everything inside of the main.zig:

fn bar() void {
    std.log.debug("Debug 3!", .{});
}

fn foo() void {
    std.log.debug("Debug 2!", .{});
}

pub fn main() !void {
    std.log.debug("Debug 1!", .{});
    foo();
    bar();
}

Output would look like:

[<full project path>\src\main.zig:82:18] debug: Debug 1!
[<full project path>\src\main.zig:78:18] debug: Debug 2!
[<full project path>\src\main.zig:74:18] debug: Debug 3!

The proper way?
Just a catch you’ve to write a wrapper around each std.log.(info, warn, debug, *) and use that wrapped function, we can probably try to use comptime magic to do that automatically for us.

fn writeLineInfoOfAddress(debug_info: *std.debug.SelfInfo, address: usize, out_stream: anytype) bool {
    const module = debug_info.getModuleForAddress(address) catch return false;
    const symbol_info = module.getSymbolAtAddress(debug_info.allocator, address) catch return false;

    defer symbol_info.deinit(debug_info.allocator);

    if (symbol_info.line_info) |li| {
        // if no file name contains "std/log.zig" we are porbably at the actual call location.
        if (std.mem.indexOf(u8, li.file_name, "std" ++ std.fs.path.sep_str ++ "log.zig") != null) return false;
        out_stream.print("[{s}:{d}:{d}] ", .{ li.file_name, li.line, li.column }) catch return false;
        return true;
    }
    return false;
}

pub fn myLogFn(
    comptime message_level: std.log.Level,
    comptime scope: @Type(.EnumLiteral),
    comptime format: []const u8,
    args_with_ret_addr: anytype,
) void {
    const level_txt = comptime message_level.asText();
    const prefix2 = if (scope == .default) ": " else "(" ++ @tagName(scope) ++ "): ";
    const stderr = std.io.getStdErr().writer();
    var bw = std.io.bufferedWriter(stderr);
    const writer = bw.writer();

    std.debug.lockStdErr();
    defer std.debug.unlockStdErr();
    nosuspend {
        const ArgsType = @TypeOf(args_with_ret_addr);
        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;
        const args = if (fields_info.len == 2) blk: {
            const second_type_info = @typeInfo(fields_info[1].type);
            // if the 1st argument is an `usize` type and 2nd argument is a `struct`
            // we assume we've been passed a return address and the format args.
            if (message_level == .debug and fields_info[0].type == usize and second_type_info == .Struct) {
                const debug_info = std.debug.getSelfDebugInfo() catch return;
                _ = writeLineInfoOfAddress(debug_info, args_with_ret_addr[0], writer);
                break :blk args_with_ret_addr[1];
            }
            break :blk args_with_ret_addr;
        } else args_with_ret_addr;

        writer.print(level_txt ++ prefix2 ++ format ++ "\n", args) catch return;
        bw.flush() catch return;
    }
}

fn custom_debug_log(
    comptime format: []const u8,
    args: anytype,
) void {
    // not sure if `@returnAddress() - 1` correct but it kinda
    // makes sense maybe, x86_64 version:
    // call [custom_debug_log]
    // mov rax, rcx # some random instruction
    // so `@returnAddress() - 1` will point to the last byte of 
    // the `call *` instruction, `std.debug.getModuleForAddress` 
    // should be able to get the proper location may be
    std.log.debug(format, .{ @returnAddress() - 1, args });
}

Example(after putting everything inside of the main.zig:

fn bar() void {
    custom_debug_log("Debug 3!", .{});
}

fn foo() void {
    custom_debug_log("Debug 2!", .{});
}

pub fn main() !void {
    custom_debug_log("Debug 1!", .{});
    foo();
    bar();
}

Output would look like:

[<full project path>\src\main.zig:78:21] debug: Debug 1!
[<full project path>\src\main.zig:74:21] debug: Debug 2!
[<full project path>\src\main.zig:70:21] debug: Debug 3!
1 Like

Can you also show an example how you use your function and what the output is?
Would make it easier for people to follow along, without investigating in detail.