Automated Function Tracing

Hi! In my C and Python code, sometimes I want to better track what data goes where when I don’t have a debugger attached. Take the following examples:

import inspect

def trace(fn):
    def wrapper(*args, **kwargs):
        _trace(fn)
        fn(*args, **kwargs)
    return wrapper

def _trace(fn):
    sig = inspect.signature(fn)

    params = []
    for name, param in sig.parameters.items():

        p = f"{name}"
        if param.annotation != inspect.Parameter.empty:
            p += f": {param.annotation.__name__}"
        if param.default != inspect.Parameter.empty:
            p += f" = {param.default}"
        params.append(p)

    print(f"{fn.__name__}({', '.join(params)}) -> {sig.return_annotation.__name__}")

@trace
def example(a: int, b, c: str = "hello") -> bool:
    return True

example(1, 2)

Output: example(a: int, b, c: str = hello) -> bool

#include <stdio.h>
#define LOG_TRACE(fmt, ...) printf(fmt "\n", __VA_ARGS__);

int someFunc(void *foo_ptr, void *bar_ptr)
{
    LOG_TRACE("%s(%p, %p)", __FUNCTION__, foo_ptr, bar_ptr);
    // ...
    return 0;
}

int main(void)
{
    someFunc((void *)0xdeadbeef, (void *)0xcafebabe);
    return 0;
}

Output: someFunc(0xdeadbeef, 0xcafebabe)

(I realize my quick Python example doesn’t actually print out the values I passed in [which is the goal here], but you get the gist of what I’m looking for).

I’m not sure if this is even possible in Zig… I tried looking through the source code for similar code, and using @src(), but nothing seems close to the examples above.

Does anyone have a function or clue that would point me in the right direction for this? Ultimately, I’m looking for something along these lines:

inline fn trace() void {
    // Some magic here...
}

const Point = struct {
    x: u32,
    y: u32,

    fn add(self: *@This(), scalar: u32) void {
        self.*.x += scalar;
        self.*.y += scalar;
    }
};

fn foo(a: u64, b: Point) !u32 {
    trace();
    // ...
}

pub fn main() !void {
    try foo(4, .{ .x = 4, .y = 9});
}

Output: foo(a: u64 = 4, b: Point = .{ .x = 4, .y = 9, fn add(...)})

@errorReturnTrace() might be useful to you?

Answered too quickly though. There are a few stack trace functions in std.debug. std.debug.dumpCurrentStackTrace may be what you are looking for

1 Like

tracy

Maybe look into using tracy, it allows you to interact with live-updating tracing profiles mark/time sections of code and log messages, I haven’t yet (but it is one of the tools I want to add eventually) and it seems like it may involve some work to get it running, but if you can get it running, I think it could be helpful for that and a lot more.

With tracy you would have to pass the data to be recorded manually, instead of magically.

zigft

I think you could use GitHub - chung-leong/zigft: Zig function transform library to create wrapper functions that replace the original functions but add logging zigft - adding-debug-output-to-a-function

scripted debugger

Another approach might be to run the program via a debugger and script it to set a breakpoint, inspect and print the variables and then continue, I think somebody in this forum wrote a post about something like that, but can’t find it right now.

in the future maybe via build system or compiler protocol?

I think it might be nice if Zig eventually had a way to automatically add tracing to some functions via some kind of build-system api or maybe by requesting it by communicating with the compiler while incremental reload is enabled.

DynamoRIO

Haven’t tested it, but it seems interesting and useful:

DynamoRIO is a system for runtime code manipulation that is efficient, transparent, and comprehensive, able to observe and manipulate every executed instruction in an unmodified application running on a stock operating system and commodity hardware.

This page explains it quite well DynamoRIO System Overview.
If somebody finds the time to try it out, let us know how it worked out!

1 Like