Recommendations
Naming conventions
All of the following conventions are based on the Zig Style Guide.
- Interface names start with
I
followed 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
self
pointer of an inheritable method is namedthis
, while theself
pointer 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
IBase
declares an interface method, or virtual functiongetName()
- The base class
Base
implements theIBase
interface and provides an inheritable but non-virtual method_print()
, which obtains the real name through the virtual methodgetName()
and prints it. Note thatBase
itself does not provide agetName()
method, soBase
here is equivalent to the abstract base class in the OOP concept. - Subclasses
SubOne
andSubTwo
both implement their owngetName()
interface method, and callBase.init()
in their owninit()
methods, so they both save their own interfaces converted toIBase
in their ownBase.iface
fields.
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.