Search code examples
clualua-apilua-userdata

Lua userdata array access and methods


I am writing in C a userdata type for use in Lua. It has some array-type properties and various methods aswell. Right now if u is of this type, I use u:set(k,v) resp. u:get(k) to access data and e.g. u:sort() as method. For this I set __index to a table containing these methods. Now if I want to access the data using u[k] = v or u[k], I need to set __newindex and __index to set resp get. But then the other methods are no longer accessible...

What's the best way to deal with this in C? I am guessing I need to write a function in C to register as __index and somehow deal with it there. Maybe check if key belongs to a Lua table of methods and if so call it.

Any help/hints would be appreciated. I did not find examples like this, although it seems a very natural thing to do (to me.)

edit: Added my C version of the solution in Lua posted in the answer below. This is more or less a direct translation, so all credit goes to @gilles-gregoire .

The following C function is registered as __index metamethod.

static int permL_index(lua_State *L) {
  struct perm **pp = luaL_checkudata(L, 1, PERM_MT);
  int i;

  luaL_getmetatable(L, PERM_MT);
  lua_pushvalue(L, 2);
  lua_rawget(L, -2);

  if ( lua_isnil(L, -1) ) {
    /* found no method, so get value from userdata. */
    i = luaL_checkint(L, 2);
    luaL_argcheck(L, 1 <= i && i <= (*pp)->n, 2, "index out of range");

    lua_pushinteger(L, (*pp)->v[i-1]);
  };

  return 1;
};

This is the code that does that,

int luaopen_perm(lua_State *L) {

  luaL_newmetatable(L, PERM_MT);
  luaL_setfuncs(L, permL_methods, 0);
  luaL_setfuncs(L, permL_functions, 0);
  lua_pop(L, 1);

  luaL_newlib(L, permL_functions);

  return 1;
};

where permL_methods is

static const struct luaL_Reg permL_methods[] = {
  { "__index",      permL_index           },
  { "__eq",         permL_equal           },
  { "__tostring",   permL_tostring        },
  { "__gc",         permL_destroy         },
  [...]
  { NULL,           NULL                  }
};

and permL_functions is

static const struct luaL_Reg permL_functions[] = {
  { "inverse",      permL_new_inverse     },
  { "product",      permL_new_product     },
  { "composition",  permL_new_composition },
  [...]
  { NULL,           NULL                  }
};

Solution

  • This looks like a problem which can be solved with nested metatables. You need one metatable for the methods (like your sort() method), and a second one for index operations. That second metatable is actually the metatable of the methods metatable.

    Let me write this as lua code. You need 3 tables:

    -- the userdata object. I'm using a table here,
    -- but it will work the same with a C userdata
    u = {}
    
    -- the "methods" metatable:
    mt = {sort = function() print('sorting...') end}
    
    -- the "operators" metatable:
    op_mt = {__index = function() print('get') end}
    

    Now, the tricky part is here: lua will first lookup u when you will call a method. If it does not find it, it will lookup in the table pointed by the __index field of u's metatable... And Lua will repeat the process for that table!

    -- first level metatable
    mt.__index = mt
    setmetatable(u, mt)
    
    -- second level metatable
    setmetatable(mt, op_mt)
    

    You can now use your u like this:

    > u:sort()
    sorting...
    > = u[1]
    get
    nil
    

    EDIT: a better solution by using a function for the __index metamethod

    Using a function for the __index metamethod is probably the right way to this:

    u = {}
    mt = {sort = function() print('sorting...') end}
    setmetatable(u, mt)
    mt.__index = function(t, key)
        -- use rawget to avoid recursion
        local mt_val = rawget(mt, key)
        if mt_val ~=nil then
            return mt_val
        else
            print('this is a get on object', t)
        end
    end
    

    Usage:

    > print(u)
    table: 0x7fb1eb601c30
    > u:sort()
    sorting...
    > = u[1]
    this is a get on object    table: 0x7fb1eb601c30
    nil
    >