Call Method by interface

Hi,

I am stuck at something that I find very difficult to do in Zig (for good and obvious reasons), which is elementary in other languages.

Java/C# type Pseudo code

interface QuitStrategy {
  bool execute();
}

class IterationQuitStrategy {
  @override bool execute() {
    return (cycles > 10);
  }
}

class MyExecutor {
  @Getter @Setter private QuitStrategy strategy;

  public void run() {
    if (strategy.execute()) {
      setTerminate(true);
    }
  }
}

There are two derivations to this:

  1. Having struct and method references so that, at compile time, the method, signature and instance of the struct is known. (second prize, but still no totally sure how to do… the source Thread.spawnThread talks to it somewhat).
  2. The more difficult one (I believe), to keep a reference to the strategy instance and the method pointers… as to invoke it by its implied signature with the instance as a parameter. This in my mind will make the it not comptime known, which voids optimization BUT it gives one the option to change the behaviour at runtime (e.g. by configuration).

I have looked for answers, but the threads I found does not talk to method of a struct as a member of a struct, callable by methods of the container struct.

1 Like

For the second option, you should look at how std.mem.Allocator, and the allocators in std.heap are implemented. In your case, it would look something like this:

const QuitStrategy = struct {
    ptr: *anyopaque,
    vtable: *const VTable,

    pub fn execute(self: QuitStrategy) bool {
        return self.vtable.execute(self.ptr);
    }

    const VTable = struct {
        execute: *const fn execute(*anyopaque) bool;
    };
};

const IterationQuitStrategy = struct {
    cycles: u32,

    pub fn execute(ctx: *anyopaque) bool {
        const self: *IterationQuitStrategy = @ptrCast(@alignCast(ctx));
        return (self.cycles > 10);
    }

    pub fn quitStrategy(self: *IterationQuitStrategy) QuitStrategy {
        return .{
            .ptr = self,
            .vtable = &.{
                .execute = execute,
            };
        }
    }
};


// ...

pub fn run(strategy: QuitStrategy) {
    if (strategy.execute()) {
        setTerminate(true);
    }
}
3 Likes

this post shows 5 ways to do interfaces in zig, most if not all are used in the standard library

3 Likes

Thank you @joed, I have worked through and tested your example in my code (with some really minor syntax fixes). I understand the pattern, and can definitely see myself enjoying and continue to follow it. As a Zig newcomer there is however definitely one or two other things I have to research the behaviour of, that is still missing from my vocabulary (anyopaque and alignCast… more specifically why).

Thank you @vulpesx, in having a quick look at the link you sent, I get a sense of deja vu that I did see it at some point already. Quite like joed’s which, very similar if not the same as “interface2”. But I promise to, when time permit, to have a look at the other ways of doing it by testing it, and not just reading it (which is deceptive as you think you understand but don’t necessarily).

Is this really safe?

Returning a pointer to a local expression seems scary. I guess it works because it is constant and therefore isn’t put on the stack, but is this assured/checked somewhere?

It should be. comptime data isn’t put on the stack. std.heap.ArenaAllocator also does this:

    pub fn allocator(self: *ArenaAllocator) Allocator {
        return .{
            .ptr = self,
            .vtable = &.{
                .alloc = alloc,
                .resize = resize,
                .remap = remap,
                .free = free,
            },
        };
    }

Also see this example on compiler explorer.

1 Like

I am using @joed sample by its baseline in a multi-threaded environment successfully; without allocators of any kind… everything stack based. I do however make very sure that all of the “interface instances” remain in scope for the duration of the threads… (obviously)

the expression is a literal so its comptime known and will be in static memory

@joed - thanks again for this. I have tested it and the concept works great. In however trying to expand on it and make it a little more “bang for the buck” I am trying to generify the setting of the event handlers.

pub const ServiceEventHandler = struct {
    ptr: *anyopaque,
    vtable: *const VTable,

    const VTable = struct {
        onHandle: *const fn (*anyopaque, Type, *anyopaque) void,
    };

    pub const Type = enum { SocketOpen, SocketClose };

    pub fn onHandle(
        self: ServiceEventHandler,
        tp: ServiceEventHandler.Type,
        sender: *anyopaque,
    ) void {
        self.vtable.onHandle(self.ptr, tp, sender);
    }
};

pub const KeepAliveEvents = struct {
    pub fn onClose(
        _: *anyopaque,
        tp: ServiceEventHandler.Type,
        sender: *anyopaque,
    ) void {
        // const self : *OnOpenHandler = @ptrCast(@alignCast(ctx));

        const other: *ClientKeepAliveService = @ptrCast(@alignCast(sender));
        print("Type {}\n", .{tp});

        print("Socket CLOSED!!!!!!!\n", .{});
    }

    pub fn onEvent(
        self: *KeepAliveEvents,
        handler: *const fn (*anyopaque, ServiceEventHandler.Type, *anyopaque) void,
    ) ServiceEventHandler {
        return .{
            .ptr = self,
            .vtable = &.{
                .onHandle = handler
            },
        };
    }
}

When I

    // alive is the object that raises the events as assigned to its delegate members
    var handlers = KeepAliveEvents{};
    alive.setQuitStrategy(aliveQs.quitStratefy());
    alive.onOpen = handlers.onEvent(KeepAliveEvents.onOpen);
    alive.onClose = handlers.onEvent(KeepAliveEvents.onClose);

