c++luaswiglua-userdata

How to pass userdata from one Lua chunk to another in C++


I'm trying to get userdata from a Lua script(chunk A) in C++(through a returned variable from function in my example) and then, later pass this userdata back to Lua script(chunk B) from C++(through a function argument in my example) so the userdata can be used in chunk B as it was in the chunk A.

MyBindings.h

class Vec2
{
public:
    Vec2():x(0), y(0){};
    Vec2(float x, float y):x(x), y(y){};
    float x, y;
};

MyBindings.i

%module my
%{
    #include "MyBindings.h"
%}

%include "MyBindings.h"

main.cpp

#include <iostream>
#include <lua.hpp>

extern "C"
{
    int luaopen_my(lua_State *L);
}

int main()
{
    lua_State *L = luaL_newstate();
    luaL_openlibs(L);
    luaopen_my(L);
    lua_settop(L, 0);
    /* chunk A */
    luaL_dostring(L, "local vec2 = my.Vec2(3, 4)\n"
                     "function setup()\n"
                       "return vec2\n"
                     "end\n");
    /* chunk B */
    luaL_dostring(L, "function test(p)\n"
                       "print(p.x)\n"
                     "end\n");
    void *userDataPtr = nullptr;

    /* call setup function */
    int top = lua_gettop(L);
    lua_getglobal(L, "setup");
    if (lua_pcall(L, 0, LUA_MULTRET, 0))
    {
        std::cout << lua_tostring(L, -1) << '\n';
        lua_pop(L, 1);
    }
    /* check the return value */
    if (lua_gettop(L) - top)
    {
        /* store userdata to a pointer */
        if (lua_isuserdata(L, -1))
            userDataPtr = lua_touserdata(L, -1);
    }
    /* check if userDataPtr is valid */
    if (userDataPtr != nullptr)
    {
        /* call test function */
        lua_getglobal(L, "test");
        lua_pushlightuserdata(L, userDataPtr); /* pass userdata as an argument */
        if (lua_pcall(L, 1, 0, 0))
        {
            std::cout << lua_tostring(L, -1) << '\n';
            lua_pop(L, 1);
        }
    }
    lua_close(L);
}

The Result I get :

[string "local vec2 = my.Vec2(3, 4)..."]:6: attempt to index a userdata value (local 'p')

The Result I expect :

3

Is it possible to get userdata from chunk A and then pass this to chunk B so it can be used like it was in chunk A?


Solution

  • In addition to the other answer I would like to show a variant where you store the a reference to the value in the Lua registry. The advantage of this approach is that you don't have to keep the value on the stack and think about what the offset will be. See also 27.3.2 – References in “Programming in Lua”.

    This approach uses three functions:

    1. int luaL_ref (lua_State *L, int t);

      Pops from the stack the uppermost value, stores it into the table at index t and returns the index the value has in that table. Hence to save a value in the registry we use

      userDataRef = luaL_ref(L, LUA_REGISTRYINDEX);
      
    2. int lua_rawgeti (lua_State *L, int index, lua_Integer n);

      Pushes onto the stack the value of the element n of the table at index (t[n] in Lua). Hence to retrieve a value at index userDataRef from the registry we use

      lua_rawgeti(L, LUA_REGISTRYINDEX, userDataRef);
      
    3. void luaL_unref (lua_State *L, int t, int ref);

      Removes the reference stored at index ref in the table at t such that the reference can be garbage collected and the index ref can be reused. Hence to remove the reference userDataRef from the registry we use

      luaL_unref(L, LUA_REGISTRYINDEX, userDataRef);
      
    #include <iostream>
    #include <lua.hpp>
    
    extern "C" {
    int luaopen_my(lua_State *L);
    }
    
    int main() {
        lua_State *L = luaL_newstate();
        luaL_openlibs(L);
        luaopen_my(L);
        lua_settop(L, 0);
        /* chunk A */
        luaL_dostring(L, "local vec2 = my.Vec2(3, 4)\n"
                         "function setup()\n"
                           "return vec2\n"
                         "end\n");
        /* chunk B */
        luaL_dostring(L, "function test(p)\n"
                           "print(p.x)\n"
                         "end\n");
        int userDataRef = LUA_NOREF;
    
        /* call setup function */
        int top = lua_gettop(L);
        lua_getglobal(L, "setup");
        if (lua_pcall(L, 0, LUA_MULTRET, 0)) {
            std::cout << lua_tostring(L, -1) << '\n';
            lua_pop(L, 1);
        }
        /* check the return value */
        if (lua_gettop(L) - top) {
            /* store userdata to a pointer */
            userDataRef = luaL_ref(L, LUA_REGISTRYINDEX);
        }
    
        /* check if userDataRef is valid */
        if (userDataRef != LUA_NOREF && userDataRef != LUA_REFNIL) {
            /* call test function */
            lua_getglobal(L, "test");
            lua_rawgeti(L, LUA_REGISTRYINDEX, userDataRef);
    
            /* free the registry slot (if you are done) */
            luaL_unref(L, LUA_REGISTRYINDEX, userDataRef);
    
            if (lua_pcall(L, 1, 0, 0)) {
                std::cout << lua_tostring(L, -1) << '\n';
                lua_pop(L, 1);
            }
        }
        lua_close(L);
    }
    

    Maybe you want to check out the Sol2 wrapper for the Lua-C-API. It can do exactly what you want with minimal boilerplate. However, it requires C++14.

    #include <iostream>
    
    #define SOL_CHECK_ARGUMENTS 1
    #include <sol.hpp>
    
    extern "C" int luaopen_my(lua_State *L);
    
    int main() {
        sol::state L;
        L.open_libraries();
        luaopen_my(L);
    
        /* chunk A */
        L.script("local vec2 = my.Vec2(3, 4)\n"
                 "function setup()\n"
                   "return vec2\n"
                 "end\n");
        /* chunk B */
        L.script("function test(p)\n"
                   "print(p.x)\n"
                 "end\n");
    
        auto userDataRef = L["setup"]();
        L["test"](userDataRef);
    }