2
votes

I'm trying to write Lua bindings so that one can call arbitrary functions on a userdata. An MCV example I've been working on is below.

In summary: we have the C function newarray pushed to a table in the Lua globals so that one can create a new array object. Suppose that the array is a database record. I have two kinds of operation that I want to perform on it after generating it with newarray (for this bad example): accessing an element, and destroying the object.

Since I don't know how many elements there will be (in a real world example), I decide to make __index a function and use an if-statement to determine if the function was "destroy" or anything else (i.e. "give me this element"). If it was "destroy", delete the object; otherwise, return the requested element.

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

#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>

#define TEST_METATABLE "_test_mt"

typedef struct
{
    int* array;
} array_t;

int newArray(lua_State* L)
{
    assert(lua_gettop(L) == 0);
    array_t* array = lua_newuserdata(L, sizeof(array_t));
    array->array = malloc(sizeof(int) * 10);

    for (int i = 0; i < 10; i++)
        array->array[i] = i;

    /* Set metatable */

    lua_getfield(L, LUA_REGISTRYINDEX, TEST_METATABLE);
    lua_setmetatable(L, -2);

    return 1;
}

int indexFunc(lua_State* L)
{
    int argc = lua_gettop(L);
    array_t* array = luaL_checkudata(L, 1, TEST_METATABLE);
    const char* key = luaL_checkstring(L, 2);
    int ret = 0;

    if (!strcmp(key, "destroy"))
    {
        if (argc != 2)
        {
            lua_settop(L, 0);
            luaL_error(L, "Invalid arguments");
        }

        if (array->array)
        {
            free(array->array);
            array->array = NULL;
        }

        printf("Finished destroy\n");

        lua_settop(L, 0);
    }
    else
    {
        if (argc != 2)
        {
            lua_settop(L, 0);
            luaL_error(L, "Invalid arguments");
        }

        if (lua_tointeger(L, 2))
        {
            lua_pushinteger(L, array->array[lua_tointeger(L, 2)]);
        }
        else
        {
            lua_settop(L, 0);
            luaL_error(L, "Bad index supplied");
        }

        lua_remove(L, 2);
        lua_remove(L, 1);
        ret = 1;
    }

    return ret;
}

int luaopen_TestArray(lua_State* L)
{
    /* Set up metatable */

    lua_newtable(L);

    lua_pushliteral(L, "__index");
    lua_pushcfunction(L, indexFunc);
    lua_settable(L, -3);

    lua_setfield(L, LUA_REGISTRYINDEX, TEST_METATABLE);

    /* Set up 'static' stuff */

    lua_newtable(L);

    lua_pushliteral(L, "newarray");
    lua_pushcfunction(L, newArray);
    lua_settable(L, -3);

    lua_setglobal(L, "TestArray");

    return 0;
}

I compiled with:

gcc -std=c99 -Wall -fPIC -shared -o TestArray.so test.c -llua

The Lua test program is as follows:

require("TestArray")

a = TestArray.newarray()

print(a[5])

a:destroy()

The output:

$ lua test.lua
5
Finished destroy
lua: test.lua:7: attempt to call method 'destroy' (a nil value)
stack traceback:
        test.lua:7: in main chunk
        [C]: ?
$

So Lua does what it's supposed to by retrieving the 6th element's value (in terms of C) and printing it (as it surely does through indexFunc). Then it proceeds to execute the destroy-specific code in indexFunc, then tries to look for a function called destroy, and I have no idea why. It found the __index metamethod, so I don't understand why it looked elsewhere afterwards. Why does it do this, and what am I doing wrong?

Lua version: 5.1.4.

1
__index metamethod should only retrieve the value for the key "destroy", that is, indexFunc must only return a value (function "destroy") without executing this function. Destructor should be implemented as separate function int destroyFunc(lua_State* L). Fast solution: just replace a:destroy() with local _ = a.destroy :-)Egor Skriptunoff
@EgorSkriptunoff Ohh, I see, so it's because of the parentheses after destroy which basically mean "invoke the result of retrieving 'destroy' as a function"? If you'd like to submit it as an answer I will accept, because AFAIK you don't get any well-deserved rep from a comment. :)Doddy

1 Answers

2
votes

__index is expected to return a value. Yours doesn't.

Specifically, when you write this:

a:destroy()

That is equivalent to:

getmetatable(a).__index(a, "destroy")(a)

i.e. call the __index metamethod, then call whatever it returns passing it a as the argument.

But if we look at your __index implementation, it doesn't respect that contract:

int indexFunc(lua_State* L)
{
  int argc = lua_gettop(L);
  array_t* array = luaL_checkudata(L, 1, TEST_METATABLE);
  const char* key = luaL_checkstring(L, 2);
  int ret = 0;

  if (!strcmp(key, "destroy"))
  {
    /* ... delete the array ... */
    lua_settop(L, 0);
  }
  else
  {
    /* ... push the value ... */
  }

  return ret; /* since key == "destroy", ret == 0 here */
}

If the key is "destroy", it doesn't return a function; instead it destroys the array immediately and returns nothing, which is equivalent in this case to returning nil. Then the lua code tries to call the returned nil and explodes.

Instead, you need to create a separate function that does the destroy, e.g.

int destroyFunc(lua_State * L) {
  array_t array = luaL_checkudata(L, 1, TEST_METATABLE);
  free(array->array);
  array->array = NULL;
  return 0;
}

And then have your __index return that function rather than calling it:

lua_pushcfunction(L, destroyFunc);
return 1;

At which point the Lua code will be able to call that function.