Good way to wrap structs / @src() for formatting (here logging)

So far the only use of @src() I have seen was together with tracy, but when trying to log a sourcelocation myself I encountered the issue of creating the format tuple manually each time.
There is a std.meta.ArgsTuple function, but no StructTuple or StructFieldsTuple.

Would this be a useful addition to the library and or am I just overseeing obvious issues with this?:

const std = @import("std");
const log = std.log.scoped(.ok);

const src_log = "\n   Module: {s}\n   File:   {s}\n   Fn:     {s}\n   Column  {d:<5}\n   Row     {d:<5}";

/// For a given struct or union type, returns a tuple type which fields will
/// correspond to the field types.
///
/// Examples:
/// - `StructTuple(struct{a: u32})` ⇒ `tuple { u32 }`
/// - `StructTuple(struct{a: u32, b: f16})` ⇒ `tuple { u32, f16 }`
pub fn StructTuple(comptime Struct_: type) type {
    const field_info = switch (@typeInfo(Struct_)) {
        .@"struct" => |info| info.fields,
        else => @compileError("StructTuple expects a struct type"),
    };
    var struct_field_list: [field_info.len]type = undefined;
    inline for (field_info, 0..) |field, i| {
        struct_field_list[i] = field.type;
    }
    return std.meta.Tuple(&struct_field_list);
}

fn format_src(comptime src: std.builtin.SourceLocation) StructTuple(std.builtin.SourceLocation) {
    return .{
        src.module,
        src.file,
        src.fn_name,
        src.line,
        src.column,
    };
}

fn dummy() void {
    log.warn(src_log, format_src(@src()));
    std.debug.print("Hi\n", .{});
}

pub fn main() !void {
    dummy();
}

Playing around with the idea / possibility of automatically filling the created tuple with the given struct values.

Why doesnt this work:

pub fn StructFieldsTuple(comptime Struct: anytype) type {
    const field_info = switch (@typeInfo(@TypeOf(Struct))) {
        .@"struct" => |info| info.fields,
        else => @compileError("StructFieldsTuple expects a struct type"),
    };
    var struct_field_list: [field_info.len]type = undefined;
    inline for (field_info, 0..) |field, i| {
        struct_field_list[i] = field.type;
    }
    return std.meta.Tuple(&struct_field_list);
}

fn format_src(comptime Struct: anytype) StructFieldsTuple(Struct) {
    const fields_tuple = StructFieldsTuple(Struct);
    const field_info = switch (@typeInfo(@TypeOf(Struct))) {
        .@"struct" => |info| info.fields,
        else => @compileError("StructFieldsTuple expects a struct type"),
    };
    inline for (field_info, 0..) |field, i| {
        @setEvalBranchQuota(10_000);
        @field(fields_tuple, std.fmt.comptimePrint("{d}", .{i})) = @field(Struct, field.name);
    }
    return fields_tuple;
}

I encounter: error: struct 'struct { [:0]const u8, [:0]const u8, [:0]const u8, u32, u32 }' has no member named '0'

Although std.meta seems to set the name to 0… respectively with .name = …:

pub fn Tuple(comptime types: []const type) type {
    return CreateUniqueTuple(types.len, types[0..types.len].*);
}

fn CreateUniqueTuple(comptime N: comptime_int, comptime types: [N]type) type {
    var tuple_fields: [types.len]std.builtin.Type.StructField = undefined;
    inline for (types, 0..) |T, i| {
        @setEvalBranchQuota(10_000);
        var num_buf: [128]u8 = undefined;
        tuple_fields[i] = .{
            .name = std.fmt.bufPrintZ(&num_buf, "{d}", .{i}) catch unreachable,
            .type = T,
            .default_value_ptr = null,
            .is_comptime = false,
            .alignment = 0,
        };
    }

    return @Type(.{
        .@"struct" = .{
            .is_tuple = true,
            .layout = .auto,
            .decls = &.{},
            .fields = &tuple_fields,
        },
    });
}

Is the order of fields in @typeInfo(T).@"struct".fields being the same as declaration order considered a language guarantee, other than for tuples?

Because, I could plausibly imagine a scenario where it changes in future to reflect layout in memory, for example. I guess even for tuples it wouldn’t be strictly necessary.

I believe reordering structs for alignment-based memorysavings only takes place in some later step during compilation, meaning that layout-order should be preserved for meta-programmin. But defenitely not sure.

since SourceLocation is a struct you can just std.debug.print(src_log, @src());
format just uses the order that it gets from @typeInfo which is the order they are defined in, (i recall a discusion about whether or not that is garunteed by the language, i dont remember the result but this is how it works atm)
if you want to change the order you can refer to the field names in the format string eg "{[module]s}", if its a tuple the field names are just their index

1 Like

Right, I can just use log.info(src_log, @src()).

But although a tuples field names are just their indexes, I cannot access them using @fields. I can compare both the @typeInfo fieldnames with my constructed fieldnames ( using std.mem.eql and std.fmt.comptimePrint) but once I attempt to use said fieldname with @fields, i get the error of error: struct 'struct { [:0]const u8, [:0]const u8, [:0]const u8, u32, u32 }' has no member named '0'

