How to check if a struct is from an instance of a generic struct

I can’t find a better title for this. What I’m asking for example:

const arr: std.ArrayList(usize) = .empty;

Is there a method that let me check if arr is an instance of the generic ArrayList struct. My main use for this is to make my own json parse that support zig first party structs without a struct wrapper like how std.json does.

I making a game, and de/serializing ArrayList and AutoHashMap for every struct with jsonParse and jsonSerialize is driving me crazy.

If it’s not possible, is there a better method without a wrapper type for ArrayList or HashMap?

1 Like
const std = @import("std");

fn isArrayList(t: type) bool {
    const name = "array_list.Aligned";
    return std.mem.eql(u8, name, @typeName(t)[0..name.len]);
}

pub fn main() !void {
    const l: std.ArrayList(u32) = .empty;
    if (isArrayList(@TypeOf(l))) {
        std.debug.print("l is array list.\n", .{});
    } else {
        std.debug.print("l is not array list.\n", .{});
    }

    const s: struct {} = undefined;
    if (isArrayList(@TypeOf(s))) {
        std.debug.print("s is array list.\n", .{});
    } else {
        std.debug.print("s is not array list.\n", .{});
    }
}

:grimacing:

1 Like

I was about to edit the post to include that I thought of this method. I didn’t like it because I thought it was cursed :distorted_face:

I guess there is no other way…

What are you trying to do with this? This seems hacky so I’m guessing there’s a better way to do what you actually want to be accomplished.

1 Like

How about this - Check if the provided struct has declaration (or field, I Always confuse these 2) named “items”
Take the type of that declaration, if it’s a slice of T then take T.
Return true if the type provided to the function == std.ArrayList(T).
If you need help I can try to write it. I think it should work.

5 Likes

I didn’t write the parse yet, this is a small snippet from the std.json:

      switch (@typeInfo(T)) {
        .bool => ...,
        .float, .comptime_float => ...,
        .int, .comptime_int => ...,
        .optional => |optionalInfo| ...,
        .@"enum" => ...,
        .@"union" => |unionInfo| ...,
        .@"struct" => |structInfo| ...,
        .array => |arrayInfo| ...,
        .vector => |vector_info| ...,
        .pointer => |ptrInfo| ...,
        else => @compileError("Unable to parse into type '" ++ @typeName(T) ++ "'"),
    }

What I basically want in the @"struct" branch is to check if the struct is an ArrayList or a HashMap and have a custom parsing for them.

A small function like this might work:

pub fn isArrayList(value: anytype) ?type {
    if (!@hasField(@TypeOf(value), "items")) return null;
    const info = @typeInfo(@TypeOf(value.items));
    if (info != .pointer) return null;
    return info.pointer.child;
}

Essentially this function checks for a string as well. Not a game dev, but a slice of “items” sounds to me like it could appear in a game code quite regularly. So maybe the check for this could give a false positive by accident more likely?

2 Likes

Ah, I missed the

part, you should integrate that part as well, I think.

1 Like

You want to directly compare to T == std.ArrayList(...) after you find the element type, like so:

const std = @import("std");

fn isArrayList(T: type) bool {
    if (!@hasField(T, "items")) return false;
    return switch (@typeInfo(@FieldType(T, "items"))) {
        .pointer => |info| T == std.ArrayList(info.child),
        else => false,
    };
}

test isArrayList {
    const A = std.ArrayList(u8);
    const B = std.ArrayList(i32);
    const C = struct { items: []const i32 };
    const D = struct {};

    try std.testing.expect(isArrayList(A));
    try std.testing.expect(isArrayList(B));
    try std.testing.expect(!isArrayList(C));
    try std.testing.expect(!isArrayList(D));
}

You should not use @typeName to compare types. The @typeName string is not stable and may change between compilation runs or Zig releases, and is also possible to spoof. The above check will always be correct.

7 Likes

Clever!
I do wander tho, if I were to make a patch to std.json rather than create my own json parser would it be accepted. Because, I think this should be supported for first-party collections imo.

I suspect that such a PR would be rejected, because different data structures are useful for different situations and std APIs favoring one or a few over all others will have the side effect of leading people to using the wrong tool for the job out of laziness/convenience.

In this case the more favored thing to do would probably be to either parse/serialize the array list as is, with an items field (i.e. {"foo":{"items":[1,2,3]}} in JSON), or use one of the dynamic JSON APIs to first parse into a regular plain slice and then instantiate an array list using .fromOwnedSlice or .initBuffer.

Edit: I quickly changed my mind, I actually think parsing and serializing array lists directly is bad because with the separate items and capacity you run a great risk of inadvertently violating important invariants.

1 Like

I don’t think I understood you well, can you give me an example, if you don’t mind?

Basically something like this, parsing into the dynamic std.json.Value, then temporary arena-allocated slices, then finally longer-lived slices owned by array lists.

const std = @import("std");

test {
    const S = struct {
        foo: std.ArrayList(i32),
        bar: std.ArrayList(f32),
    };

    const json =
        \\{
        \\  "foo": [1, 2, 3, 4, 5],
        \\  "bar": [1.25, 573.765]
        \\}
    ;

    const gpa = std.testing.allocator;

    var result: S = parse: {
        var arena_state: std.heap.ArenaAllocator = .init(std.testing.allocator);
        defer arena_state.deinit();

        const arena = arena_state.allocator();

        const dynamic = try std.json.parseFromSliceLeaky(std.json.Value, arena, json, .{});
        if (dynamic != .object) return error.InvalidJson;

        const dynamic_foo = dynamic.object.get("foo") orelse return error.InvalidJson;
        if (dynamic_foo != .array) return error.InvalidJson;
        const temp_foo_slice = try std.json.parseFromValueLeaky([]i32, arena, dynamic_foo, .{});

        const owned_foo_slice = try gpa.dupe(i32, temp_foo_slice);
        errdefer gpa.free(owned_foo_slice);

        const dynamic_bar = dynamic.object.get("bar") orelse return error.InvalidJson;
        if (dynamic_bar != .array) return error.InvalidJson;
        const temp_bar_slice = try std.json.parseFromValueLeaky([]f32, arena, dynamic_bar, .{});

        const owned_bar_slice = try gpa.dupe(f32, temp_bar_slice);
        errdefer comptime unreachable;

        break :parse .{
            .foo = .fromOwnedSlice(owned_foo_slice),
            .bar = .fromOwnedSlice(owned_bar_slice),
        };
    };
    defer {
        result.foo.deinit(gpa);
        result.bar.deinit(gpa);
    }

    // Assert that the lists are initially at full capacity,
    // but that expanding them by adding further items still works.
    try std.testing.expect(result.foo.items.len == result.foo.capacity);
    try std.testing.expect(result.bar.items.len == result.bar.capacity);
    try result.foo.append(gpa, 999);
    try result.bar.append(gpa, 55.55);

    try std.testing.expectEqualSlices(i32, &.{ 1, 2, 3, 4, 5, 999 }, result.foo.items);
    try std.testing.expectEqualSlices(f32, &.{ 1.25, 573.765, 55.55 }, result.bar.items);
}

There are probably better and more efficient ways of accomplishing this, such as using the low-level std.json.Scanner, but it gets the idea across. With clever use of @typeInfo and the isArrayList suggested above you can probably handle this in a generic manner that works for any struct. The sky’s the limit. The important detail is that the way std.json handles memory allocation means that you need to dupe allocated slices if you want to use them for array lists that may be resized in the future.

1 Like

Thank you. However, it seems like I’ll end up with implementing it for every struct I have. That was my main pain point with jsonParse/jsonSerialize methods…