How to set struct field with runtime values?

Is it possible to do something like:

self[some_key] = some_value

When the key is not known at compile time nor the value is.

I think what you want is hash map? Struct fields are always known compile time so I’m not sure what you mean.

1 Like

Can you specify what you mean by key? In your line it looks like you are doing an indexing operation, structs generally don’t support that with the exception of anonymous structs aka tuples. Those can be inferred to arrays and then indexed. You can add new ā€œfieldsā€ to tuples using array concatenation (++ operator), but this is a compiletime-operation.

Like Cloudef said it looks like what you want is a Hashmap which is the standard way of associating ie. runtime-known strings to runtime-known values.

2 Likes

Thanks for the reply. Yeah, I was talking about an indexing operation. For example in JS you can do this:

const obj = {
 a: 'aa',
 someFunction: function() {...},
}; 
obj['b'] = 'bb';

// or from inside a js class:

class A {
    someField = 5;

    constructor(newValue) {
        this['someField'] = newValue;
    }
}

const a = new A(6);

console.log(a);


And I was wondering if I can achieve the same thing with structs in Zig which should be the equivalent of a JS obj ā€˜{}’ right?

1 Like

This is not directly possible in Zig. Though I believe you can use a StringArrayHashMap(std.json.Value) to approximate json data. You can append to that keys which are only runtime known. You can also see the declaration of std.json.ObjectMap, which is a StringArrayHashMap as it is used to store data for a JSON value.

1 Like

It’s important to remember that JavaScript is interpreted and not compiled. What we’re referring to here is dynamically added properties - dynamic in this case being closely associated to runtime. After reading about it, it breaks down like this…

As of V8, JavaScript actually uses a kind of fragmented object. A simple visualization would be something like tables. One table contains properties, another table contains the elements, and there’s a table in the middle that combines a property record with an element record (again, this is a visualization tool and not a technical breakdown of the backend). With this setup, you can see that the ā€œobjectā€ is just a combination of two other containers: properties and elements. These are often described as ā€œhidden objectsā€ (apparently) where the hidden object actually contains metadata about the shape and form of another object.

You can certainly build something like this in Zig (or any language for that mattter), but it’s not natively supported because there’s a lot of overhead to this beyond just performance concerns.

A rough first approximation here is like what @mscott9437 is talking about where you have something like a hash-map. That hash map would need to point to a generic type that can have active members selected (like a union) or a generic type-erased object that can be casted to the type itself (like an inline buffer with a dynamic dispatch fallback). I’d be happy to show you how type-erased objects are built in Zig, but that’s a different thread :slight_smile:

To really get what you want here… a structure that uses RTTI (run time type information) to generate efficient offsets into your data is more work. Here’s a first approximation of how that would work…

First, you have an object… let’s call it ObjMetaData that contains some kind of map (could be a hash map, could be a pairwise array, or something more clever) that has two elements for each pair:

  1. A name,
  2. An Offset

Then, there’s a second object, let’s call it ObjStorage that has a buffer of bytes that are aliged to some common offset for the hardware. That raw-byte data stores the bytes of whatever you want.

Then your actual Object class can combine the two and provide an interface. That interface would lookup a name and get an offset from the metadata. Then that offset would be used to index the buffer and cast that portion to the variable type you want.

Not recommending this, btw - it’s just a thought experiment.

5 Likes

To me this topic seems more like ā€œwhat you want to do is what you were used toā€,
but Javascript is quite a different language from zig. It seems important to me to point out that a Javascript style of solving a problem doesn’t necessarily align well with a zig style of solving a problem, they can be bridged, the question for me is that really necessary?

Before trying to turn zig into javascript, I want to ask this question:
Are you really sure that it all needs to be runtime?

Can you give us more of the actual problem description you are trying to solve, regardless of how you solved it in javascript, so that we can suggest alternative solutions that are easier in the context of zig?

For example if your problem is changed to:
self[some_string_key] = a_value_that_has_one_of_a_few_different_types

It is easily solvable in zig by combining a StringHashMap with a custom tagged union for the value. Or if your values have only one type than the answer simply becomes use a StringHashMap.

4 Likes

It’s possible with the help of anytype:

const std = @import("std");

pub fn StructTag(comptime T: type) type {
    switch (@typeInfo(T)) {
        .Struct => |st| {
            var enum_fields: [st.fields.len]std.builtin.Type.EnumField = undefined;
            inline for (st.fields, 0..) |field, index| {
                enum_fields[index] = .{
                    .name = field.name,
                    .value = index,
                };
            }
            return @Type(.{
                .Enum = .{
                    .tag_type = u16,
                    .fields = &enum_fields,
                    .decls = &.{},
                    .is_exhaustive = true,
                },
            });
        },
        else => @compileError("Not a struct"),
    }
}

pub fn setField(ptr: anytype, tag: StructTag(@TypeOf(ptr.*)), value: anytype) void {
    const T = @TypeOf(value);
    const st = @typeInfo(@TypeOf(ptr.*)).Struct;
    inline for (st.fields, 0..) |field, index| {
        if (tag == @as(@TypeOf(tag), @enumFromInt(index))) {
            if (field.type == T) {
                @field(ptr.*, field.name) = value;
            } else {
                @panic("Type mismatch: " ++ @typeName(field.type) ++ " != " ++ @typeName(T));
            }
        }
    }
}

pub fn main() void {
    const S = struct {
        int_a: i32 = 0,
        int_b: i32 = 0,
        int_c: i32 = 0,
        float_a: f64 = 0,
        float_b: f64 = 0,
    };
    var s: S = .{};
    var tag: StructTag(S) = .int_a;
    var i: i32 = 1234;
    var f: f64 = 3.14;
    setField(&s, tag, i);
    std.debug.print("{any}\n", .{s});
    tag = .float_b;
    setField(&s, tag, f);
    std.debug.print("{any}\n", .{s});
    i = 333;
    tag = .int_c;
    setField(&s, tag, i);
    std.debug.print("{any}\n", .{s});
    f *= 2;
    tag = .float_a;
    setField(&s, tag, f);
    std.debug.print("{any}\n", .{s});
}
2 Likes

Why not use @field(st, name) at that point?
EDIT: nvm, point being runtime here