Copy a struct type wrapping its pub functions

fn WrappedStruct(comptime in_type: type) type {
    const type_info = @typeInfo(in_type);
    if (type_info != .Struct) {
        @compileError("expected struct argument, found " ++ @typeName(in_type));
    }

    const struct_info = type_info.Struct;
    const fields_info = struct_info.fields;

    comptime var fields: [struct_info.decls.len + fields_info.len]std.builtin.Type.StructField = undefined;
    comptime var fields_idx = 0;

    inline for (fields_info) |field| {
        fields[fields_idx] = field;
        fields_idx += 1;
    }

    inline for (struct_info.decls) |decl| {
        const decl_as_field = @field(in_type, decl.name);
        const decl_type = @TypeOf(decl_as_field);
        const decl_type_info = @typeInfo(decl_type);
        // convert all the pub declaration into comptime field?
        fields[fields_idx] = .{
            .name = decl.name,
            .type = decl_type,
            .default_value = &decl_as_field,
            .is_comptime = true,
            .alignment = 0,
        };
        if (decl_type_info == .Fn) {
            fields[fields_idx].decl_as_field = ;// generate new body?
        }
        fields_idx += 1;
    }

    return @Type(.{
        .Struct = .{ .layout = .Auto, .fields = fields[0..], .decls = &.{}, .is_tuple = false, .backing_integer = null },
    });
}

fn foo_wrapper() void {
    std.debug.print("was called in between\n", .{});
}

const SomeStruct = struct {
    pub fn foo(_: @This()) void {
        std.debug.print("Foo\n", .{});
    }
}

const WrappedStruct = Wrapped(SomeStruct);

the goal is to somehow create an identical struct of the given struct with all it’s Type Declaration and other stuff and also to call this foo_wrapper function before calling any other public function of the given struct type.

@AndrewCodeDev has given some headstarts in my help request post.

An example by @AndrewCodeDev where a function that returns void and take a tuple as an argument but is not part of any Struct type namespace:

const std = @import("std");

const builtin = @import("builtin");

fn FunctionWrapper(func: anytype) type {
    return struct {
        // anytype makes it a member function
        pub fn call(args: anytype) void {
            std.debug.print("\nHello, Wrapper!\n", .{});
            @call(.always_inline, func, args);
        }  
    };
}

fn WrapperStruct(func: anytype) type {
    comptime var fields: [1]std.builtin.Type.StructField = undefined;
    const wrapped_call = FunctionWrapper(func).call;

    fields[0] = .{
        .name = "whatever",
        .type = @TypeOf(wrapped_call),
        .default_value = wrapped_call, 
        .is_comptime = true,
        .alignment = 0,
    };

    return @Type(.{
        .Struct = .{
            .layout = .Auto,
            .fields = fields[0..],
            .decls = &.{},
            .is_tuple = false,
            .backing_integer = null
        },
    });
}

pub fn foo() void {
    std.debug.print("\nGoodbye, Wrapper!\n", .{});
}

pub fn main() !void {
    const wrapped = WrapperStruct(foo){ };
    wrapped.whatever(.{});
}

Apologies in advance because you’re not going to like this answer, but there’s a reason that it’s difficult to express this in Zig. I intentionally designed the language so that this kind of thing would be discouraged. So you’re fighting an uphill battle to do this. I don’t like working with codebases that do this kind of thing because I find it difficult to track down where things are implemented. I think often the solution is to refactor, rework, and generally move code around instead of doing this kind of thing.

That said, don’t let me get in the way of your fun.

9 Likes

I had a feeling that this decision was by design when I started looking at what the options are, lol.

Generating wrappers can be helpful, but they also can become an absolute nightmare. For instance, it’s nice in python that you can basically wrap anything. Then again, I’ve read several large python codebases where it’s like “wrapper… wrapper… wrapper… does any of this code actually do something??”

btw if zig ever tries to support C’s #define V_ARGS_MACRO(...) some_fn(__VA_ARGS__) eventually has to introduce something for that right? or will just support it using @cVa..? but that feels like it will add extra overhead, but again still feels unrelated to this use case.

The closests thing I could came up is this:

const std = @import("std");

