Search code examples
c++luatuples

Read lua object (metatable?) as std::tuple


I have some functions that can read tuples from Lua (5.4) stack, assuming that the stack contains a Lua table:

template <typename type_t>
type_t stack_value_as(lua_State *const L, int index)
{
    // template magic happens here…
    return {};
}

template <typename type_t>
type_t table_value_at(lua_State *const L, int table_stack_position, int index)
{
    lua_rawgeti(L, table_stack_position, index);
    auto result = stack_value_as<type_t>(L, -1);
    lua_pop(L, 1);
    return result;
}

template <typename ... type_pack>
auto read_tuple(lua_State *L, int index)
{
    using tuple_t = std::tuple<type_pack ...>;

    if (lua_istable(L, index))
    {
        const auto size_lua = lua_rawlen(L, index);
        constexpr auto size_cpp = sizeof...(type_pack);
        if (size_lua == size_cpp)
        {
            auto helper = [&]<int ... index_pack>(const std::integer_sequence<int, index_pack ...> &)
            {
                return tuple_t{ table_value_at<type_pack>(L, index, index_pack - index) ... };
            };
            return helper(std::make_integer_sequence<int, size_cpp>{});
        }
    }

    return tuple_t{};
}

When I test this code with the Lua script below it fails:

test = {e = 42, n = 3.1415, id = "test, test, 1, 2, 3..."};

-- call 1: ok
tup({1, 2.3456, "789"})
-- call 2 & 3: lua_istable is true, lua_rawlen is 0
tup(test)
tup({e = 42, n = 3.1415, id = "test, test, 1, 2, 3..."})

The function tup is tied to a C++ function that receives tuple<int, double, string> as parameter. When the stack contains a Lua object the lua_istable function returns truthy but the lua_rawlen returns 0. My guess is that in this case the content is a LUA metatable. How can I read those Lua objects (metatables?) as std::tuple?


Solution

  • About lua_rawlen == 0

    test = {e = 42, n = 3.1415, id = "test, test, 1, 2, 3..."};
    -- call 2 & 3: lua_istable is true, lua_rawlen is 0
    tup(test)
    

    In this snippet, lua_rawlen == 0 is correct according to Lua 5.4 reference manual.

    From lua_rawlen:

    Returns the raw "length" of the value at the given index: [...]; for tables, this is the result of the length operator ('#') with no metamethods;

    From the length operator:

    The length operator applied on a table returns a border in that table. A border in a table t is any non-negative integer that satisfies the following condition:

    (border == 0 or t[border] ~= nil) and
    (t[border + 1] == nil or border == math.maxinteger)
    

    In words, a border is any positive integer index present in the table that is followed by an absent index, plus two limit cases: zero, when index 1 is absent; and the maximum value for an integer, when that index is present. Note that keys that are not positive integers do not interfere with borders.

    In our case, test[i] == nil for any integer i, so 0 is the only border of this table. Conclusion: #test == 0.

    The TL,DR: in general, the length operator doesn't compute the number of (key/value) pairs of a Lua table. It does so with {1, 2.3456, "789"} because it is an array: all its keys are consecutive integers starting from 1. In any other case, the length operator doesn't have much practical value.

    tup({1, 2.3456, "789"}) vs tup(test)

    The way test was declared, all (key/value) pairs have string keys. An equivalent declaration would be:

    # test = {e = 42, n = 3.1415, id = "test, test, 1, 2, 3..."}
    test = {}
    test['e'] = 42
    test['n'] = 3.1415
    test['id'] = "test, test, 1, 2, 3..."
    

    Which is different from a table with integer keys:

    # {1, 2.3456, "789"}
    equivalent = {}
    equivalent[1] = 1
    equivalent[2] = 2.3456
    equivalent[3] = "789"
    

    Getting the correct number of (key/value) pairs in all cases is an easy fix. However for the tuple conversion there is a bigger problem. Fundamentally, Lua tables are similar to c++'s std::unordered_map. In {e = 42, n = 3.14, id = "test"} the pairs have no inherent order: ("e",42) isn't the 1st pair, nor is ("n",3.14) the 2nd. If you iterate over all (key/value) pairs of a Lua table in your c++ function, you can get them in a different order. You'll have to do some logic with the string keys to get a consistent order on the tuple's elements.