Discovering public functions via reflection

here’s a simple mod.zig file that exposes a single function:

pub fn foo(x: u32) void {
    // body
}

i then import mod.zig and use @typeInfo to discover more…

const Mod = @import("mod.zig");

const ti = @typeInfo(Mod);

// print ti.Struct.decls

all i can discover is that there is a std.builtin.Type.Declaration whose name is “foo”…

what i want is to learn more about “foo” as a std.builtin.Type.Fn, where i can reflect on this function’s type signature…

what’s the path to discovering this information???

1 Like

One thing you can try (if you’re just looking for a single function) is to use @hasDecl: Documentation - The Zig Programming Language

Once you find the declaration, you can reflect on the type information using a guard statement:

const MyType = struct {
    pub fn foo(x: i32, y: i32) i32 {
        return x + y;
    }
};

export fn foo() i32 {

    if (comptime @hasDecl(MyType, "foo")) {
        switch (@typeInfo(@TypeOf(MyType.foo))) {
            .Fn => {
                return MyType.foo(1, 2);
            },
            else => @compileError("foo isn't a function"),
        }
    } else {
        @compileError("No foo to be found");
    }
}

Edited: @field works on functions too - not just struct fields.

If you want to, you can bind your functions to comptime fields and then iterate through them using the struct fields. Here’s an example: How to iterate over struct declarations? - #3 by AndrewCodeDev

With the comptime fields approach, you can see all of your function fields and inspect them like normal. Hope that helps!

1 Like

this should work for me!!! and i can see a path to validate whether a given struct in fact implements all the functions of some prescribed abstract interface – in effect, ensuring that i am looking at a “duck”…

presumably all these checks can be performed at comptime ???

I actually stand corrected - I don’t know if this is true or not, but it looks like the docs say this can work for declarations too?

Works on both fields and declarations.

Hey, looks like it does!

return @field(MyType, "foo")(1, 2);

And yes, this can all be done at comptime. Here’s how you get the declaration names out of a struct:

const decls = @typeInfo(MyType).Struct.decls;

And you can iterate over them and check the type returned by @field for the decl name.

For completeness, if you want to obtain a list of all public function declarations of a container type (struct, enum, union or opaque), you can use something like the following:

const std = @import("std");

const FnDecl = struct {
    name: []const u8,
    info: std.builtin.Type.Fn,
};

fn fnDecls(T: type) []const FnDecl {
    var fn_decls: []const FnDecl = &.{};
    const decls: []const std.builtin.Type.Declaration = switch (@typeInfo(T)) {
        inline .Struct, .Enum, .Union, .Opaque => |container_info| container_info.decls,
        inline else => |_, tag| @compileError("expected container, found '" ++ @tagName(tag) ++ "'"),
    };
    for (decls) |decl| {
        switch (@typeInfo(@TypeOf(@field(T, decl.name)))) {
            .Fn => |fn_info| {
                fn_decls = fn_decls ++ .{.{ .name = decl.name, .info = fn_info }};
            },
            else => {},
        }
    }
    return fn_decls;
}

pub fn main() void {
    inline for (fnDecls(std)) |fn_decl| {
        std.debug.print("{s} {?}\n", .{ fn_decl.name, fn_decl.info.return_type });
    }
}

std.builtin.Type.Declaration is very sparse in information and only returns the name of the declaration, so to actually get its type and see if it’s a field we need to access that declaration and reflect its type directly. This can be accomplished using @field, which given a value and a name evaluates to the result of the expression value.<name>, i.e. it dynamically accesses a member. Despite what its name suggests, it works for declarations as well (it should really be renamed @access).

2 Likes

Agreed on the renaming. Question for ya, @castholm, how does this play with public and private declarations? If decls returns all declarations, could you end up trying to check declarations that are not public? If so, is there any-way around that? Part of why I like the struct field approach is that the fields are public for a given instance.

This is a matter of opinion and style so take it for what it is, but it might be worth mentioning that at least in the standard library, the current philosophy seems to be that functions should not explicitly check that arguments passed to duck-typed anytype parameters certain constraints by reflecting their fields/declarations and checking that they implement some interface. Instead, you should briefly document the interface and any invariants that need to hold in a doc comment and let the compile errors guide the user toward the correct usage.

In other words, prefer

/// `duck` is expected to expose `pub fn say(self: ?, sound: []const u8) void`
fn poke(duck: anytype) void {
    duck.say("quack");
}

over

fn poke(duck: anytype) void {
    if (!(@hasDecl(duck, "say") and @typeInfo(@TypeOf(duck.say)) == .Fn)) {
        @compileError("'duck' must provide a 'say' method");
    }
    duck.say("quack");
}
3 Likes

For @typeInfo, only pub decls are returned, regardless of whether the code that calls it can access it or not (remember, you can access any non-public declarations from anywhere in the same file). But for @field, both public and private declarations work. If foo.bar is legal in a given context, @field(foo, "bar") will also be legal.

pub const aaa = struct {
    pub const bbb: usize = 123;
    const ccc: usize = 456;
};

pub fn main() void {
    // This will only log `bbb`, not `ccc`.
    @compileLog(@typeInfo(aaa).Struct.decls);

    // But this is legal because the access expression `aaa.ccc` is legal.
    @compileLog(@field(aaa, "ccc"));
}

And no, there’s no way around this. Private decls are private and inaccessible outside of the file they are defined in.

1 Like

Nice - thanks!