Zig NEWS

Cover image for Cool Zig Patterns - Type Identifier
Felix "xq" Queißner
Felix "xq" Queißner

Posted on

Cool Zig Patterns - Type Identifier

So did you ever encounter the reason to implement some runtime type erasure and you couldn't figure out how to assign a unique number to a type?

Well, there' this one tiny trick Spex once told me:

pub fn typeId(comptime T: type) usize {
    _ = T;
    const H = struct {
        var byte: u8 = 0;
    };
    return @ptrToInt(&H.byte);
}
Enter fullscreen mode Exit fullscreen mode

This piece of code take any Zig type as a parameter and returns a unique ID for that type. This is done by generating a global variable each time we call the function. Due to memoization, we get the same result if we call the function with the same parameters again in the future, we have the guarantee that typeId(u32) == typeId(u32) as well as typeId(u32) != typeId(u31) in all cases.

I have implemented this feature in a library called any-pointer which also implements a type erased pointer that has runtime safety features in Safe modes, so you will get a panic instead of a type confusion error.

We can also play this further by enforcing the compiler to emit "sequential" type ids:

const TypeMap = struct {
    const section_name = ".bss.TypeMapKeys"; // use a distinct section to ensure sequential linking
    export const @"TypeMap.head": u8 linksection(section_name) = 0;

    pub fn get(comptime T: type) usize {
        _ = @"TypeMap.head"; // enforce referencing and instantiation of the head before we every export anything else
        const Storage = struct {
            const index: u8 linksection(section_name) = 0;
        };
        comptime {
            @export(Storage.index, .{ .name = "TypeMap.index." ++ @typeName(T), .linkage = .Strong });
        }
        return @ptrToInt(&Storage.index) - @ptrToInt(&@"TypeMap.head");
    }
};
Enter fullscreen mode Exit fullscreen mode

This uses a hack that we create a custom section to put our type marker byte in. By forcing the export of a head variable, we get a reference point for the sequence.

Note that this does not guarantee you anything except that we get a dense type id set that starts at most likely 1. If we employ a linker script, we can enforce the head to be put at the start of the section to get the guarantee that type ids start at 1. I don't recommend do use this though, just use the sparsely distributed typeId function if you need some RTTI.

Oldest comments (2)

Collapse
 
emmanueloga profile image
Emmanuel Oga

I wonder if you have a link to a repo/project were you used this, to see it in action.

Collapse
 
mbartelsm profile image
Miguel Bartelsman • Edited

I believe that starting with 0.12.0 you must capture the passed comptime parameters in order to ensure a distinct type is being generated, since memoization no longer plays a part in the semantics of the language.

pub fn typeId(comptime T: type) usize {

    const H = struct {
        var byte: u8 = 0;
        var _ = T;
    };
    return @intFromPtr(&H.byte);
}
Enter fullscreen mode Exit fullscreen mode