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.
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:
LEngine::runScript
you make all lightuserdata behave likeTMeasurement
objects. This is ok for the value in the global variablestep
.step.min
for the first time, you make all lightuserdata behave likeTLimit
objects (inlmeas_get
). This is ok for the value pushed bystep.min
, but now the value instep
also behaves like aTLimit
, sostep.min
for the second time,step
acts as aTLimit
object, so it doesn't have a fieldmin
and returnsnil
.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 yourTMeasurement
object. You also set a new table as uservalue (seelua_setuservalue
) which will act as a cache for the sub-userdata. When yourlmeas_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 theTLimit
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 theTLimit
sub-userdata to the parentTMeasurement
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.