Search code examples
c++objectluametatablelua-api

Lua 5.2 - C++ object within an object (using lua_lightuserdata)


edit: [SOLUTION IN ANSWER 2]

I am new to LUA and am having trouble trying to do what I want. I have a C++ object that looks like this:

C++ Object definitions

struct TLimit
{
    bool   enabled;
    double value;

    TLimit() : enabled(false), value(0.0) {}
    ~TLimit() {}
};

class TMeaurement
{
public:
    TMeasurement() : meas(0.0) {}
    ~TMeasurement() {}

    TLimit min;
    TLimit max;
    double meas;
};

I want to be able in LUA to access an object of type TMeasurement in the following form:

LUA desired use

-- objmeas is an instance of TMeasurement
objmeas.min.enabled = true
print(objmeas.min.value);

...etc

The other one thing, I do not want LUA to allocate memory for the instance of the object of type TMeasurement. That will be done in my C++ code. I have tried many different things, all unsuccessful. I will post now the last of my tries.

In my C++ code, I have defined the following:

TLimit - Get function that will be mapped to __index

#define LUA_MEAS_LIMIT    "itse.measurement.limit"

extern int llim_get(lua_State* L)
{
    TLimit*     lim = (TLimit*)lua_chekuserdata(L, 1, LUA_MEAS_LIMIT);
    std::string key = std::string(luaL_checkstring(L, 2));

    //-- this is only to check what is going on
    std::cout << "lim.get: " << key << std::endl;

    if(key.find("enabled") == 0)
        lua_pushboolean(L, lim->enabled);
    else if(key.find("value") == 0)
        lua_pushnumber(L, lim->value);
    else
        return 0;   //-- should return some sort of error, but let me get this working first

    return 1;
}

TLimit - Set function that will be mapped to __newindex

extern int llim_set(lua_State* L)
{
    TLimit*     lim = (TLimit*)lua_chekuserdata(L, 1, LUA_MEAS_LIMIT);
    std::string key = std::string(luaL_checkstring(L, 2));

    //-- this is only to check what is going on
    std::cout << "limit.set: " << key << " <-" << std::endl;

    if(key.find("enabled") == 0)
        lim->enabled = lua_toboolean(L, 3);
    else if(key.find("value") == 0)
        lim->value = lua_tonumber(L, 3);

    return 0;
}

Now, one more functions for the TMeasurement class. (I will not provide in this example the set function for member "meas").

TMeasurement - Get function for __index

#define LUA_MEASUREMENT    "itse.measurement"

extern int lmeas_get(lua_State* L)
{
    TMeasurement* test = (TMeasurement*)lua_checkuserdata(L, 1, LUA_MEASUREMENT);
    std::string   key  = std::string(luaL_checkstring(L, 2));

    //-- this is only to check what is going on
    std::cout << "meas." << key << " ->" << std::endl;

    if(key.find("meas") == 0)
        lua_pushinteger(L, test->meas);
    else if(key.find("min") == 0)
    {
        lua_pushlightuserdata(L, &test->min);
        luaL_getmetatable(L, LUA_MEAS_LIMIT);
        lua_setmetatable(L, -2);
    }
    else if(key.find("max") == 0)
    {
        lua_pushlightuserdata(L, &test->max);
        luaL_getmetatable(L, LUA_MEAS_LIMIT);
        lua_setmetatable(L, -2);
    }
    else
        return 0;  //-- should notify of some error... when I make it work

    return 1;
}

Now, the part in the code that creates the mettatables for these two objects:

C++ - Publish the metatables

(never mind the nsLUA::safeFunction<...> bit, it is just a template function that will execute the function within the < > in a "safe mode"... it will pop-up a MessaegBox when an error is encountered)

static const luaL_Reg lmeas_limit_f[] = { { NULL, NULL} };
static const luaL_Reg lmeas_limit[] =
{
        { "__index",    nsLUA::safeFunction<llim_get> },
        { "__newindex", nsLUA::safeFunction<lllim_set> },
        { NULL,      NULL }
};
//-----------------------------------------------------------------------------

static const luaL_Reg lmeas_f[] =  { { NULL, NULL} };
static const luaL_Reg lmeas[] =
{
        { "__index", nsLUA::safeFunction<lmeas_get> },
        { NULL,   NULL }
};
//-----------------------------------------------------------------------------

