1

I'm trying to push a Lua class object onto the stack. The pointer to that object can be returned by multiple functions.

In other words: I need to push userdata values while still keeping the ability to use '==', '~=' etc. on them so the userdata pointer must be the same if its the same C++ object.

-- this should push the object onto the stack
local firstObject = GetClassObject();
firstObject:doSomething();

firstObject will be stored by the lua script and later in code i will need to do this again:

-- the c++ class pointer has not changed here
-- so I would like to push the same userdata pointer as in the first call...
local object = GetClassObject();

-- if I would not do this the following here would fail... :C
if object == firstObject then
...

My Push function should basically check if there is already the same C++ class pointer somewhere and push the associated userdata pointer if so (no matter how i push it, the object should work 1:1 the same)

If not it should create a new userdata (push it on the stack) and set the content of it to the class object.

Here's my code:

template <typename T>
void Push( const T &tObject )
{
    lua_State *L = GetLuaState();

    // Here i need to check if such a C++ object (the same tObject)
    // already exists!
    //
    // If so i want to push the associated userdata.


    // Object didn't exist yet -> we need a new userdata
    void *pUserData = lua_newuserdata( L, sizeof( tObject ) );
    *reinterpret_cast<T*>( pUserData ) = tObject;
}

template <typename T>
void Push( const T &tObject, const char *pszTable )
{
    Push( tObject );
    lua_State *L = GetLuaState();
    luaL_getmetatable( L, pszTable );
    lua_setmetatable( L, -2 );
}

template <typename T>
T& Get( int nIndex )
{
    T *pUserData = reinterpret_cast<T*>( lua_touserdata( GetLuaState(), nIndex ) );
    if( pUserData == nullptr )
        throw std::exception( "Invalid userdata!" );

    return *pUserData;
}

template <typename T>
T& Get( int nIndex, const char *pszTable )
{
    T *pUserData = reinterpret_cast<T*>( LuaToUData( nIndex, pszTable ) );
    if( pUserData == nullptr )
        throw std::exception( "Invalid userdata!" );

    return *pUserData;
}

LuaToUData is an own function which i wrote to not throw a lua error:

