Search code examples
c++lualua-tablelua-api

How can I read and write to nested lua tables from C++?


I have a nested table in my lua code that I want to pass to C++ so the native code can manipulate it:

-- Some persistent data in my game
local data = {
        { 44, 34, 0, 7, },
        { 4, 4, 1, 3, },
}
-- Pass it into a C++ function that can modify the input data.
TimelineEditor(data)

How do I write my C++ code to read the nested table and modify its values?

Reading Lua nested tables in C++ and lua c read nested tables both describe how I can read from nested tables, but not how to write to them.


Solution

  • Short answer

    Lua uses a stack to get values in and out of tables. To modify table values you'll need to push the table you want to modify with lua_rawgeti, push a value you want to insert with lua_pushinteger, and then set the value in the table with lua_rawseti.

    When writing this, it's important to visualize the stack to ensure you use the right indexes:

    lua_rawgeti()
        stack:
            table
    
    lua_rawgeti()
        stack:
            number <-- top of the stack
            table
    
    lua_tonumber()
        stack:
            number
            table
    
    lua_pop()
        stack:
            table
    
    lua_pushinteger()
        stack:
            number
            table
    
    lua_rawseti()
        stack:
            table
    

    Negative indexes are stack positions and positive indexes are argument positions. So we'll often pass -1 to access the table at the stack. When calling lua_rawseti to write to the table, we'll pass -2 since the table is under the value we're writing.

    Example

    I'll add inspect.lua to the lua code to print out the table values so we can see that the values are modified.

    local inspect = require "inspect"
    
    local data = {
            { 44, 34, 0, 7, },
            { 4, 4, 1, 3, },
    }
    print("BEFORE =", inspect(data, { depth = 5, }))
    TimelineEditor(data)
    print("AFTER =", inspect(data, { depth = 5, }))
    

    Assuming you've figured out BindingCodeToLua, you can implement the function like so:

    
    // Replace LOG with whatever you use for logging or use this:
    #define LOG(...) printf(__VA_ARGS__); printf("\n")
    
    // I bound with Lunar. I don't think it makes a difference for this example.
    int TimelineEditor(lua_State* L)
    {
        LOG("Read the values and print them out to show that it's working.");
        {
            int entries_table_idx = 1;
            luaL_checktype(L, entries_table_idx, LUA_TTABLE);
            int n_entries = static_cast<int>(lua_rawlen(L, entries_table_idx));
            LOG("%d entries", n_entries);
            for (int i = 1; i <= n_entries; ++i)
            {
                // Push inner table onto stack.
                lua_rawgeti(L, entries_table_idx, i);
                int item_table_idx = 1;
                luaL_checktype(L, -1, LUA_TTABLE);
                int n_items = static_cast<int>(lua_rawlen(L, -1));
                LOG("%d items", n_items);
                for (int i = 1; i <= n_items; ++i)
                {
                    // Push value from table onto stack.
                    lua_rawgeti(L, -1, i);
                    int is_number = 0;
                    // Read value
                    int x = static_cast<int>(lua_tonumberx(L, -1, &is_number));
                    if (!is_number)
                    {
                        // fire an error
                        luaL_checktype(L, -1, LUA_TNUMBER);
                    }
                    LOG("Got: %d", x);
                    // pop value off stack
                    lua_pop(L, 1);
                }
                // pop table off stack
                lua_pop(L, 1);
            }
        }
    
        LOG("Overwrite the values");
        {
            int entries_table_idx = 1;
            luaL_checktype(L, entries_table_idx, LUA_TTABLE);
            int n_entries = static_cast<int>(lua_rawlen(L, entries_table_idx));
            LOG("%d entries", n_entries);
            for (int i = 1; i <= n_entries; ++i)
            {
                // Push inner table onto stack.
                lua_rawgeti(L, entries_table_idx, i);
                int item_table_idx = 1;
                luaL_checktype(L, -1, LUA_TTABLE);
                int n_items = static_cast<int>(lua_rawlen(L, -1));
                LOG("%d items", n_items);
                for (int j = 1; j <= n_items; ++j)
                {
                    int x = j + 10;
                    // Push new value onto stack.
                    lua_pushinteger(L, x);
                    // rawseti pops the value off. Need to go -2 to get to the
                    // table because the value is on top.
                    lua_rawseti(L, -2, j);
                    LOG("Wrote: %d", x);
                }
                // pop table off stack
                lua_pop(L, 1);
            }
        }
        // No return values
        return 0;
    }
    

    Output:

    BEFORE =    { { 44, 34, 0, 7 }, { 4, 4, 1, 3 } }
    Read the values and print them out to show that it's working.
    2 entries
    4 items
    Got: 44
    Got: 34
    Got: 0
    Got: 7
    4 items
    Got: 4
    Got: 4
    Got: 1
    Got: 3
    Overwrite the values
    2 entries
    4 items
    Wrote: 11
    Wrote: 12
    Wrote: 13
    Wrote: 14
    4 items
    Wrote: 11
    Wrote: 12
    Wrote: 13
    Wrote: 14
    AFTER =     { { 11, 12, 13, 14 }, { 11, 12, 13, 14 } }