int luaopen_meas(lua_State* L)
{
    //-- Create Measurement Limit Table
    luaL_newmetatable(L, LUA_MEAS_LIMIT);
    luaL_setfuncs(L, lmeas_limit, 0);
    luaL_newlib(L, lmeas_limit_f);

    //-- Create Measurement Table
    luaL_newmetatable(L, LUA_MEASUREMENT);
    luaL_setfuncs(L, lmeas, 0);
    luaL_newlib(L, lmeas_f);

    return 1;
}

Finally, my main function in C++, initializes LUA, creates and instance of object TMeasurement, passes it to LUA as a global and executes a lua script. Most of this functionality is enclosed in another class named LEngine:

C++ - Main function

int main(int argc, char* argv[])
{
    if(argc < 2)
        return show_help();

    nsLUA::LEngine eng;

    eng.runScript(std::string(argv[1]));

    return 0;
}
//-----------------------------------------------------------------------------

int LEngine::runScript(std::string scrName)
{
    //-- This initialices LUA engine, openlibs, etc if not already done. It also
    //   registers whatever library I tell it so by calling appropriate "luaL_requiref"
    luaInit();

    if(m_lua)    //-- m_lua is the lua_State*, member of LEngine, and initialized in luaInit()
    {
        LMeasurement measurement;

        measurement.value = 4.5;   //-- for testing purposes

        lua_pushlightuserdata(m_lua, &tst);
        luaL_getmetatable(m_lua, LUA_MEASUREMENT);
        lua_setmetatable(m_lua, -2);
        lua_setglobal(m_lua, "step");

        if(luaL_loadfile(m_lua, scrName.c_str()) || lua_pcall(m_lua, 0, 0, 0))
            processLuaError();   //-- Pops-up a messagebox with the error
    }

    return 0;
}

Now, at last the problem. Whe I execute whatever lua script, I can access step no problem, but I can only access a memebr within "min" or "max" the first time... any subsequent access gives an error.

LUA - example one

print(step.meas);        -- Ok
print(step.min.enabled); -- Ok
print(step.min.enabled); -- Error: attempt to index field 'min' (a nil value)

The output generated by this script is:

                              first script line: print(step.meas);
meas.meas ->                     this comes from lmeas_get function
4.5                              this is the actual print from lua sentence
                              second script line: print(step.min.enabled)
meas.min ->                      accessing step.min, call to function lmeas_get
limit.get: enabled ->            accessing min.enabled, call to function llim_get
false                            actual print from script sentence
                              third script line: print(step.min.enabled)
limit.get: min ->                accessing min from limit object, call to llim_get ???????

So. After the first time I access the field 'min' (or 'max' for that matter), any subsequent attempts to acess it will return "attempt to access index..." error. It doesn't matter whether I access first the __index (local e = step.min.enabled) function or the __newindex function (step.min.enabled = true).

It seems that I mess up the LUA stack the first time I access the min metatble of object step. It somehow "replaces" the "pointer to step" from a LUA_MEASUREMENT metatable to a LUA_MEAS_LIMIT... and I simply don't know why.

Please help... what is it that I am messing up so much?

Thank you and sorry for the long post... I just don't know how to make it shorter.


Solution

  • As already mentioned in the comments, all lightuserdata share a single metatable (see here), so all lightuserdata values are treated exactly the same at all times. If you change the metatable for one lightuserdata then it changes for all of them. And this is what happens in your code:

    1. In LEngine::runScript you make all lightuserdata behave like TMeasurement objects. This is ok for the value in the global variable step.
    2. When you access step.min for the first time, you make all lightuserdata behave like TLimit objects (in lmeas_get). This is ok for the value pushed by step.min, but now the value in step also behaves like a TLimit, so
    3. when you try to access step.min for the second time, step acts as a TLimit object, so it doesn't have a field min and returns nil.

    Lightuserdata is simply not the right tool for the job. See e.g. this discussion for cases where lightuserdata can be used. For everything else you need full userdata. This will allocate some extra memory compared to lightuserdata (sorry, can't be helped), but you can do some caching to avoid generating too many temporaries.

    So for your step value you use a full userdata holding a pointer to your TMeasurement object. You also set a new table as uservalue (see lua_setuservalue) which will act as a cache for the sub-userdata. When your lmeas_get is called with a "min"/"max" argument, you look in the uservalue table using the same key. If you don't find a pre-existing userdata for this field, you create a new full userdata holding a pointer to the TLimit sub-object (using an appropriate metatable), put it in the cache, and return it. If your object lifetimes get more complicated in the future, you should add a back reference from the TLimit sub-userdata to the parent TMeasurement userdata to ensure that the later isn't garbage-collected until all references to the former are gone as well. You can use uservalue tables for that, too.