Search code examples
clua

How to use _newindex arrays I always get bad arguments


I'm writing C code to check and modify struct members in lua, I'm using metatables

It works for simple definitions, but in array I couldn't make it work, I have the following code

struct pinfo {
    int time;
    bool hide_name;
    int type_info[10];
};

struct mystruct {
    int id;
    int value;
    int option[20];
    struct pinfo pf[5];
    ....
};


int Get_mystruct(lua_State *L) {
    struct mystruct *ms = (struct mystruct*)lua_touserdata(L, 1);

    lua_pushlightuserdata(L, ms);
    luaL_getmetatable(L, "mystruct");
    lua_setmetatable(L, -2);

    return 1;
}

int mystruct_index(lua_State *L) {
    struct mystruct *ms = (struct mystruct*)lua_touserdata(L, 1);
    const char *field = lua_tostring(L, 2);


    if (strcmp(field, "id") == 0) {
        lua_pushinteger(L, ms->id);
    }
    else if (strcmp(field, "value") == 0) {
        lua_pushinteger(L, ms->value);
    }
    else if (strcmp(field, "option") == 0) {
        lua_pushinteger(L, ms->option[lua_tointeger(L, 3)]);
    }
    else {
        lua_pushnil(L);
    }

    return 1;
}
    
int mystruct_newindex(lua_State  *L) {
    struct mystruct *ms = (struct mystruct*)lua_touserdata(L, 1);
    const char *field = lua_tostring(L, 2);
    int value = (int)lua_tointeger(L, 3);


    if (strcmp(field, "id") == 0) {
        ms->id = value;
    }
    else if (strcmp(field, "value") == 0) {
        ms->value = value;
    }
    else if (strcmp(field, "option") == 0) {
        ms->option[lua_tointeger(L, 3)] = (int)lua_tointeger(L, 4);
    } else {
        lua_pushnil(L);
    }

    return 0;
}

void mystruct_create(lua_State *L) {
    luaL_Reg mystructData[] = {
        { "GetMyStruct", Get_mystruct},
        { "__index", mystruct_index},
        { "__newindex", mystruct_newindex},
        {NULL, NULL}
    };

    luaL_newmetatable(L, "mystruct");
    luaL_setfuncs(L, mystructData, 0);
    lua_setfield(L, -2, "MyStructLua");
}

In lua I use it as follows

local ms = MyStructLua.GetMyStruct(my_ptr)

print(ms.id) // Returns 0, not defined
ms.id = 1000 
print(ms.id) // Returns 1000, has been defined

Now when I go to define the array a problem occurs when I try for example

for o = 0, 19 do
    mp.option[o] = 1
end

When I go to set the value I get the error bad argument #3 to 'index' (number expected, got no value)

I would like to know how I can work with simple and complex arrays in this metatable, I have struct which is a member array of mystruct how can I also add it to _newindex and _index


Solution

  • Oka has pointed out that a.b[o] = d contains two index operations, so you have to create metatables for each array. BTW, as I commented under the previous deleted question, I suggest that you use third-party libraries to complete the binding, as it is quite complex to implement. I will put aside the pf field for now, the basic principle is the same. Let's begin with a mistake:

    lua_pushlightuserdata(L, ms);
    luaL_getmetatable(L, "mystruct");
    lua_setmetatable(L, -2);
    

    This looks quite reasonable, but I believe the result is not what you want, that's because from the manual you can read:

    Tables and full userdata have individual metatables, although multiple tables and userdata can share their metatables. Values of all other types share one single metatable per type; that is, there is one single metatable for all numbers, one for all strings, etc. By default, a value has no metatable, but the string library sets a metatable for the string type.

    If you set the metatable mystruct for your struct, you cannot set another metatable for other arrays. The solution is to create full userdata to store the struct and the arrays. You just need to save pointers, so I write two functions to simplify these operations.

    void newpointerud(lua_State* L, const char* mt, int nuv, const void* data) {
        auto p = (const void**)lua_newuserdatauv(L, sizeof(void*), nuv);
        luaL_setmetatable(L, mt);
        *p = data;
    }
    
    void* getpointerud(lua_State* L, int idx) {
        return *(void**)lua_touserdata(L, 1);
    }
    

    Next, you need to write the corresponding meta methods for int[] and create a metatable.

    // ..options[idx]
    int mt_c_int_array_index(lua_State* L) {
        int* options = (int*)getpointerud(L, 1);
        int idx = lua_tointeger(L, 2);
        if (idx >= 0 && idx < 20)
            lua_pushinteger(L, *(options + idx));
        else
            lua_pushnil(L);
        return 1;
    }
    
    // ..options[idx] = value
    int mt_c_int_array_newindex(lua_State* L) {
        int* options = (int*)getpointerud(L, 1);
        int idx = lua_tointeger(L, 2);
        if (idx >= 0 && idx < 20)
            *(options + idx) = lua_tointeger(L, 3);
        else
            luaL_error(L, "Out of index");
        return 0;
    }
    
    .....
    luaL_Reg mt_c_int_array[] = {
        { "__index", mt_c_int_array_index },
        { "__newindex", mt_c_int_array_newindex },
        {NULL, NULL}
    };
    luaL_newmetatable(L, "mt_c_int_array");
    luaL_setfuncs(L, mt_c_int_array, 0);
    

    Third is to use full userdata to package your struct and arrays. Here, you usually don't want to create a new userdata every time you access the option array. You can use the user value of the userdata to pre save the userdata of the option array.

    newpointerud(L, "mystruct", 1, ms); // 1 user value
    newpointerud(L, "mt_c_int_array", 0, ms->option);
    lua_setiuservalue(L, -2, 1);
    

    Finally in your mystruct_index function, return this user value.

    ....
    else if (strcmp(field, "option") == 0) {
        lua_getiuservalue(L, 1, 1);
    }
    ....