fn FunctionWrapper(comptime func: anytype, comptime in_type: type) type {
    const fn_type = @TypeOf(func);
    const fn_info = @typeInfo(fn_type).Fn;
    const fn_ret_type = fn_info.return_type orelse @compileError("Missing return type");
    const fn_conv = fn_info.calling_convention;

    const to_funcs_no_instance = struct {
        fn @"1"() type {
            return struct {
                instance: *in_type = undefined,
                fn call(self: @This(), arg0: anytype) callconv(fn_conv) fn_ret_type {
                    _ = self;
                    return @call(.always_inline, func, .{arg0});
                }
            };
        }
        // ...
    };

    const to_funcs_instance = struct {
        fn @"0"() type {
            return struct {
                instance: *in_type = undefined,
                fn call(self: @This()) callconv(fn_conv) fn_ret_type {
                    // TODO: we can check if the `instance` needs be pointer or value
                    return @call(.always_inline, func, .{self.instance.*});
                }
            };
        }
        fn @"1"() type {
            return struct {
                instance: *in_type = undefined,
                fn call(self: @This(), arg0: anytype) callconv(fn_conv) fn_ret_type {
                    // TODO: we can check if the `instance` needs be pointer or value
                    return @call(.always_inline, func, .{ self.instance.*, arg0 });
                }
            };
        }
        // ...
    };

    // ... manually copy paste probably not the actual issue? also this approach will fail if an argument type has to be `comptime`
    // we could try to use va args, but it's still probably not going to work for `anytype` params

    const to_funcs = if (fn_info.params.len > 0 and fn_info.params[0].type == in_type) to_funcs_instance else to_funcs_no_instance;
    const to_fn_name = if (to_funcs == to_funcs_instance) std.fmt.comptimePrint("{d}", .{fn_info.params.len - 1}) else std.fmt.comptimePrint("{d}", .{fn_info.params.len});
    if (!@hasDecl(to_funcs, to_fn_name)) {
        @compileError("Couldn't find converter with " ++ to_fn_name ++ " arguments");
    }

    const to_fn = @field(to_funcs, to_fn_name);
    return to_fn();
}

fn WrapperStruct(comptime in_type: type) type {
    const type_info = @typeInfo(in_type);
    if (type_info != .Struct) {
        @compileError("expected struct argument, found " ++ @typeName(in_type));
    }

    const struct_info = type_info.Struct;

    comptime var fields: [struct_info.decls.len + 1]std.builtin.Type.StructField = undefined;
    fields[0] = .{
        .name = "instance",
        .type = in_type,
        .default_value = null,
        .is_comptime = false,
        .alignment = 0,
    };
    comptime var fields_idx = 1;

    inline for (struct_info.decls) |decl| {
        const decl_as_field = @field(in_type, decl.name);
        const decl_type = @TypeOf(decl_as_field);
        const decl_type_info = @typeInfo(decl_type);

        fields[fields_idx] = .{
            .name = decl.name,
            .type = decl_type,
            .default_value = &decl_as_field,
            .is_comptime = true,
            .alignment = 0,
        };
        if (decl_type_info == .Fn) {
            const wrapped = FunctionWrapper(decl_as_field, in_type){};
            fields[fields_idx].type = @TypeOf(wrapped);
            fields[fields_idx].default_value = &wrapped;
        }
        fields_idx += 1;
    }
    return @Type(.{
        .Struct = .{ .layout = .Auto, .fields = fields[0..], .decls = &.{}, .is_tuple = false, .backing_integer = null },
    });
}

fn wrapperStruct(instance: anytype) WrapperStruct(@TypeOf(instance)) {
    var wrapped = WrapperStruct(@TypeOf(instance)){ .instance = instance };

    const s_type = @TypeOf(wrapped);
    const fields = @typeInfo(s_type).Struct.fields;

    inline for (fields) |field| {
        if (@typeInfo(field.type) == .Struct) {
            var w_field = @field(wrapped, field.name);
            if (@hasDecl(@TypeOf(w_field), "call")) {
                @field(w_field, "instance") = &wrapped.instance;
            }
        }
    }

    return wrapped;
}

const SomeStruct = struct {
    pub fn foo(s: u32) void {
        std.debug.print("Foo: {}\n", .{s});
    }

    pub fn bar(self: @This()) void {
        _ = self;
        std.debug.print("Bar\n", .{});
    }
};

pub fn main() !void {
    const instance = SomeStruct{};
    const wrapped = wrapperStruct(instance);
    wrapped.foo.call(5);
    wrapped.bar.call();
}

