Terminology and Principles
Term
fatptr: a zig struct containing two pointers, one named ptr pointing to data, and one named vptr pointing to a list of functionsrootptr: A pointer that always points to the original object during interface and type conversionClass: a zig struct that conforms to the Class specification defined byzoopMethod: A function belonging toClassthat can be inherited, overridden, and used to implementInterfaceApiMethod: can be inherited byInterface, a glue function dedicated to accessing interface functions inVtable, belonging toMethodofInterfaceVtable: contains the data type of allMethodpointers of theClassto which the interface belongsInterface: afatptrwhose ptr points torootptrand whose vptr points to its ownVtable- Mixin: The data type used in
Classto store all parent class fields and type information - MixinData: Mixin is used to store the data type of all parent class fields
- MetaInfo: Data type used by Mixin to store interface and type conversion information
- TypeInfo: The data in MetaInfo that actually implements the interface and type conversion
- DefVtable: Function used to calculate
Vtabletype forInterface - Api: Functions implemented by
Interfaceto calculate allApiMethods of itself - Fn: A function implemented by
Classthat calculates all of its ownMethod
Principle
Mixin Design
First look at the key code in the zoop.Mixin() function:
pub fn Mixin(comptime T: type) type {
return struct {
deallocator: ?std.mem.Allocator = null,
meta: ?MetaInfo = null,
data: MixinData(T) = .{},
...
}
}Description
MixinData design
Assume that there are several class definitions in the module mymod:
pub const Base = struct {
pub usingnamespace zoop.Fn(@This());
mixin: zoop.Mixin(@This()),
};
pub const BaseTwo = struct {
pub usingnamespace zoop.Fn(@This());
mixin: zoop.Mixin(@This());
}
pub const Child = struct {
pub const extends = .{Base, BaseTwo};
pub usingnamespace zoop.Fn(@This());
mixin: zoop.Mixin(@This());
}
pub const SubChild = struct {
pub const extends = .{Child};
pub usingnamespace zoop.Fn(@This());
mixin: zoop.Mixin(@This());
}Then the MixinData structure of all classes is as follows:
MixinData(Base) = struct {};
MixinData(BaseTwo) = struct {};
MixinData(Child) = struct {
mymod_Base: Base,
mymod_BaseTwo: BaseTwo,
}
MixinData(SubChild) = struct {
mymod_Child: Child,
}MixData(T) will determine what data is included in the returned zig struct based on the contents of T.extends. In this way, a class can contain the data of all parent classes in an orderly manner.
MetaInfo design
MetaInfo supports the following type conversions:
- Transformations between any two points in the class hierarchy of
Class - Conversion between any two points in the interface inheritance tree of
Class - Conversion between any two points in the inheritance tree between the interface and the class of
Class
Simply put, you can make any intuitive conversion between classes and interfaces.
Let’s first look at the key code of MetaInfo:
pub const MetaInfo = packed struct {
rootptr: ?*anyopaque = null,
typeinfo: ?*const TypeInfo = null,
...
}Description
rootptr:rootptrof all parent class data underMixinData(T)points to the real address ofTtypeinfo: type conversion information, see TypeInfo for details
Because the rootptr in all parent class data in the class's MixinData points to the real class data, the original data can be found during the type conversion process, and then with the help of TypeInfo, it can be freely converted in the interface and type tree. Let's see how TypeInfo performs interface and type conversion.
Design of TypeInfo
The structure of TypeInfo is as follows:
pub const VtableFunc = *const fn (ifacename: []const u8) ?*IObject.Vtable;
pub const SuperPtrFunc = *const fn (rootptr: *anyopaque, typename: []const u8) ?*anyopaque;
pub const TypeInfo = struct {
typename: []const u8,
getVtable: VtableFunc,
getSuperPtr: SuperPtrFunc,
...
}Description
- typename: the type name of the object pointed to by
rootptr - getVtable: Given an interface name, return the function of the interface
Vtable - getSuperPtr: Given
rootptrand a class name, returns a function that points to the class data pointer.
example:
const t = std.testing;
var o = try SubChild.new(t.allocator);
defer o.destroy();
const ptr1 = o.mixin.meta.?.typeinfo.?.getSuperPtr(o.mixin.meta.?.rootptr.?, @typeName(Base)).?;
const ptr2 = &o.mixin.data.mymod_Child.mixin.data.mymod_Base;
try t.expect(@intFromPtr(ptr1) == @intFromPtr(ptr2));
const iobj = o.as(zoop.IObject).?;
const vptr = o.mixin.meta.?.typeinfo.?.getVtable(@typeName(zoop.IObject)).?;
try t.expect(@intFromPtr(iobj.vptr) == @intFromPtr(vptr));Principle of DefVtable
The declaration of DefVtable is as follows:
pub fn DefVtable(comptime Iface: type, comptime APIs: type) typeSuppose there is the following Interface definition:
pub const ISome = struct {
pub const extends = .{IBase1, IBase2};
pub const Vtable = zoop.DefVtable(ISome, struct {
someFunc: *const fn(*anyopaque) void,
});
}The pseudo code for the highlighted part that actually works is as follows (assuming that usingnamespace can introduce struct field):
pub const Vtable = struct {
pub usingnamespace IBase1.Vtable;
pub usingnamespace IBase2.Vtable;
someFunc: *const fn(*anyopaque) void,
}The principle of API
The declaration of Api is as follows:
pub fn Api(comptime I: type) typeIf there is the following definition of Interface:
pub const IBase = struct {
pub usingnamespace zoop.Api(@This());
...
pub fn Api(comptime I: type) type {
return struct {
pub fn baseFunc1(self: I) void { _ = self; }
pub fn baseFunc2(self: I) void { _ = self; }
}
}
}
pub const IChild = struct {
pub usingnamespace zoop.Api(@This());
...
pub fn Api(comptime I: type) type {
return struct {
pub fn childFunc(self: I) void { _ = self; }
}
}
}Through the calculation of zoop.Api, the above code is equivalent to:
pub const IBase = struct {
...
pub fn baseFunc1(self: IBase) void { _ = self; }
pub fn baseFunc2(self: IBase) void { _ = self; }
}
pub const IChild = struct {
...
pub fn baseFunc1(self: IChild) void { _ = self; }
pub fn baseFunc2(self: IChild) void { _ = self; }
pub fn childFunc(self: IChild) void { _ = self; }
}The principle of Fn
The declaration of Fn is as follows:
pub fn Fn(comptime T: type) typeSuppose there is the following Class definition:
pub const Base = struct {
pub usingnamespace zoop.Fn(@This());
...
pub fn someFunc(self: *Base) void { _ = self; }
pub fn Fn(comptime T: type) type {
return zoop.Method(.{
struct {
pub fn baseFunc(this: *T) void { _ = this; }
},
});
}
};
pub const Child = struct {
pub const extends = .{Base};
pub usingnamespace zoop.Fn(@This());
...
pub fn Fn(comptime T: type) type {
return zoop.Method(.{
struct {
pub fn childFunc(this: *T) void { _ = this; }
},
});
}
}Through the calculation of zoop.Fn, the above code is ultimately equivalent to:
pub const Base = struct {
...
pub fn someFunc(self: *Base) void { _ = self; }
pub fn baseFunc(this: *Base) void { _ = this; }
}
pub const Child = struct {
...
pub fn baseFunc(this: *Child) void { _ = this; }
pub fn childFunc(this: *Child) void { _ = this; }
}Note
Note that Base.someFunc is not inherited by Child, because only methods defined in Fn participate in inheritance.
Note that the first parameter of Base.someFunc is called self, while the first parameter of other functions in Fn is called this. This is a recommended specification, so that in functions with this, self can still be defined to point to the
Classwhere the function is implemented.zoop.Method is actually just an alias for tuple.Init.