void* LuaToUData( int nIndex, const char *pszTable )
{
    void *pUserData = lua_touserdata( g_luaState, nIndex );
    if( pUserData != nullptr )
    {
        if( lua_getmetatable( g_luaState, nIndex ) != 0 )
        {
            lua_getfield( g_luaState, LUA_REGISTRYINDEX, pszTable );
            bool bEqual = ( lua_rawequal( g_luaState, -1, -2 ) == 1 );
            lua_pop( g_luaState, 2 );

            if( bEqual )
                return pUserData;
        }
    }

    return nullptr;
}
user1478081
  • 11
  • 1
  • 5
  • I am finding it quite difficult to work out exactly what you are trying to do here. Are you just trying to cache a lua userdata instance after you've created it? – Rook Jun 24 '12 at 14:46
  • I'm trying to push userdata values while still keeping the ability to use '==', '~=' etc. on them so the userdata pointer must be the same if its the same C++ object. – user1478081 Jun 24 '12 at 14:57
  • Lua userdata objects are compared by reference internally. Two userdata instances created from the same underlying pointer should compare as equal. Are you saying that `==` is not working in this case? And have you overridden the `__eq` metatable entry? – Rook Jun 24 '12 at 15:08
  • The '==' check fails in that case. No I did not override __eq... – user1478081 Jun 24 '12 at 15:10
  • Does it actually compare the userdatas content itself and checks it for equality ? – user1478081 Jun 24 '12 at 15:14
  • This check here does not seem to work: [code] for k, v in pairs( g_objectList ) do if k == object then return true; end end return false; – user1478081 Jun 24 '12 at 15:33
  • @Rook I just tried something else... `local frmMain, test = Frame.Create( "Test" );` `if frmMain == test then` `MessageBox( "Worked!" );` `end` ` I let `Frame.Create` do 2x `Push( pFrame )` and return 2 The == check fails. – user1478081 Jun 24 '12 at 18:05
  • It worked when I registered my class with an own equality handler, but It seems like lua doesn't check it itself. Are you sure you were talking about actual block userdata? It worked when my handler compared them... – user1478081 Jun 24 '12 at 18:47
  • Two references to the same userdata are identical; but what you're doing is creating two separate bits of userdata. Lua cannot tell that these two are identical, hence the need for __eq metamethods. It wasn't entirely clear from your original code what was going on. My answer below may not be entirely relevant in light of what you've just said. – Rook Jun 24 '12 at 19:09

2 Answers2

1

Right, in Lua, any two instance of the same userdata are guaranteed to be equal. However, when you are boxing up a C++ class instance as you are doing, each boxed instance gets put in a new userdatum, which means they are not directly comparable.

What you will need to do is to define a __eq metamethod for your object. It might look something a little like this:

int l_compare_things(lua_State* l)
{
    MyClass* a = reinterpret_cast<MyClass*>(lua_touserdata(L, 1));
    MyClass* b = reinterpret_cast<MyClass*>(lua_touserdata(L, 2));

    lua_pushboolean(L, (*a) == (*b));

    return 1;
}

This assumes that MyClass has some kind of operator== override. You can set this function as the __eq metamethod in the metatable that you've associated with your MyClass userdata items. You seem to already have metatable handling covered, so I won't bother with that here.

Now, next problem: you're boxing up entire class instances as lua full userdata items. You probably don't want to keep pushing the same thing over and over and use up all the memory available to you... this is less of a problem if you're only pushing pointers, but you're not doing that. So... you will need some unique way of identifying each instance of your C++ classes. Here's an example with a string:

class MyClass
{
private:
    std::string _id;
public:
    MyClass(const std::string& id) : _id(id) {}

    const std::string& get_id() { return _id; }

    // setters and operator= overrides not included.
};

void l_push_thing(lua_State* L, const MyClass& thing)
{
    // try to get our instance by ID from the registry table:
    lua_getfield(L, LUA_REGISTRYINDEX, thing.id());

    // if so, return, leaving it at the top of the stack.
    if (lua_isuserdata(L, -1))
        return;

    void *ud = lua_newuserdata(L, sizeof(MyClass));                       
    *reinterpret_cast<MyClass*>(ud) = thing; 
    // set up the metatable, etc

    // duplicate the userdata reference:
    lua_pushvalue(L, -1);

    // push our new userdata into the registry. pops the duplicate from the stack
    lua_setfield(L, LUA_REGISTRYINDEX, thing.get_id());
}

(note: I've not compiled or tested this example. E&OE!)

This will leave the userdatum associated with some particular MyClass instance at the top of the stack. You'll need to take your own steps to 'unregister' class instances; in this case, a hard reference to each instance exists in the registry, and so the userdatum will not be garbage collected until you destroy that reference. You might consider using weak/ephemeron tables here.

Rook
  • 5,734
  • 3
  • 34
  • 43
  • Thanks for that, sadly I have no idea how weak / ephemeron tables work what do they do? – user1478081 Jun 25 '12 at 19:21
  • I tried to map all the userdata along with their values in a std::map and add a own `__gc` method but it didn't work. I pushed the full userdata pointer as light userdata, that dsn't work :c – user1478081 Jun 25 '12 at 19:41
  • @user1478081 One step at a time. Don't worry about weak tables until your basic push system works correctly! I'm not sure what you're trying to accomplish with your `std::map`; were you able to make use of the code I have above? – Rook Jun 26 '12 at 09:29
  • Yeah, everything works but it causes a memory leak as you said. Look below to see how I tried to fix it :) – user1478081 Jun 26 '12 at 12:47
  • @user1478081: So that's a bit of good news, at least! Which version of lua are you using? – Rook Jun 26 '12 at 13:33
  • At the moment I'm using lua 5.2 - not 5.2.1 yet :) – user1478081 Jun 26 '12 at 14:09
0

Is that how weak tables work ?

void Push( const T &tObject )
{
    std::ostringstream o;
    o << tObject;
    std::string sIdentifier = o.str();
    const char *pszIdentifier = sIdentifier.c_str();

    lua_State *L = GetLuaState();
    luaL_getmetatable( L, "lua_userdata" );
    if( !lua_istable( L, -1 ) )
    {
        // create new weak table
        luaL_newmetatable( L, "lua_userdata" );
        lua_pushstring( L, "v" );
        lua_setfield( L, -2, "__mode" );
    }

    lua_getfield( L, -1, pszIdentifier );
    if( lua_isuserdata( L, -1 ) == TRUE )
        return lua_remove( L, -2 );

    lua_pop( L, 1 ); // didnt exist yet - getfield is nil -> need to pop that
    void *pUserData = lua_newuserdata( L, sizeof( UINT64 ) );
    *reinterpret_cast<UINT64*>( pUserData ) = UINT64( tObject );

    lua_pushvalue( L, -1 );
    lua_setfield( L, -3, pszIdentifier );
    lua_remove( L, -2 );
}
user1478081
  • 11
  • 1
  • 5
  • Please do not forget that userdata cached in weak table may still be there after first userdata garbage collection phase and needs additional check, which form is dependent on how an actual pointer to class instance is stored in udata chunk. Simply checking for non-nil value misses that case. If udata contains a single pointer, it must be checked against NULL, because that's what __gc metamethod should probably do. Otherwise you return resurrected but dead objects back to the interpreter. – user3125367 Feb 18 '14 at 21:34