Skip to content
Stjepan Bakrac edited this page May 22, 2020 · 4 revisions

Structs are defined by the struct.struct method. The first, optional parameter is a metadata table. The second argument is a table describing the fields of the struct.

The table is a key-value mapping of final struct fields and their respective descriptions:

struct.struct({
    field_a = description_a,
    field_b = description_b,
    field_c = description_c,
    --[[ ... ]]
})

A description is always a table that can have one of three formats:

  1. Type fields: field_c = {type = some_ftype}
  2. Getter fields: field_a = {get = some_function}
  3. Data fields: field_b = {data = anything}

Type fields

These kind of struct fields define a simple C-based field on the struct:

struct.struct({
    int_field           = {type = struct.int32},
    float_field         = {type = struct.float},
    string_field        = {type = struct.string(0x10)},
})

This would result in the following C struct:

struct {
    int32_t int_field;
    float float_field;
    char string_field[16];
};

These fields are, by far, the most common. As such they have a shortcut, by omitting the type key. The struct from before can be rewritten as follows:

struct.struct({
    int_field           = {struct.int32},
    float_field         = {struct.float},
    string_field        = {struct.string(0x10)},
})

Optionally a position can be provided where to place the field (if memory layout matters, such as for packets or emulating in-game memory structs). The position can either be specified with a position key or by omitting the key and placing it in the first position of the table, before the type:

struct.struct({
    int_field           = {0x00, struct.int32},
    float_field         = {type = struct.float, position = 0x10},
    string_field        = {struct.string(0x10), position = 0x2C},
})

The table created here is almost identical to the first two, except that the position of the field is not contiguous. In cases like this, with gaps in the struct field positions the intermediate space is filled up with dummy fields:

struct {
    int32_t int_field;
    char __1[12];
    float float_field;
    char __2[20];
    char string_field[16];
};

Getter fields

This kind of field description results in a getter metamethod on the struct which, when accessed, calls the provided function with the cdata itself as an argument. It is used to create logical fields that can be calculated based on other, existing fields and possibly other upvalues:

struct.struct({
    first               = {struct.int32},
    second              = {struct.int32},
    max                 = {get = function(data)
        return math.max(data.first, data.second)
    end},
})

This results in the same C struct as if it was defined without the getter:

struct {
    int32_t first;
    int32_t second;
};

However, this struct is assigned a metamethod which, when the dummy field max is accessed, invokes the function with the cdata object:

local object = struct.new(struct.struct({
    first               = {struct.int32},
    second              = {struct.int32},
    max                 = {get = function(data)
        return math.max(data.first, data.second)
    end},
}))

object.first = 3
object.second = 7
print(object.max) -- 7

In our libraries this is commonly used to define resource-lookups in a struct:

local object = struct.new(struct.struct({
    zone_id             = {struct.int16},
    zone                = {get = function(data)
        return client_data.zones[data.zone_id]
    end}
}))

object.zone_id = 101
print(object.zone.name) -- East Ronfaure

Data fields

This kind of field description simply assigns the provided value to the field and returns it when accessed. As with the getter, this is done via metamethods and does not show up in the C definition. This has the advantage of being able to hold any type, like regular Lua tables. In our libraries it is most often used for events:

local object = struct.new(struct.struct({
    zone_change         = {data = event.new()},
}))

packet.incoming[0x00A]:register(function()
    object.zone_change:trigger()
end)

Field metadata

Fields can take any other metadata. This can later be used when dealing with the ftype:

local ftype = struct.struct({
    int_field           = {struct.int32, order = 'first'},
    float_field         = {struct.float, order = 'third', optional = true},
    bool_field          = {struct.bool, order = 'second', true_value = 3, false_value = -1},
})

local object = struct.new(ftype)

local assign_bool = function(object, ftype, value)
    object.bool_field = value and ftype.fields.bool_field.true_value or ftype.fields.bool_field.false_value
end

assign_bool(object, ftype, false) -- assigns -1 to the bool_field

In this case the keys order, optional, true_value and false_value are not part of our API and do nothing by default. But they do persist and can be used outside of our API, as displayed above.

The following are all keys used by the struct library and should not be used by developers:

  • type, position, get, data as defined above
  • offset used for bit-packed values that don't start at an 8-bit boundary
  • label used to represent the outside visible key to use as the index on the resulting struct
  • cname used as the internal key to the cdata, which can be different from label in certain cases (like reserved keyword types, or converter fields)
  • internal used to designate fields internal to the struct library and not for outside use (used for VLA delimiters)
Clone this wiki locally