Skip to content

Queues, maps, union fields

Not everything is a linked list.

  • A generic queue carries different structs.
  • A map keeps one handler per type.
  • A union field carries a struct, but must not copy it.

Use Any.


What an Any is

An Any is two words:

  • ptr, the struct's address;
  • type_id, the struct's type id.

It does not hold a copy of your struct.

A queue, a map or a union field copies the two words. Your struct stays where it is.

call what it does
TypedMessage.toAny(&message) packs the address and the type id
TypedMessage.fromAny(any) checks the type id, and gives you *Message or null

Call setTypeId on the struct before toAny.

  • toAny does not check it.
  • fromAny checks it when runtime safety is on.
NAQ: Why Any, and not *anyopaque?

A *anyopaque is an address. Nothing tells you what is behind it.

An Any carries the type id next to the address.

So fromAny can check the type before it gives you the struct.


Through a queue

A std.Io.Queue stores a copy of what you put in.

A connection must not be copied. So the queue carries its Any.

/// Takes the connection out of the list. Sends its `Any` through the
/// queue.
fn removeAndSend(timeouts: *std.DoublyLinkedList, queue: *Queue, io: std.Io, c: *Connection) !void {
    timeouts.remove(TypedConnection.node(c));
    try queue.putOne(io, TypedConnection.toAny(c));
}

/// Gets an `Any` from the queue. Puts the connection back in the list,
/// with a new deadline.
fn receiveAndAppend(timeouts: *std.DoublyLinkedList, queue: *Queue, io: std.Io) !void {
    const any: paternitas.Any = try queue.getOne(io);
    const c: *Connection = TypedConnection.fromAny(any) orelse return error.WrongParent;

    c.*.deadline += 100;
    timeouts.append(TypedConnection.node(c));

    std.log.info("connection {d} is back, deadline {d}", .{ c.*.id, c.*.deadline });
}

The full program: Timeout list and a queue.


A handler map

The receiver does not even have to know the type.

Keep one handler per type, in a map keyed by the type id:

var handlers: std.AutoHashMap(paternitas.TypeId, Handler) = .init(allocator);
defer handlers.deinit();

// Register the handlers. The dispatch code does not use your struct types.
try handlers.put(TypedMessage.typeId(), onMessage);
try handlers.put(TypedJob.typeId(), onJob);

The dispatch looks up the handler, and gives it ptr:

/// Picks the handler by type id. No struct type appears here.
fn dispatch(handlers: *const std.AutoHashMap(paternitas.TypeId, Handler), any: paternitas.Any, counts: *Counts) void {
    const h: Handler = handlers.get(any.type_id) orelse {
        std.log.info("no handler for this type", .{});
        counts.*.unhandled += 1;
        return;
    };
    h(any.ptr, counts);
}

The handler casts ptr to its own type:

// The cast has no check. The map matched the type id, so the type is
// right.
fn onMessage(parent: *anyopaque, counts: *Counts) void {
    const m: *Message = @ptrCast(@alignCast(parent));
    std.log.info("message: {s}", .{m.*.text});
    counts.*.messages += 1;
}
  • The dispatch code does not use your struct types.
  • The map matched the type id, so the cast is right.
  • A type with no handler is counted, not cast.

The full program: Handler map.


A large struct in a union field

Small events travel by value in a tagged union.

A union field stores a copy of what you put in.

A 4 KB buffer should not be copied. So the field is an Any:

/// It is too large to copy. Events carry its address and type id.
const Download: type = struct {
    tnode: paternitas.SinglyTypedNode = .{},
    buffer: [4096]u8 = undefined,
    received: usize = 0,
};
const TypedDownload: type = paternitas.Typed(Download);

const Event: type = union(enum) {
    tick: u64,
    resize: Size,
    parent: paternitas.Any,
};

The handler gets the Download back with fromAny:

.parent => |any| {
    const d: *Download = TypedDownload.fromAny(any) orelse return error.UnknownParent;
    d.*.received += 1;
    std.log.info("download, {d} bytes of buffer", .{d.*.buffer.len});
},

Each Event stays small.

The full program: Large struct in a union.


The struct must stay alive

An Any does not keep your struct alive.

  • The struct MUST stay alive while its Any is in a queue, a map or a union.
  • Paternitas checks the type. It does not check the lifetime.

Without a TypedNode

toAny and fromAny work for every struct.

The struct does not need a TypedNode.

Type ids, listless and nodeless shows that case.