1

I wonder if it's possible to access the variables of the class that runs the Lua script from the bound C++ class that is being used in Lua script.

From the example below, I wonder if it's possible to access name variable in myLua class from the bound Test class somehow.

Here are my codes.

main.cpp :

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

class myLua {

public:

    struct myData
    {
        std::string name;
        lua_State *L;
    };
    myLua(std::string name)
    {
        data = make_shared<myData>();
        data->name = name;
        data->L = luaL_newstate();
        lua_State *L = data->L;
        luaL_openlibs(L);
        luaopen_my(L);
        lua_settop(L, 0);

        const char *script =
        "function setup() \
           test = my.Test() \
           test:callHello() \
         end \
         function hello(name) \
           print('hello is called by : ' .. name) \
         end";
        //------------Added----------------
        lua_pushlightuserdata(L, data.get());
        myLua::myData *b = static_cast<myLua::myData *>(lua_touserdata(L, 1));
        cout << "RESULT1 : " << b->name << endl;
        //---------------------------------

        const int ret = luaL_loadstring(L, script);
        if (ret != 0 || lua_pcall(L, 0, LUA_MULTRET, 0) != 0)
        {
            std::cout << "failed to run lua script" << std::endl;
            return;
        }
        lua_getglobal(L, "setup");
        if (lua_pcall(L, 0, 0, 0))
        {
            std::cout << "failed to call setup function" << std::endl;
            return;
        }
    }
    shared_ptr<myData> data;
};

void main() 
{
    myLua lua1("Apple");
    myLua lua2("Orange");
}

bindings.h :

class Test
{
public:
    void callHello(lua_State *L) {

        //------------Added----------------
        myLua::myData *b = static_cast<myLua::myData *>(lua_touserdata(L, -1));
        cout << "RESULT2 : " << b->name << endl;
        //---------------------------------

        lua_getglobal(L, "hello");
        lua_pushstring(L, "ClassName");
        if (lua_pcall(L, 1, 0, 0))
        {
            std::cout << "failed to call hello function" << std::endl;
            return;
        }
    };
};

bindings.i : (Used to bind bindings.h using SWIG)

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

%include <stl.i>
%include <std_string.i>
%include <std_vector.i>
%include <std_map.i>
%include <typemaps.i>

%typemap(default) (lua_State *L) 
{
    $1 = L;
}
typedef std::string string;

%include "bindings.h"

Current result:

hello is called by : ClassName
hello is called by : ClassName

Result I want :

hello is called by : Apple
hello is called by : Orange

Maybe I can register the variable to lua_State* somehow?

I think it would be great if there's something like

lua_registerdata(L, &name);

And then later get it using something like

string name = lua_getregistereddata(L);

Result with the added code:

