In the project root directory:
zig fetch "git+https://github.com/zhuyadong/zoop.git" --save=zoop
If you want to install a specific version:
zig fetch "git+https://github.com/zhuyadong/zoop.git#<ref id>" --save=zoop
// Define a class Human
pub const Human = struct {
// The first field of the zoop class must be aligned to `zoop.alignment`
name: []const u8 align(zoop.alignment),
age: u8 = 30,
// If there is no cleanup work, can skip define `deinit`
pub fn deinit(self: *Human) void {
self.name = "";
}
pub fn getName(self: *const Human) []const u8 {
return self.name;
}
pub fn setName(self: *Human, name: []const u8) void {
self.name = name;
}
};
const t = std.testing;
// Create a `Human` on the heap
var phuman = try zoop.new(t.allocator, Human, null);
// If the class field has a default value, the object field will be initialized to the default value
try t.expect(phuman.age == 30);
// Destroy the object and release the memory.
// If the class defines `deinit`, it will be called first and then release the memory.
zoop.destroy(phuman);
// Create a `Human` on the stack
var human = zoop.make(Human, null);
// Access object fields through `ptr()`
try t.expect(human.ptr().age == 30);
// Clean up the object (call `deinit` if any).
// If there is no work to clean up, you don't need to call `zoop.destroy`
zoop.destroy(human.ptr());
// Both `zoop.new` and `zoop.make` support creation-time initialization
phuman = try zoop.new(t.allocator, Human, .{.name = "HeapObj", .age = 1});
human = zoop.make(Human, .{.name = "StackObj", .age = 2});
try t.expect(phuman.age == 1);
try t.expect(human.ptr().age == 2);
Note about deinit
:
zoop.destroy
will sequentially call the deinit
method of the class and all its parent classes
// Define `SuperMan`, inherit from `Human`,
// the parent class must be the first field and the alignment is `zoop.alignment`,
// The field name is arbitrary and does not have to be `super`, but it is recommended to use `super`
pub const SuperMan = struct {
super: Human align(zoop.alignment),
// SuperMan can live a long time, u8 can't satisfy it, we use u16
age: u16 = 9999,
pub fn getAge(self: *SuperMan) u16 {
return self.age;
}
pub fn setAge(self: *SuperMan, age: u16) void {
self.age = age;
}
};
// First create a `SuperMan` object
var psuperman = try zoop.new(t.allocator, SuperMan, null);
//Call parent class method
psuperman.super.setName("super");
// Or call the parent class method like this. This method is suitable for situations where the
// inheritance hierarchy is too deep and you don't know which parent class implements the `setName` method.
// In addition, since it is called `upcall`, it means that even if `SuperMan` implements `setName`,
// The following call will still call the `setName` method of the nearest parent class
zoop.upcall(psuperman, .setName, .{"super"});
// You can also flexibly access all fields in the class inheritance tree. For example,
// if you want to access the `Human.age` field, you can do this:
var phuman_age = zoop.getField(psuperman, "age", u8);
try t.expect(phuman_age.* == 30);
// Access `SuperMan.age`, you can do this:
var psuper_age = zoop.getField(psuperman, "age", u16);
try t.expect(psuper_age.* == 9999);
// Note that if two `age` are of the same type and both are called "age",
// The above `zoop.getField` call will cause a compilation error to avoid bugs
// First create a Human and a SuperMan
var phuman = try zoop.new(t.allocator, Human, null);
var psuper = try zoop.new(t.allocator, SuperMan, null);
// Subclasses can be converted to parent classes
t.expect(zoop.as(psuper, Human) != null);
t.expect(zoop.cast(psuper, Human).age == 30);
// The parent class cannot be converted to a subclass (if `zoop.cast` is used, a compilation error will occur)
t.expect(zoop.as(phuman, SuperMan) == null);
// A parent class pointer to a subclass can be converted to a subclass
phuman = zoop.cast(psuper, Human);
try t.expect(zoop.as(phuman, SuperMan) != null);
// Define an interface `IName` for accessing names
pub const IName = struct {
// The interface can only define two fields, `ptr` and `vptr`,
// and the names and types must be the same as below
ptr: *anyopaque,
vptr: *anyopaque,
// Define the `getName` interface method
pub fn getName(self: IHuman) []const u8 {
return zoop.icall(self, .getName, .{});
}
// Define the `setName` interface method
pub fn setName(self: IHuman, name: []const u8) void {
zoop.icall(self, .setName, .{name});
}
// Don't worry about what `zoop.icall` is, just follow it
};
// Define another interface `IAge` for accessing age
pub const IAge = struct {
ptr: *anyopaque,
vptr: *anyopaque,
pub fn getAge(self: IHuman) u16 {
return zoop.icall(self, .getAge, .{});
}
pub fn setAge(self: IHuman, age: u16) void {
zoop.icall(self, .setAge, .{age});
}
}
// Interfaces can also be inherited
pub const INameAndAge struct {
pub const extends = .{IName, IAge};
ptr: *anyopaque,
vptr: *anyopaque,
}
// can specify exclude APIs.
// Only methods defined in this interface can be specified,
// and inherited methods will not be affected.
pub const INameAndAge struct {
pub const extends = .{IName, IAge};
// exclude “eql" method
pub const excludes = .{"eql"};
ptr: *anyopaque,
vptr: *anyopaque,
pub fn eql(self: INameAndAge, other: INameAndAge) bool {
return self.ptr == other.ptr;
}
}
// Interfaces can also provide default implementations of methods,
// so that classes that declare to implement interfaces can still
// compile and work correctly without implementing these methods
// (the interface becomes an abstract class)
pub const IName = struct {
...// Same as above code
pub fn Default(comptime Class: type) type {
return struct {
pub fn getName(_: *Class) []const u8 {
return "default name";
}
}
}
}
// We let `Human` implement the `IName` interface
pub const Human = struct {
pub const extends = .{IName};
...// Same as above code
};
// Let `SuperMan` implement the `IAge` interface
pub const SuperMan = struct {
pub const extends = .{IAge};
...//Same as above code
}
// The interface implemented by the parent class is automatically implemented by the class,
// so `SuperMan` also implements `IName`, although it only declares that it implements `IAge`.
// A subclass can repeatedly declare that it implements an interface that has already been implemented
// by its parent class. This will not cause any problems and will not affect the results.
// For example, the following code is equivalent to the above:
pub const SuperMan = struct {
pub const extends = .{IAge, IName};
...
}
// First create a Human and a SuperMan
var phuman = try zoop.new(t.allocator, Human, .{.name = "human"});
var psuper = try zoop.new(t.allocator, SuperMan, .{.super = .{.name = "super"}});
// Human implements IName, so it can be converted
var iname = zoop.cast(phuman, IName);
// SuperMan implements IAge, so it can be transferred
var iage = zoop.cast(psuper, IAge);
try t.expect(iage.getAge() == psuper.age);
try t.expectEqualStrings(iname.getName(), phuman.name);
// Human does not implement IAge, so the conversion will fail.
// (Note that now `iname` points to `phuman`, and `iage` points to `psuper`)
try t.expect(zoop.as(phuman, IAge) == null);
try t.expect(zoop.as(iname, IAge) == null);
// Now let iname point to psuper
iname = zoop.cast(psuper, IName);
// Or you can write it like this, but the performance is a little affected
// (`cast` is O(1), while `as` is O(n) in the worst case n=the number of interfaces implemented by SuperMan)
iname = zoop.as(psuper, IName).?;
// Now iname can be converted to IAge
try t.expect(zoop.as(iname, IAge) != null);
try t.expectEqualStrings(iname.getName(), "super");
// Everything can be converted to zoop.IObject
try t.expect(zoop.as(phuman, zoop.IObject) != null);
try t.expect(zoop.as(psuper, zoop.IObject) != null);
// Can also be converted back from IObject
var iobj = zoop.cast(psuper, zoop.IObject);
try t.expect(zoop.as(iobj, SuperMan).? == psuper);
To summarize cast
and as
:
cast
is applicable- Subclass -> Parent class
- Sub-interface -> Parent interface
- Class -> Interfaces implemented by the class and its parent class
as
is applicable- All the cases where
cast
is applicable and not applicable (everything can beas
)
- All the cases where
// If SuperMan overrides the getName method
pub const SuperMan = struct {
...//Same as above
pub fn getName(_: *SuperMan) []const u8 {
return "override";
}
}
// Now IName.getName will call SuperMan.getName instead of Human.getName
var psuper = try zoop.new(t.allocator, SuperMan, .{.super = .{.name = "human"}});
var iname = zoop.cast(psuper, IName);
try t.expectEqualStrings(iname.getName(), "override");
// Another style of calling interface methods
try t.expectEqualStrings(zoop.vcall(psuper, IName.getName, .{}), "override");
// Virtual method calls are also useful for converted classes
var phuman = zoop.cast(psuper, Human);
iname = zoop.cast(phuman, IName);
try t.expectEqualStrings(iname.getName(), "override");
try t.expectEqualStrings(zoop.vcall(phuman, IName.getName, .{}), "override");
Performance notes for vcall
:
vcall
will use cast
when possible, and as
otherwise
zoop.IObject
can conveniently output the string content of the object through the format(...)
mechanism of std.fmt
.
// define a class that implemented `zoop.IObject.formatAny`
pub const SomeClass = struct {
name:[]const u8 align(zoop.alignment) = "some";
pub fn formatAny(self: *SomeClass, writer: std.io.AnyWriter) anyerror!void {
try writer.print("SomeClass.name = {s}", .{self.name});
}
}
// print string from `SomeClass.formatAny`
const psome = try zoop.new(t.allocator, SomeClass, null);
std.debug.print("{}\n", .{zoop.cast(psome, zoop.IObject)});
// output: SomeClass.name = some