Type id comptime generation

I have a code that generates IDs for types in comptime.

const ComponentID = comptime_int;

const component_identifier = block_name: {
    comptime var last_id = 0;

    const result = struct {
        fn getComponentTypeIDnum(comptime T: type) ComponentID {
            _ = T;
            last_id += 1;
            return last_id;
        }

        pub fn getComponentTypeID(comptime T: type) ComponentID {
            const S = struct {
                const type_id = getComponentTypeIDnum(T);
            };
            return S.type_id;
        }
    };

    break :block_name result;
};

test "static" {
    const std = @import("std");

    const a = component_identifier.getComponentTypeID(i32);
    const b = component_identifier.getComponentTypeID(i32);

    const c = component_identifier.getComponentTypeID(usize);
    const d = component_identifier.getComponentTypeID(isize);
    const e = component_identifier.getComponentTypeID(f32);

    const TestStruct = struct {};
    const f = component_identifier.getComponentTypeID(TestStruct);

    try std.testing.expect(a == 1);
    try std.testing.expect(b == 1);

    try std.testing.expect(c == 2);
    try std.testing.expect(d == 3);
    try std.testing.expect(e == 4);

    try std.testing.expect(f == 5);
}

It compiles with Zig v0.11 but doesn’t with later versions.

Is there a way to have the same logic in v0.14?

Hi and welcome to the forum!

Here’s one approach: https://github.com/ziglang/zig/issues/19858#issuecomment-2369861301

The code from that comment:

const TypeId = *const struct {
    _: u8,
};

pub inline fn typeId(comptime T: type) TypeId {
    return &struct {
        comptime {
            _ = T;
        }
        var id: @typeInfo(TypeId).pointer.child = undefined;
    }.id;
}
2 Likes

Could you abuse errors for this?

const id = @intFromError(error.StructName);

The linked issue mentions a variation of that in a comment, but I’d just use what the core team recommends (the snippet above)

I’ve came up with this solution, if someone needs an integer representation of a type.

const std = @import("std");

const TypeId = comptime_int;

// FNV-1a 64-bit hash
fn typeNameToId(comptime T: type) TypeId {
    const name = @typeName(T);
    var hash: u64 = 0xcbf29ce484222325;

    inline for (name) |c| {
        hash ^= c;
        hash *%= 0x100000001b3;
    }

    return @as(TypeId, hash);
}

pub inline fn typeId(comptime T: type) TypeId {
    return struct {
        const id = typeNameToId(T);
    }.id;
}

test "hashed type id" {
    const id1 = typeId(i32);
    const id2 = typeId(i32);
    const id3 = typeId(usize);

    const TestStruct = struct {};
    const id4 = typeId(TestStruct);
    const id5 = typeId(TestStruct);

    const TestStruct2 = struct {};
    const id6 = typeId(TestStruct2);

    try std.testing.expect(id1 == id2);
    try std.testing.expect(id1 != id3);

    try std.testing.expect(id4 == id5);
    try std.testing.expect(id4 != id6);
}

It has an astronomically small chance of collision tho

1 Like

Comptime-known string values are deduplicated so there’s only one of any given string. So a static pointer to the type name could be used as a type ID. Advantage: won’t collide, can retrieve the type name, it’s null-terminated so it can be slice-ified if necessary. Disadvantage: the code has a bunch of type name strings, which maybe it doesn’t need.

If the latter isn’t a problem this seems like the simplest solution. Type names are qualified by namespace so synonyms aren’t a problem.

1 Like

Just wanted to chime in with two additional approaches to this, which are working in 0.14.1.

This one is a pointer-based approach posted by Miguel Bartelsman in the comments of this article: Cool Zig Patterns - Type Identifier - Zig NEWS

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

This is the most concise implementation I’ve seen yet, however it still has the (subjective) drawback of having the IDs clustered around wherever these structs happen to get loaded into memory.

Here is a version of the sample that takes advantage of link sections to get comptime ordered counting starting at 1:

const section_name = ".bss.RTTI_Types";
const @"RTTI_Types.head": u8 linksection(section_name ++ "0") = 0;

pub fn typeId(comptime T: type) TypeId {
    const H = struct {
        const byte: u8 linksection(section_name ++ "1") = 0;
        const _ = T;
    };
    return &H.byte - &@"RTTI_Types.head";
}

This is pretty dodgy, but it actually works surprisingly well. Initially I tried to have the header and the type entries in the same section (closer to the example in the article), but I was running into issues with the header always being added to the symbol table after the first type was added. This meant that the first type I’d register would have the unsigned equivalent of -1 as its ID, and then each subsequent type registered would increment correctly from 1.

The workaround to this problem is to rely on the symbol tables being loaded in lexicographic order, but I can’t say with any confidence that this is a portable technique.

1 Like