Obvious issues:

  • Has to manually define different kinds of function signature, this is may be possible to hack around. Not sure if va args would help but I think it would not since anytype params would’ve caused issues.
  • Cannot access any public declerations directly from the type. Some use case could be:
const SomeStruct = struct {
   pub const SomeErrorType = error{SomeError};
   // decls..
};
// cannot do
WrapperFunction(SomeStruct).SomeErrorType;
// can do by creating an instance which is definately not ideal
WrapperFunction(SomeStruct){}.SomeErrorType;

The main issue:

  • Cannot find a way to access @This() or self like the traditional way inside the generated wrapper functions thus had to resort with something like this wrapped.foo.call(..).

Well that’s about enough fun for today, we’re probably trying to achieve something impossible(using only zig’s comptime features) as andrewrk indirectly stated. But may be in future we could abuse some new features of the language to achieve it and also for the future users.

This is a topic that does something similar, at least the function part:

Hello, I understand that something like this might be needed, but… but 5 months later, when it’s time to delve back into the code, ouch, ouch… especially if it’s only used for one application.

2 Likes

well the ideal way to do this would be something like this

pub fn WrapperStruct(comptime in_type: type) type {
	return struct {
		instance: in_type;
		pub usingnamespace in_type;
		pub inline fn wrapped_value(self: @This()) in_type {
			// does wrapper stuff...
			return self.in_type;
		}
		pub inline fn wrapped_ptr(self: *@This()) *in_type {
			// does wrapper stuff...
			return &self.in_type;
		}
	}
}

var wrapped = WrappedStruct(SomeStruct) { .instance = SomeStruct{} };
wrapped.wrapped_value().foo();
wrapped.wrapped_ptr().bar();

so this easily tells someone what we’re really doing or trying to achieve so they can directly checkout those wrapped_* functions. nothing wrong with this approach imo but it’s kinda not fun :").

and i think it wouldn’t be as bad as python’s way of doing wrapper coz u have to be specific about this wrapper function of yours and i think it would be obvious and openly visible where this function might be coming from the way we use zig to import other files, but again if we do ‘wrapper… wrapper… wrapper’ that is really a bad design decision at that point if we’re doing that in zig.

1 Like

Updating a contact (first name last name), add update. Because “foo” always works… This would also give me a real idea of how to approach…

ahh yes i forgot this is the reason why i actually was not satisfied with wrapped.wrapped_value().foo()

1 Like

Instead of a whole bunch of wrapper stuff you can just write a freestanding generic function taking anytype, switching on the type and do whatever.
That way, you still can do essentially the same thing, just that it isn’t wrapped in an instance of a new type.

Why do you really want this?
Why not just define in_type differently?
You even could use pub usingnamespace my_generic_methods(@This()) to add a bunch of methods to in_type directly, instead of creating in_type without what you want, just to add it later.

I think topics like this, should show some actually realistic usage scenarios, not this pseudo code stuff, where nobody can provide an alternative, because the code doesn’t do anything.

It is just a mystic amorphous thing, that boils down to “I want wrappers” but nobody can argue “you don’t need wrappers for this” because you don’t show what “this” is.

To me this seems like another case of the xy problem, you ask for wrappers because you like wrappers, but what is your actual problem, that made you think “I want to use wrappers for this”?

4 Likes

I think at this point I should’ve mentioned like I am not really trying to solve any problem here any more, just trying to see what it would take to do something like this in Zig. That’s why moved to brainstorming category.

So say we’re trying to use some sort of (not our own) parser to parse by reading over socket fd and it might return WouldBlock error while parsing step by step so at that point we probably want to save the parser’s state or the things it have parsed correctly. But the original parser doesn’t expose some kind of function… But at this point we might just have to reimplement the the parser from scratch… So this was the thought which lead to this.

And again just trying to explore zig’s comptime capabilities at this point.

1 Like

A little off-topic thank you note here. I’ve been searching for years for a good way to express this situation and had never heard of the xy problem. Thanks for linking this.

1 Like

Yeah, my idea only takes care of the function part. You can automatically create the wrapper functions You cannot, however, automatically create a struct with these functions attached as @Type() doesn’t not accept decls currently. As far as I know #6709 is still on the development roadmap.

1 Like