RESULT1 : Apple
RESULT2 : \360n\240\300`\255\276\255\336\336\300ݺ\220\300`DD\255\276\255\336\336\300ݺ\300\217\300`\340_\300`D\376
hello is called by : ClassName
RESULT1 : Orange
RESULT2 : \360n\300`\255\276\255\336\336\300ݺ\200\236\300`DD\255\276\255\336\336\300ݺ@\236\300``w\300`D\376
hello is called by : ClassName
Zack Lee
  • 2,784
  • 6
  • 35
  • 77
  • If you are creating a new state everytime, just push the `name` variable into the global table. `lua_pushstring(L, "orange"); lua_setglobal(L, "g_contextName")`. Then callHello would be `lua_getglobal(L, "hello"); lua_getglobal(L, "g_contextName"); if (lua_pcall(L, 1, 0, 0))` – James Poag Jul 06 '18 at 11:45
  • @JamesPoag Thanks, but what if the `name` variable is not string but other type such as struct pointer? – Zack Lee Jul 06 '18 at 11:57
  • You can use [light userdata](http://lua-users.org/wiki/LightUserData) to push pointers onto the lua stack. I.e. set "g_contextName" as pointer to `data` object. Normally, you would add a `__tostring` metamethod to the meta table that would reach in and push the name to the stack. However, [lightuserdata all share the same metatable](https://www.gamedev.net/forums/topic/590181-lua-multiple-light-userdata-with-metatables/) (which is set to nil by default). You should probably instead create a cfunction that converts the lightuserdata to a string(pops top of stack as data*, push data->name). – James Poag Jul 06 '18 at 12:22
  • In `main.cpp`, I tried adding `int a = 123; lua_pushlightuserdata(L, &a);` right before I run the Lua script with `luaL_loadstring` and in `bindings.h` I added `int *b = static_cast(lua_touserdata(L, 1)); cout << "RESULT : " << *b << endl;` inside `callHello()` function but it gave me `RESULT : 7270648` which is not the value I want. What am I doing wrong? – Zack Lee Jul 06 '18 at 12:26
  • 1
    what is the scope / lifetime of `a`? Does `a` got out of scope? Does `&a` still point to valid memory? `data` is better because it is allocated on the heap. Specifically, you can use `data.get()` to get the raw pointer. Just keep in mind that it is not reference counted. – James Poag Jul 06 '18 at 12:35
  • 1
    I.e. `lua_pushlightuserdata(&L, data.get())`, then check that the pointer value is the same as what you retrieve later with `lua_touserdata` – James Poag Jul 06 '18 at 12:37
  • In `main.cpp` I added `lua_pushlightuserdata(L, data.get()); myLua::myData *b = static_cast(lua_touserdata(L, 1)); cout << "RESULT1 : " << b->name << endl;` and in `binding.h` inside `callHello()` function, I added `myLua::myData *b = static_cast(lua_touserdata(L, -1)); cout << "RESULT2 : " << b->name << endl;` And I got the result : `RESULT1 : Apple RESULT2 : \360n@@\255\276\255\336\336\300ݺ\300<@` – Zack Lee Jul 06 '18 at 13:24
  • 1
    Can you update your examples? I'm having trouble imagining the Lua stack still has the lightuserdata on the top. This is why I suggested storing the lightuserdata in the global table (`setglobal`). – James Poag Jul 06 '18 at 13:42
  • @JamesPoag Thanks. I updated my code. Please check the parts where I marked **----Added----** in my code. Also the result. – Zack Lee Jul 06 '18 at 14:10
  • Ok, it looks like what I've expected: the Lua Stack no longer has the lightuserdata on top after the [luaL_loadString](https://www.lua.org/manual/5.3/manual.html#luaL_loadstring) call. It has the script chunk. In that documentation, there is a `[ -0, +1, -]` tag on that means loadstring pops `-0` values from the stack and pushes `+1` (and throws no errors). This is why I suggest setting your lightuserdata in as a global. – James Poag Jul 06 '18 at 15:05
  • The Lua documentation has a cool little [dump stack](https://www.lua.org/pil/24.2.3.html) function that's useful for examining the stack types to make sure what you are expecting is what you are getting. – James Poag Jul 06 '18 at 15:16
  • Could you show me a example how to do it? I don't really understand what you mean. – Zack Lee Jul 06 '18 at 16:45

1 Answers1

2

Pass by value

I suggest that you pass the name as an argument to setup and callHello. That solves the problem with lifetime of objects.

N.B.: Calling a Lua function from C++ which then calls a C++ function from Lua seems very inefficient. Are you sure about your design? Do you really need this extra indirection through Lua?

bindings.h

#pragma once

#include <iostream>
#include <string>

class Test {
public:
    void callHello(std::string const &name, lua_State *L) {
        lua_getglobal(L, "hello");
        lua_pushstring(L, name.c_str());
        if (lua_pcall(L, 1, 0, 0) != 0) {
            std::cout << "failed to call hello function\n"
                      << lua_tostring(L, -1) << '\n';
            return;
        }
    }
};

test.cpp

#include <iostream>
#include <string>

#include <lua.hpp>

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

class myLua {
public:
    myLua(std::string const &name) {
        lua_State *L = luaL_newstate();
        luaL_openlibs(L);
        luaopen_my(L);

        const char *script = "function setup(name)\n"
                             "    local test = my.Test()\n"
                             "    test:callHello(name)\n"
                             "end\n"
                             "function hello(name)\n"
                             "    print('hello is called by : ' .. name)"
                             "end";

        if (luaL_dostring(L, script) != 0) {
            std::cout << "failed to run lua script\n"
                      << lua_tostring(L, -1) << '\n';
            lua_close(L);
            return;
        }

        lua_getglobal(L, "setup");
        lua_pushstring(L, name.c_str());
        if (lua_pcall(L, 1, 0, 0) != 0) {
            std::cout << "failed to call setup function\n"
                      << lua_tostring(L, -1) << '\n';
            lua_close(L);
            return;
        }

        lua_close(L);
    }
};

int main() {
    myLua lua1("Apple");
    myLua lua2("Orange");
}

Pass by lightuserdata

As you have requested, you can also push a pointer to the string as lightuserdata into the registry and fetch it in the callHello function. Using the registry is dangerous for various reason. Keys might collide and you have to absolutely sure that the key hasn't been used elsewhere. The pointers to the C++ data might go dangling and Lua does not and cannot know about that and will happily hand you an invalid pointer. Dereferencing leads to a hard-to-debug segmentation fault.

N.B.: I believe that this is bad design and should be avoided. Giving up memory safety for the convenience of not having to pass a parameter doesn't sound like a good trade-off.

bindings.h

#pragma once

#include <iostream>
#include <string>

class Test {
public:
    void callHello(lua_State *L) {
        // Fetch light userdata from the registry with key "name" and
        // pray that it is there
        lua_pushstring(L, "name");
        lua_gettable(L, LUA_REGISTRYINDEX);
        std::string name;
        if (lua_islightuserdata(L, -1) == 1) {
            name = *static_cast<std::string *>(lua_touserdata(L, -1));
            lua_pop(L, 1);
        } else {
            lua_pushstring(L, "userdata corrupted or absent");
            lua_error(L);
            return;
        }

        // Call hello function with fetched name
        lua_getglobal(L, "hello");
        lua_pushstring(L, name.c_str());
        if (lua_pcall(L, 1, 0, 0) != 0) {
            std::cout << "failed to call hello function\n"
                      << lua_tostring(L, -1) << '\n';
            return;
        }
    }
};

test.cpp

#include <iostream>
#include <string>

#include <lua.hpp>

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

class myLua {
public:
    myLua(std::string name) {
        lua_State *L = luaL_newstate();
        luaL_openlibs(L);
        luaopen_my(L);

        const char *script = "function setup()\n"
                             "    local test = my.Test()\n"
                             "    test:callHello()\n"
                             "end\n"
                             "function hello(name)\n"
                             "    print('hello is called by : ' .. name)"
                             "end";

        if (luaL_dostring(L, script) != 0) {
            std::cout << "failed to run lua script\n"
                      << lua_tostring(L, -1) << '\n';
            lua_close(L);
            return;
        }

        // Push light userdata into the registry with key "name"
        lua_pushstring(L, "name");
        lua_pushlightuserdata(L, static_cast<void *>(&name));
        lua_settable(L, LUA_REGISTRYINDEX);

        lua_getglobal(L, "setup");
        if (lua_pcall(L, 0, 0, 0) != 0) {
            std::cout << "failed to call setup function\n"
                      << lua_tostring(L, -1) << '\n';
            lua_close(L);
            return;
        }

        lua_close(L);
    }
};

int main() {
    myLua lua1("Apple");
    myLua lua2("Orange");
}

Common bits

The SWIG interface file doesn't need to be adapted and stays the same for either case.

my.i

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

%include <std_string.i>
%include <typemaps.i>

%typemap(default) (lua_State *L) 
{
    $1 = L;
}

%include "bindings.h"

Compile and run can be done for both cases with (for example)

$ swig -lua -c++ my.i
$ clang++ -Wall -Wextra -Wpedantic -I/usr/include/lua5.2/ my_wrap.cxx test.cpp -llua5.2
$ ./a.out 
hello is called by : Apple
hello is called by : Orange
Henri Menke
  • 10,705
  • 1
  • 24
  • 42
  • Thank you Sir, but I want to do this without passing `name` variable via Lua function. Is it not possible using the `lua_pushlightuserdata` and `lua_touserdata`? Do you know why my code doesn't work properly? – Zack Lee Jul 07 '18 at 05:01
  • 1
    @ZackLee You can do it without passing `name` via the Lua function but then you are creating an ownership problem where there shouldn't be one and where it can be easily avoided. Are you sure you want to do this? – Henri Menke Jul 07 '18 at 06:09
  • Yes, I'm sure I want to do this! – Zack Lee Jul 07 '18 at 06:23
  • I would appreciate if you could take a look at my other question too https://stackoverflow.com/questions/51220356/passing-table-with-fields-as-an-argument-to-lua-function-from-c – Zack Lee Jul 07 '18 at 06:26
  • 1
    @ZackLee See the second section of my updated answer. – Henri Menke Jul 07 '18 at 06:36
  • Wow. Your updated answer works like magic! Thank you so much Sir. I will give you a bounty tomorrow. – Zack Lee Jul 07 '18 at 07:03
  • `Keys might collide and you have to absolutely sure that the key hasn't been used elsewhere.` Could you please be more specific about this? Could you show me an example that generates the key collision? And what can I do to prevent this? You mean there should be one unique key per one `lua_State*`? – Zack Lee Jul 07 '18 at 08:06
  • 1
    @ZackLee Different Lua states have different registries, so you do not need to worry about crosstalk. But within the same Lua state you can accidentally overwrite existing field. You can check whether a field exists before writing but you cannot know whether the field has been overwritten when reading. If you load a third-party module it might insert own keys into the registry which collide with yours. Therefore it is best to also add a prefix when using strings as keys, e.g. `"ZackLee_name"` instead of `"name"` (the prefixes `"lua"` and `"lualib"` are reserved, as well as any numeric key). – Henri Menke Jul 07 '18 at 09:55
  • You guys are basically replicating the functionality of `lua_setglobal`. – James Poag Jul 07 '18 at 10:46
  • @HenriMenke Thank you for your explanation. – Zack Lee Jul 07 '18 at 10:54
  • @JamesPoag What do you suggest? I'd appreciate if you post any example so I can understand what you mean. – Zack Lee Jul 07 '18 at 10:55
  • 1
    @JamesPoag In my opinion, that is worse than using the registry, because then you can also collide with any regular Lua variable (and your “key” has to be a string). – Henri Menke Jul 07 '18 at 11:11
  • 1
    @HenriMenke Hey, I'm having a little trouble following. You suggest using a prefix when using a string key (e.g. `ZackLee_name`) to avoid collisions, and that numeric keys are reserved. But if you are going through the trouble of avoiding these collisions, that putting the data in the global table somehow has different restrictions? Wouldn't a string named variable in the global table also follow closely the first solution that you posted because you could use the global as a normal lua variable? – James Poag Jul 07 '18 at 20:08
  • 2
    @JamesPoag You are right, that when using a sufficiently obscure prefix, you will likely avoid collisions for global variables as well. However, you'd then have to communicate to the user of the Lua interface that touching variables with a certain prefix should be avoided. Also the user might just ignore that warning to see what happens and the program starts crashing. This will lead to a lot of bug reports. Better hide it from the Lua interface by using the registry. – Henri Menke Jul 07 '18 at 22:50
  • @HenriMenke Ok, I think I understand where you are coming from. Thanks! – James Poag Jul 08 '18 at 12:12