Can you modify a C struct from lua?

Viewed 276

I want to be able to have this Lua code:

function myfunc(s)
    print(s.value)
    s.value = 7
end

And it should work with this C(++) code:

struct MyStruct {
    float value;
};

void func() {
    MyStruct var;
    var.value = 5;

    lua_getglobal(L, "myfunc");
    // push `var` to lua, somehow

    lua_call(L, 1, 0); // prints "5"

    // now, var.value is 7
    assert(var.value == 7);
}

How would I push var onto the stack such that the lua code can modify its variables?

1 Answers

Standard Lua doesn't have a built-in way to expose the contents of structs through tables, so doing so is a bit of a manual process. Here's an example of a complete program that does what you want:

#include <assert.h>
#include <string.h>

#include <lua5.3/lua.h>
#include <lua5.3/lualib.h>
#include <lua5.3/lauxlib.h>

struct MyStruct {
    float value;
};

int MyStruct_index(lua_State *L) {
    struct MyStruct *t = (struct MyStruct *)luaL_checkudata(L, 1, "MyStruct");
    const char *k = luaL_checkstring(L, 2);
    if(!strcmp(k, "value")) {
        lua_pushnumber(L, t->value);
    } else {
        lua_pushnil(L);
    }
    return 1;
}

int MyStruct_newindex(lua_State *L) {
    struct MyStruct *t = (struct MyStruct *)luaL_checkudata(L, 1, "MyStruct");
    const char *k = luaL_checkstring(L, 2);
    if(!strcmp(k, "value")) {
        t->value = luaL_checknumber(L, 3);
        return 0;
    } else {
        return luaL_argerror(L, 2,
                             lua_pushfstring(L, "invalid option '%s'", k));
    }
}

struct MyStruct *MyStruct_new(lua_State *L) {
    struct MyStruct *var = (struct MyStruct *)lua_newuserdata(L, sizeof *var);
    luaL_setmetatable(L, "MyStruct");
    return var;
}

void func(lua_State *L) {
    // Since this is allocated inside of Lua, it will be garbage collected,
    // so we don't need to worry about freeing it
    struct MyStruct *var = MyStruct_new(L);
    var->value = 5;

    lua_getglobal(L, "myfunc");
    lua_pushvalue(L, -2); // push `var` to lua, somehow

    lua_call(L, 1, 0); // prints "5"

    // now, var->value is 7
    assert(var->value == 7);
}

int main(void) {
    lua_State *L = luaL_newstate();
    luaL_openlibs(L);

    // One-time setup that needs to happen before you first call MyStruct_new
    luaL_newmetatable(L, "MyStruct");
    lua_pushcfunction(L, MyStruct_index);
    lua_setfield(L, -2, "__index");
    lua_pushcfunction(L, MyStruct_newindex);
    lua_setfield(L, -2, "__newindex");
    lua_pop(L, 1);

    luaL_dostring(L, "function myfunc(s) print(s.value) s.value = 7 end");
    func(L);

    lua_close(L);
    return 0;
}

To support more fields, you'd have to add them to MyStruct_index and MyStruct_newindex. If you want pairs to work on it, then you'd need to add a __pairs metamethod too. In general, though, exposing getters and setters is usually preferred over an approach like this one.

If you want to allocate your struct MyStruct somewhere other than inside of Lua, then you could add an extra layer of indirection everywhere it's used, like this:

#include <assert.h>
#include <string.h>

#include <lua5.3/lua.h>
#include <lua5.3/lualib.h>
#include <lua5.3/lauxlib.h>

struct MyStruct {
    float value;
};

int MyStruct_index(lua_State *L) {
    struct MyStruct **t = (struct MyStruct **)luaL_checkudata(L, 1, "MyStruct");
    const char *k = luaL_checkstring(L, 2);
    if(!strcmp(k, "value")) {
        lua_pushnumber(L, (*t)->value);
    } else {
        lua_pushnil(L);
    }
    return 1;
}

int MyStruct_newindex(lua_State *L) {
    struct MyStruct **t = (struct MyStruct **)luaL_checkudata(L, 1, "MyStruct");
    const char *k = luaL_checkstring(L, 2);
    if(!strcmp(k, "value")) {
        (*t)->value = luaL_checknumber(L, 3);
        return 0;
    } else {
        return luaL_argerror(L, 2,
                             lua_pushfstring(L, "invalid option '%s'", k));
    }
}

void MyStruct_push(lua_State *L, struct MyStruct *var) {
    struct MyStruct **t = (struct MyStruct **)lua_newuserdata(L, sizeof *t);
    *t = var;
    luaL_setmetatable(L, "MyStruct");
}

void func(lua_State *L) {
    struct MyStruct var;
    var.value = 5;

    lua_getglobal(L, "myfunc");
    MyStruct_push(L, &var); // push `var` to lua, somehow

    lua_call(L, 1, 0); // prints "5"

    // now, var.value is 7
    assert(var.value == 7);

    // WARNING! `var` is about to go out of scope! If Lua uses the userdata
    // that references it beyond this point, it's Undefined Behavior! Make
    // sure that it didn't keep a copy of it!
}

int main(void) {
    lua_State *L = luaL_newstate();
    luaL_openlibs(L);

    // One-time setup that needs to happen before you first call MyStruct_new
    luaL_newmetatable(L, "MyStruct");
    lua_pushcfunction(L, MyStruct_index);
    lua_setfield(L, -2, "__index");
    lua_pushcfunction(L, MyStruct_newindex);
    lua_setfield(L, -2, "__newindex");
    lua_pop(L, 1);

    luaL_dostring(L, "function myfunc(s) print(s.value) s.value = 7 end");
    func(L);

    lua_close(L);
    return 0;
}

But this is dangerous! If the Lua userdata outlives the object that it points to, Undefined Behavior will result!

Related