Skip to content

Type ids, listless and nodeless

No list here.

No Node.

Just your struct.


Why a type id

Zig's type lives only at compile time.

At run time, a value of unknown type is just an address.

Callbacks, queues and handler maps pass such addresses around. Each one needs to know what is behind the address.

Paternitas gives any struct a runtime type id.

  • Typed(P) takes any struct.
  • The struct needs no TypedNode.
NAQ: What about a value that is not a struct?

Wrap it in a struct.

const Count = struct { value: u64 }; is enough.

Typed(Count) then gives it a type id.


The four calls

call what it does
TypedPoint.typeId() gives the type id of Point
TypedPoint.isId(id) tells if id is the type id of Point
TypedPoint.toAny(&point) packs the address and the type id
TypedPoint.fromAny(any) checks the type id, and gives you *Point or null

No allocation.

No registry.

No init call.


A handler map, no list

Two structs, with no TypedNode:

const Point: type = struct {
    x: i32,
    y: i32,
};
const TypedPoint: type = paternitas.Typed(Point);

const Tick: type = struct {
    count: u64,
    label: []const u8,
};
const TypedTick: type = paternitas.Typed(Tick);

Register a handler per type. Send the structs as Anys:

try handlers.put(TypedPoint.typeId(), onPoint);
try handlers.put(TypedTick.typeId(), onTick);

var point: Point = .{ .x = 3, .y = 4 };
var tick: Tick = .{ .count = 7, .label = "tick" };
var empty: Empty = .{};

const items: [3]paternitas.Any = .{
    TypedPoint.toAny(&point),
    TypedTick.toAny(&tick),
    TypedEmpty.toAny(&empty),
};

var counts: Counts = .{};
for (items) |any| dispatch(&handlers, any, &counts);

Empty has no fields. It still gets its own type id.

It has no handler, so the dispatch counts it as unhandled.

Ask about the type, without turning it back:

/// `isId` asks about one type. It is true for that type only.
fn checkIsId(any: paternitas.Any) !void {
    if (!TypedPoint.isId(any.type_id)) return error.WrongId;
    if (TypedTick.isId(any.type_id)) return error.WrongId;
}

The full program: Type id without a node.


The rules

  • Structs only. Wrap any other value in a struct.
  • The struct has no room for the type id. It travels next to the pointer, in Any.
  • A bare pointer carries no type id.
  • The type id is for one running program. Do not save it or send it.
NAQ: Why can I not save or send a type id?

A type id is an address inside your running program.

Another run puts it somewhere else. Another program has its own.

So it means something only inside one run of one program.


What the compiler stops

Only a struct with a TypedNode can go in a list.

So node, parentFromNode and the other list functions need a TypedNode.

A plain std Node is not one:

//! A plain std Node is not a TypedNode.
//!
//! A struct without a TypedNode cannot go in a list.
//!
//! So `node(&b)` does not compile.

const BareNode = struct {
    node: std.DoublyLinkedList.Node = .{},
};

export fn listCall() void {
    var b: BareNode = .{};
    _ = p.Typed(BareNode).node(&b);
}

const p = @import("paternitas");
const std = @import("std");
bare_node.BareNode: no TypedNode, so it has only typeId, isId, toAny and fromAny

Typed takes structs only:

//! Typed takes structs only.

comptime {
    _ = p.Typed(u32).typeId();
}

const p = @import("paternitas");
u32: not a struct

Each error message shows the type name.