On Static Dispatch vs Dynamic Dispatch

I have found an interface design pattern where static dispatch can coexist with dynamic dispatch.

I can define a trait:

pub const Trait = struct {
    pub fn method(self: *Trait) void {
        _ = self;
        @compileError("");
    }
};

Trait is usually used to describe the contract of a static dispatch interface. I can use some tools to check whether a certain type implements a certain trait.

I also can define an interface:

pub const Interface = struct {
    vptr: *anyopaque,
    vtable: struct {
        method: fn(self: *anyopaque) void,
    },
};

The interface contains virtual pointers and virtual tables, which can be replaced at runtime.

There is no difference between the types that implement this interface. I can write a unified packaging for them:

pub fn Warpper(comptime T: type) type {
    const dispath_dyn = T == Interface;
    return struct {
        const Self = @This();
        const Impl = if (dispath_dyn) T else *T;
        impl: Impl,

        pub fn from(impl: Impl) Self {
            return .{ .impl = impl };
        }

        pub inline fn method(self: Self) void {
            if (comptime dispath_dyn) {
                self.impl.vtable.method(self.impl.vptr);
            } else {
                @call(.always_inline, T.method, .{self.impl});
            }
        }
    };
}

In this way, the caller can use Warpper(Interface) to adopt a dynamic dispatch version, or use Warpper(T) to adopt a static dispatch version.

For the function parameters that receive this interface, using anytype is sufficient:

pub fn func(warpper: antype) void {
    // ...
}

I rewrote the allocator interface of the standard library in this way in vftrait. I also tested it, and the results showed that the implementation performance of dynamic dispatch was almost the same as that of the standard library, while the performance of the static dispatch version was higher than that of dynamic dispatch.

1 Like