Recommendations
Naming conventions
All of the following conventions are based on the Zig Style Guide.
- Interface names start with
Ifollowed by camel case. For example,ISomeInterface - Among the inheritable methods, the method names used to implement the interface are the same as the Zig Style Guide, and those that are not interface methods start with
_. For example,_notInterfaceMethod - The default implementation function of the interface, starting with
_. For example,_getMsg - The
selfpointer of an inheritable method is namedthis, while theselfpointer of a non-inheritable method is still calledself. The reason for this is that in an inheritable method, when you need to access the data and methods of the class you are in, you can still name the variable that is statically converted to the class you are in asself, such as:
const Self = @This();
pub fn Fn(comptime T: type) type {
return zoop.Method(.{
struct {
pub fn inheritableFunc(this: *T) void {
var self = this.cast(Self);
...
}
}
});
}Performance Optimization
In zoop, virtual functions can only be called through interfaces, and converting objects to interfaces often requires dynamic conversion, which has overhead. To address this problem, the recommended solution is to save an interface containing all virtual functions in the most basic class inherited by the class. The following example illustrates this:
pub const IBase = struct {
pub const Vtable = zoop.DefVtable(@This(), struct {
getName: *const fn (*anyopaque) []const u8 = &_getName,
});
pub usingnamespace zoop.Api(@This());
ptr: *anyopaque,
vptr: *Vtable,
fn _getName(_:*anyopaque) []const u8 {
@panic("Not implemented.");
}
pub fn Api(comptime I: type) type {
return struct {
pub fn getName(self: I) []const u8 {
return self.vptr.getName(self.ptr);
}
};
}
}
pub const Base = struct {
pub const extends = .{IBase};
pub usingnamespace zoop.Fn(@This());
mixin:zoop.Mixin(@This());
iface: IBase,
pub fn init(self: *Base) void {
self.iface = self.as(IBase).?;
}
pub fn Fn(comptime T: type) type {
return zoop.Method(.{
struct {
pub fn _print(this: *T) void {
const iface = this.cast(Base).iface;
std.debug.print("My name is:{s}\n", .{iface.getName()});
}
},
});
}
}
pub const SubOne = struct {
pub const extends = .{Base};
pub usingnamespace zoop.Fn(@This());
mixin: zoop.Mixin(@This()),
pub fn init(self: *SubOne) void {
self.cast(Base).init();
}
pub fn Fn(comptime T: type) type {
return zoop.Method(.{
struct {
pub fn getName(this: *T) []const u8 {
return "SubOne";
}
},
});
}
}
pub const SubTwo = struct {
pub const extends = .{Base};
pub usingnamespace zoop.Fn(@This());
mixin: zoop.Mixin(@This()),
pub fn init(self: *SubTwo) void {
self.cast(Base).init();
}
pub fn Fn(comptime T: type) type {
return zoop.Method(.{
struct {
pub fn getName(this: *T) []const u8 {
return "SubTwo";
}
},
});
}
}Description
- The base interface
IBasedeclares an interface method, or virtual functiongetName() - The base class
Baseimplements theIBaseinterface and provides an inheritable but non-virtual method_print(), which obtains the real name through the virtual methodgetName()and prints it. Note thatBaseitself does not provide agetName()method, soBasehere is equivalent to the abstract base class in the OOP concept. - Subclasses
SubOneandSubTwoboth implement their owngetName()interface method, and callBase.init()in their owninit()methods, so they both save their own interfaces converted toIBasein their ownBase.ifacefields.
Therefore, when we call the _print() method on the SubOne and SubTwo objects (this method is inherited from Base), the implementation of _print() directly uses Base.iface to perform the virtual function call of getName(), which saves the dynamic conversion overhead. With this method, each object only needs to be dynamically converted once when it is initialized. Thereafter, the use of the object will no longer incur this overhead, thus greatly reducing the overhead of virtual function calls.