I get

Using SecondsSegmentation fault at address 0x0

If I however use the code as per your suggestion (by setting the VTable from inside the struct) then it works. In my mind it is the same, as the self is the same, and the pointer to the method is also supposed to be the same, which are the only two parameters to the VTable?

In onEvent…, if I simply ignore the handler (_) parameter and pass in onClose it works. The compiler has no issue in me passing the pointer to the function, but it segment faults it. I have a suspicion it is my lack of knowledge regarding the meaning of @alignCast or similar other pointer operations.

Now you changed it from a constant to a dynamic expression and boom, it blows up just as @dpirch warned against. I do believe if you made the function comptime (you might have to throw in an anytype on top) and explicitly made a

const vtable : VTable = .{ .onHandle = handler };

that would end up being a proper constant built into the binary rather than a stack object and therefor it would work! But honestly I find this kind of stack vs data segment schenanigans very dangerous and error prone. You definitely need to leave a warning comment in there describing the importance of the expression being constant at compile time. Which imo is a clear sign not to do it.

Thank you. Still assessing your reply. Sadly I assumed that since it is making me jump though a “1000 safety” hoops that it would compile-time fail. But again, it is a case of a what I don’t know. What I do know is that something seemingly simple is going to turn out to be more complicated than I could/would give it credit for.

I have read the link provided regarding the other methods. This one resonates with me.

Think I am going to give interface 5 a try as per the link… Thanks again for the response.

This lands solidly into an existing debate. There are multiple ways to fix this in the language, but the word we have from the creators is that they want the language to be simple (as opposed to it’s usage being simple).

One suggestion is that we add a check for pointers to out of scope stack addresses, which was suggested at function level, but which your example shows it must be at every scope level.

Another suggestion is an interface keyword where the comptime constant would be enforced so the compiler would guide you to success. This comptime constant would also allow the compiler to optimize it so that the vtable cost went away.

We’ll just have to wait and see where this lands, but rest assured you’re not the only one who feels the current status quo is not ideal.

1 Like

@joed - I am too new to Zig, but I definitely get the gist in what you are saying. I am going to avoid leaving an opinion on Zig (which for the bigger part I think is doing a whole lot really really well and that I can totally agree with), but I also don’t want to spend days and weeks in getting hung up on “basics” which should be part of the language itself… or clearly defined at least.

In other words, in doing something obvious I should not be required to have years of Zig experience… (and write and rewrite reams of code)

PS: I see interface 5 also uses comptime… So that won’t work nicely either.

Thanks, it was very useful.

IMHO, it would be even more useful if you describe whats is good and bad for each approach.

here is the link again: Code study: interface idioms/patterns in zig standard libraries - Zig NEWS

tagged union

you retain type information and safety.
The user can specialise using that information.
It does add branches, but they should be able to be optimised into a look-up table.
Depending on alignment, the tag may add significant size to the data. there are ways around that

dynamic dispatch (vtable)

data size is just 2 pointers.
2 levels of indirection can affect performance
no type information, unless the interface offers a function to get that info.

dynamic dispatch (inline)

size of data pointer + pointers for each function
1 level of indirection can affect performance, slightly better than vtable
no type information, unless the interface offers a function to get that info.

dynamic dispatch (fieldParentPtr)

data ptr is calculated from vtable pointer
1/2 levels of indirection can affect performance
no type information unless the interface provides it.
@fieldParentPtr can be used wrong and lead to weird errors.

generic interface

performance and data size are identicle to using the types directly.
Keep type information and safety
zig doesn’t have, and may never have, a way of describing generic interfaces.
You have to read/write documentation to describe the interface.
Or read the source code, which is fine in simple cases.

1 Like

Thanks for the quick reply!

Just out of curiosity: is the reason why Allocator and Random use different interface implementations the fact that Allocator has many virtual methods and Randon has only one?

I have done a C like implementation where I simply carry 2 usize pointers.

The one has the structure address. The other has the function address. The structure address never knows its type as it is simply assumed to be the first pointer to the function called, where the function called signature is assumed (for now anyway).

I don’ think this is similar to “dynamic dispatch” as there is no indirection… it is a single function call.

I am sure there are a 1000 things wrong with this thinking pattern for Zig, but it is ugly, but it works… and conceptually if the function pointer remains the same throughout the lifetime of the app, then it should work (just no safety)

Dynamic dispatch is exactly that, you have a pointer that points to arbitrary data, and you have pointers to functions with known signature that take the data pointer.

Often the function pointers are in a struct, which you then have a pointer too, that’s a vtable.

But with one function that’s unnecessary, so you keep the function pointer inline, that’s inline dynamic dispatch

calling a function via a pointer is indirect

1 Like

Understood - thanks. However, with the VTable I was running into the comptime issues:

    var s = MyStructA {};

    // etc...
    if (s.b > 200) {
        callback(@intFromPtr(&s), @intFromPtr(&MyStructA.print));
    } else {
        callback(@intFromPtr(&s), @intFromPtr(&MyStructA.printB));
    }

pub fn callback(object : usize, function: usize) void {
    const fn_ptr : (*const fn(*anyopaque) void) = @ptrFromInt(function);
    std.debug.print("\n{}", .{fn_ptr});
    std.debug.print("\n{}", .{function});

    fn_ptr(@ptrFromInt(object));
}

Primitive… ugly and all the other things… but working