Generic Structs
We can create generic structs by returning them from a generic function using the following syntax:
fn function_name ( T: type) type {
return struct {
//… we build a struct (a new type) based on T
// with its own methods and fields
};
}
Just like with generic functions, the compiler uses the generic struct as a template to generate specialized versions for each use in the source code.
This gives us great flexibility to encapsulate data of different types without duplicating logic.
After the rough and deafening "landing" of the flying machine against the bonfire pole, the crowd in the square had fled in terror. They must have thought it was a dragon or the devil himself. There didn’t seem to be any inquisitors left around either.
Manitas approached the inventor and, after silently observing his situation for a while -as if it were a malfunction in the oscillating buckets; Juanelo even thought he’d gotten stuck: “great time for a breakdown, amico”-, finally got to work trying to cut the ropes that tied the inventor to the post. Well, more like hand... the one that still worked after the crash.
- Grazie Manite, what a crash you took, amico.
Manitas didn’t reply. He didn’t have any modules for speaking or communication. Always working in silence, accompanied only by the soft creaking of his own levers and gears… he almost seemed wiser, thought Juanelo.
He still hadn’t finished cutting through the thick ropes when Juanelo spotted a pair of inquisitors returning to the square, swords in hand. They saw them and, after hesitating for a moment, began to approach.
- It’s the wooden man who fell from the sky. He’s freeing the warlock -Juanelo heard the armed clerics say.
- Manite, amico. We have to go - he said urgently.
Manitas lifted his head and finally saw them. He stood up, turned around, and began walking slowly away.
- Manite, don’t leave me here, amico. No! -shouted Juanelo, desperate.
It wasn’t just a couple anymore. The inquisitors were returning to the square, determined to finish him off.
- Manite, don’t go! Help! Come back! -shouted Juanelo, who, from his position on the ground, could barely see the automaton clearly.
Manitas seemed to be damaged from the fall. “He’s trying to escape in the flying machine,” thought the inventor as he saw him reach into the wreckage. And then he realized...
- In the name of God! -The inquisitor thundered once again, drawing ever closer, enraged, from the other side of the square.
- Warlock! Die by my sword and then we will burn your remains!
Manitas pulled the machine gun from inside the wrecked machine. As always, in silence, accompanied by the soft hum of his gears, he fitted a heavy magazine filled with metal projectiles into the weapon. He raised his gaze and aimed at the inquisitors charging toward them... and turned the crank.
magazines.zig |
|
const std = @import("std"); const print = std.debug.print; const Metal = enum { Iron, Steel, Copper, const N_SIZE: u8 = 1; }; const Nature = enum { Wood, Rock, const N_SIZE: u8 = 2; }; // returns a magazine for a given projectile type fn pack_magazine(T: type) type { return struct { // capacity depends on projectile size const N_MAG_CAPACITY: u8 = 100 / T.N_SIZE; a_slots: [N_MAG_CAPACITY]T = undefined, l_ammo: std.ArrayListUnmanaged(T) = undefined, b_initialized: bool = false, fn init(self: *@This()) void { self.l_ammo = std.ArrayListUnmanaged(T) .initBuffer(&self.a_slots); self.b_initialized = true; } fn load_ammo(self: *@This(), x_item: T) bool { if (!self.b_initialized) self.init(); if (self.l_ammo.items.len == self.a_slots.len) { print("No more projectiles fit \n", .{}); return false; } self.l_ammo.appendAssumeCapacity(x_item); return true; } }; } // machine gun that can use different types of magazine const MachineGun = struct { p_magazine: ?*anyopaque = null, // pointer to magazine // pointer to anonymous function specific to the magazine type fn_empty_mag: ?*const fn (*anyopaque) void = null, // insert the magazine fn load_mag(self: *@This(), x_mag: anytype) void { // we get the type of the magazine const T = @TypeOf(x_mag); const o_tinfo = @typeInfo(T); // minimum contract for a magazine if (!@hasField( o_tinfo.pointer.child, "l_ammo", )) return; self.p_magazine = x_mag; // anonymous function to handle the specific type self.fn_empty_mag = struct { fn _(p_magazine: *anyopaque) void { const p_mag: *o_tinfo.pointer.child = @ptrCast(@alignCast(p_magazine)); if (!p_mag.*.b_initialized or p_mag.*.l_ammo.items.len == 0) return print("click? (no bullets)\n\n", .{}); while (p_mag.*.l_ammo.items.len > 0) print( "{s}! ", .{@tagName(p_mag.*.l_ammo.orderedRemove(0))}, ); print("\n", .{}); } }._; } // fires all projectiles, emptying the magazine fn shoot(self: *MachineGun) void { // if no magazine, there's no function to fire either if (self.fn_empty_mag) |fn_x| fn_x(self.p_magazine.?) else print("clack? (no magazine)\n\n", .{}); } }; pub fn main() !void { var o_gun = MachineGun{}; // shoot with no magazine inserted o_gun.shoot(); var o_mag1 = pack_magazine(Metal){}; var o_mag2 = pack_magazine(Nature){}; print("Filling the magazines\n", .{}); const fill_mag = struct { fn _(o_m1: anytype, o_m2: anytype) void { for (0..100) |_| { if (!o_m1.load_ammo(Metal.Iron) or !o_m1.load_ammo(Metal.Steel) or !o_m2.load_ammo(Nature.Rock)) break; } } }._; // magazines with bullets fill_mag(&o_mag1, &o_mag2); // magazine 1 inserted o_gun.load_mag(&o_mag1); o_gun.shoot(); o_gun.shoot(); // by mistake // magazine 2 inserted o_gun.load_mag(&o_mag2); o_gun.shoot(); // carga de balas fill_mag(&o_mag1, &o_mag2); // magazine 1 inserted o_gun.load_mag(&o_mag1); o_gun.shoot(); } |
|
$ zig run magazines.zig |
|
clack? (no magazine) Filling the magazines No more projectiles fit Iron! Steel!... |
In this example, Juanelo’s primitive machine gun was modified that very morning to accept magazines of different types.
To get a magazine adapted to a specific kind of ammo, we use the pack_magazine function, which returns a generic struct representing it:
|
// returns a magazine for a given projectile type fn pack_magazine(T: type) type { return struct { // its own fields and functions }; } |
We avoid replicating the logic of a magazine (its methods and fields) while still using strong typing. As an example, we can adjust properties like capacity based on the projectile type:
|
// capacity depends on projectile size const N_MAG_CAPACITY: u8 = 100 / T.N_SIZE; a_slots: [N_MAG_CAPACITY]T = undefined, |
Our machine gun is now completely agnostic to the type of projectile it fires and the type of magazine we load. This time, each magazine holds more or fewer bullets depending on its type, and different kinds of projectiles can't be mixed (let’s say this prevents jams).
Anyopaque
The machine gun’s magazine is “loaded” into a pointer called p_magazine, which we declare as anyopaque. We mark it with ? so it can be initialized as null.
anyopaque means that it is a pointer with erased type: it points to memory with no type or size information at the time of its declaration, but it is not empty at runtime.
|
// a machine gun that can use different types of magazines const MachineGun = struct { p_magazine: ?*anyopaque = null, // pointer to magazine // ... |
To be able to process (fire) the bullets from the magazine pointed to by the anyopaque type, we declare a property called fn_empty_mag. This is a function pointer, and here’s why we need it:
|
// pointer to an anonymous function specific to the magazine type fn_empty_mag: ?*const fn (*anyopaque) void = null, |
This function pointer is also marked as optional, so we can initialize it to null in its declaration. We specify that it will take a single argument: a pointer to anyopaque, which will actually be the magazine of unknown type pointed to by p_magazine.
We use anyopaque because, for the weapon, the type of magazine is completely unknown-it makes no difference whether it fires rock, wood, or steel pellets. We’ve imposed very few constraints in that regard.
@This()
When the magazine is mounted using the load_mag function, the machine gun retrieves its type and structured info using @TypeOf and @typeInfo. Without that information, the anyopaque pointer cannot be used.
Until now, we’ve always used something like self: *ConcreteType as the first argument of a struct method. You’ve probably noticed that in this example we use self: *@This() instead.
@This() is a builtin function that returns the type (struct, enum, or union) in which it’s declared.
This is super useful here because the struct is anonymous: we haven’t declared it as an explicit type, but we still need a pointer to that type to handle it through self.
|
// insert the magazine fn load_mag(self: *@This(), x_mag: anytype) void { // ... } |
Specialized function pointer, @ptrCast and @alignCast
In previous examples, anonymous and encapsulated functions were used to declare code that couldn’t be accessed from outside the functions that contain them.
In this case, we use the technique for something entirely different: this time, we do want the function to be callable from outside, and that’s why we store it in the self.fn_empty_mag pointer declared earlier.
It’s not possible to define this function in the usual static way if we want it to work with any type of magazine pointed to by p_magazine.
By declaring it with anyopaque, we erase the data type, but when it's time to use it, we need to recover the original type. We do that using the information stored in o_tinfo.
|
// insert the magazine fn load_mag(self: *@This(), x_mag: anytype) void { // ... // anonymous function used to handle a specific type self.fn_empty_mag = struct { fn _(p_magazine: *anyopaque) void { const p_mag: *o_tinfo.pointer.child = @ptrCast(@alignCast(p_magazine)); if (!p_mag.*.b_initialized or p_mag.*.l_ammo.items.len == 0) return print("click? (no bullets)\n\n", .{}); while (p_mag.*.l_ammo.items.len > 0) print( "{s}! ", .{@tagName(p_mag.*.l_ammo.orderedRemove(0))}, ); print("\n", .{}); } }._; } |
We obtain this data using @alignCast and @ptrCast.
@alignCast
Alignment is a hardware constraint that governs access to a value. A value of a certain type must begin at a memory address that is a multiple of N bytes. That N is the alignment of the type.
For example:
- u8: 1 byte
- u16: 2 bytes
- u32: 4 bytes
- struct (8 or 16 bytes, depending on its contents)
Since its alignment is 1, a u8 can be stored at any memory address. A u32 must start at multiples of 4: 0, 4, 8, 12, etc.
A struct aligned to 8 must start at addresses that are multiples of 8: 0, 8, 16, 24, etc. If we try to read a u32 (alignment 4) from address 3, it will trigger errors, undefined behavior, or a crash.
In Zig, a pointer always has a defined alignment. An *anyopaque has alignment 1 because we don’t know what type is behind it, but a pointer to a struct will have alignment 8, 16, or more depending on its contents.
When we have:
*anyopaque → *MetalMagazine
We know the anyopaque pointer is pointing to a value with alignment greater than 1.
@alignCast adjusts the pointer’s alignment. This alignment is inferred from the type of the variable it’s being assigned to.
@ptrCast converts the pointer’s type to match the variable it’s being assigned to, but does not affect alignment.
Neither of these functions moves memory, copies data, or fixes errors.
|
const p_mag: *o_tinfo.pointer.child = @ptrCast(@alignCast(p_magazine)); |
Here, o_tinfo is the structured info we got earlier about the type. The .pointer.child gives us the exact struct type that it points to.
After the incident in Green Square, the Holy Inquisition forced all clerics to leave the city against their will. They claimed that the demon had taken hold and that they could no longer guarantee the clergy’s safety.
Fortunately, the king ensured that the situation didn’t last long.
In the meantime, Juanelo created an automaton monk who helped people with their prayers, weddings, and baptisms… but that is another story.