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;
}
9
  • 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? Commented Jun 24, 2012 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. Commented Jun 24, 2012 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? Commented Jun 24, 2012 at 15:08
  • The '==' check fails in that case. No I did not override __eq... Commented Jun 24, 2012 at 15:10
  • Does it actually compare the userdatas content itself and checks it for equality ? Commented Jun 24, 2012 at 15:14

2 Answers 2

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.

Sign up to request clarification or add additional context in comments.

6 Comments

Thanks for that, sadly I have no idea how weak / ephemeron tables work what do they do?
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 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?
Yeah, everything works but it causes a memory leak as you said. Look below to see how I tried to fix it :)
@user1478081: So that's a bit of good news, at least! Which version of lua are you using?
|
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 );
}

1 Comment

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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.