Question

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;
}
Was it helpful?

Solution 2

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 );
}

OTHER TIPS

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.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top