strange, wen doing things manually on normally constructed tuples, it works:

    const bb = struct { u32, f16 };
    var cc: bb = undefined;
    @field(cc, "0") = 45;
    std.debug.print("{any}", .{cc});

I cant help you if you dont show the code that is problematic

First post I made on the very top.:

fn format_src(comptime Struct: anytype) StructFieldsTuple(Struct)

Thank you for engaging with my post, but I found the solution. I created a type instead of an undefined instance of a type. Functional code below.
Remaining question is only if my Function StructFieldsTuple and tupleFromStructis useful enough to be included in std.meta for general usage.

const std = @import("std");
const log = std.log.scoped(.ok);

const src_log = "\n   Module: {s}\n   File:   {s}\n   Fn:     {s}\n   Column  {d:<5}\n   Row     {d:<5}";

/// For a given struct or union type, returns a tuple type which fields will
/// correspond to the field types.
///
/// Examples:
/// - `StructFieldsTuple(struct{a: u32})` ⇒ `tuple { u32 }`
/// - `StructFieldsTuple(struct{a: u32, b: f16})` ⇒ `tuple { u32, f16 }`
pub fn StructFieldsTuple(comptime Struct: anytype) type {
    const field_info = switch (@typeInfo(@TypeOf(Struct))) {
        .@"struct" => |info| info.fields,
        else => @compileError("StructFieldsTuple expects a struct type"),
    };
    var struct_field_list: [field_info.len]type = undefined;
    inline for (field_info, 0..) |field, i| {
        struct_field_list[i] = field.type;
    }
    return std.meta.Tuple(&struct_field_list);
}

fn tupleFromStruct(comptime Struct: anytype) StructFieldsTuple(Struct) {
    var fields_tuple: StructFieldsTuple(Struct) = undefined;
    const field_info = switch (@typeInfo(@TypeOf(Struct))) {
        .@"struct" => |info| info.fields,
        else => @compileError("StructFieldsTuple expects a struct type"),
    };
    inline for (field_info, 0..) |field, i| {
        @setEvalBranchQuota(10_000);
        const name_i = std.fmt.comptimePrint("{d}", .{i});
        @field(fields_tuple, name_i) = @field(Struct, field.name);
    }
    return fields_tuple;
}

fn dummy() void {
    log.warn(src_log, tupleFromStruct(@src()));
}

pub fn main() !void {
    dummy();
}

I don’t think it’s useful. I’d argue if you needed this, you’re probably doing something wrong. There are ofc exceptions, but I think the legitimate use case for this is very niche

1 Like

I have this that I use sometimes

const std = @import("std");
const debug = std.debug;

pub const Debug = struct {
    address: ?usize = null,
    sym: ?debug.Symbol = null,

    pub fn init(address: usize) Debug {
        var self: Debug = .{
            .address = address,
            .sym = null,
        };

        const info = debug.getSelfDebugInfo() catch {
            return self;
        };

        const module = info.getModuleForAddress(address) catch {
            return self;
        };

        const symbols = module.getSymbolAtAddress(info.allocator, address) catch {
            return self;
        };

        self.sym = symbols;
        return self;
    }

    pub fn format(
        s: @This(),
        comptime fmt: []const u8,
        options: std.fmt.FormatOptions,
        writer: anytype,
    ) !void {
        _ = fmt;
        _ = options;

        if (s.sym) |sym| {
            if (sym.source_location) |loc| {
                try writer.print("{s} : fn {s}()[{d}:{d}] ", .{ loc.file_name, sym.name, loc.line, loc.column });
            }
        }
    }
};

pub fn dbg(v: anytype) @TypeOf(v) {
    std.debug.print("{} '{any}'\n", .{ Debug.init(@returnAddress()), v });
    return v;
}

pub fn assertDebug(cond: bool, comptime fmt: []const u8, args: anytype) void {
    if (!cond) {
        const address = @returnAddress();
        const info = debug.getSelfDebugInfo() catch {
            std.debug.print("[" ++ fmt ++ "]", args);
            return;
        };
        const module = info.getModuleForAddress(address) catch {
            std.debug.print("[" ++ fmt ++ "]", args);
            return;
        };

        const symbols = module.getSymbolAtAddress(info.allocator, address) catch {
            std.debug.print("[" ++ fmt ++ "]", args);
            return;
        };

        if (symbols.source_location) |loc| {
            std.debug.print("{s}::[{s}:{d}:{d}]", .{ loc.file_name, symbols.name, loc.line, loc.column });
        } else {
            const s = @src();
            std.debug.print("{s}::[{s}:???:???]", .{ s.file, std.fs.path.basename(s.file) });
        }

        std.debug.print("[" ++ fmt ++ "]\n", args);
    }
}

Doing what @vulpesx suggested seems simpler:

const std = @import("std");
const log = std.log.scoped(.ok);

const src_log = "\n   Module: {[module]s}\n   File:   {[file]s}\n   Fn:     {[fn_name]s}\n   Column  {[column]d:<5}\n   Row     {[line]d:<5}";

fn dummy() void {
    log.warn(src_log, @src());
}

pub fn main() !void {
    dummy();
}

Also note that with tuples or the original struct, the column and row end up being wrong, because they are printed in the opposite order they are declared in.
With using field name specifiers in the format string it is much harder to mess that up.

2 Likes

You should use a multi-line string literal here.

2 Likes