Brian McCallister wrote:
> I am leaning towards changing all the userdata types (request_rec,
> server_rec, conn_rec, apr_table_t) in mod_wombat to have anonymous
> metatables rather than the current setup where they share a metatable.


+1

> The drawback is that we lose the ~"type safety" of being able to use
> luaL_checkuserdata(...) to  pop userdata with a specific metatable
> out of the stack.

There are other ways of doing this. One good way is to keep
the metatable as the environment of the instance methods
(and, if necessary, of the metamethods). Then the check
is really simple (and quite a lot faster than luaL_checkuserdata).

Code (from memory, I don't have my computer with me):

static Foo* check_self (lua_State *L) {
  if (!lua_getmetatable(L, 1) || !lua_rawequal(L, 1, LUA_ENVIRONINDEX)) {
    luaL_typerror(L, 1, "Foo");
  }
  lua_pop(L, 1);
  return lua_touserdata(L, 1);
}


The self-test is the most common, but it is often also
desirable to check individual parameters for specific
types, either prescriptively or polymorphically. This
can be done safely by keeping the type's name as
a field in the metatable, say __typename, and also
using the luaL_newmetatable mechanism in parallel.
luaL_newmetable just stores the metatable in the
registry with the type name as a key, so we can
do the same thing also putting the type name as
a key in the metatable:

int my_newmetatable (lua_State *L, const char* typename) {
  lua_pushstring(L, typename);
  lua_pushvalue(L, -1);  /* dup the name */
  lua_gettable(L, LUA_REGISTRYINDEX);
    /* ... name meta-or-nil */
  switch (lua_type(L, -1)) {
    default: {
      return luaL_error(L, "Invalid registration of %s: not a table",
typename);
    }
    case LUA_TTABLE: {
      /* Make sure it satisifies the constraint */
      lua_getfield(L, -1, "__typename");
      if (!lua_rawequal(L, -1, -3))
        luaL_error(L, "Invalid registration of %s: typename not set",
typename);
      /* Everything's ok, fix the stack up */
      lua_pop(L, 1);
      lua_replace(L, -2);
      return 0;
    }
    case LUA_TNIL: {
      /* Need a new table */
      lua_pop(L, 1):  /* S: ... name */
      lua_createtable(L, 0, 4);  /* S: ... name meta */
      lua_insert(L, -2);  /* S: ... meta name */
      lua_pushvalue(L, -1): /* S: ... meta name name */
      lua_setfield(L, -3, "__typename");  /* S: ... meta name */
      lua_pushvalue(L, -2);  /* S: ... meta name meta */
      lua_settable(L, LUA_REGISTRYINDEX);  /* S: ... meta */
      return 1;
    }
  }
}


Now, we can verify that a metatable is correct by getting the
__typename field out of it, and making sure that the
registered typename is that metatable:

/* Only use this with arguments (positive index); see
 * lauxlib for how to normalize the index if necessary
 *
 * Returns true if the indicated argument is some
 * valid type; in that case, leaves the typename on
 * top of the stack. Otherwise, returns 0 and does
 * not change the stack
 */
int *my_checksometype (lua_State *L, int argindex) {
  if (lua_getmetatable(L, argindex)) {
    lua_getfield(L, -1, "__typename");  /* ... meta name */
    lua_pushvalue(L, -1);  /* ... meta name name */
    lua_rawget(L, LUA_REGISTRYINDEX);  /* ... meta name registered-table */
    if (lua_rawequal(L, -1, -3)) {
      /* OK, fix the stack */
      lua_pop(L, 1);
      lua_replace(L, -2);
      return 1;
    }
    lua_pop(L, 3);  /* restore the stack */
  }
  return 0;  /* Not ours */
}

This leaves the typename on top of the stack for possible
polymorphism. If we're just interested in a single type,
we can just check with strcmp:

static void* my_checktype (lua_State *L, int argindex, const char
*typename) {
  if (my_checksometype(L, argindex)
      && 0 == strcmp(lua_tostring(L, -1), typename)) {
    lua_pop(L, 1);  /* Ditch the typename */
    return lua_touserdata(L, argindex);
  }
  luaL_typerror(L, argindex, typename);
  return NULL;  /* Never reached */
}

A simple way to do polymorphism would be to just
run through the possibilities, but if there are a lot of
them, it's also possible to use a dispatch table (i.e.
a Lua table) kept as an upvalue, with the typename as a key.

/* See example below.
int my_dispatch (lua_State *L, int argindex, int vindex, const char *msg) {
  const luaL_Reg *entry;
  if (!my_checksometype(L, argindex)) {
    /* In case we want to dispatch on lua types */
    if (lua_isnoneornil(L, argindex))
      lua_pushliteral(L, "nil");
    else
      lua_pushstring(L, lua_typename(L, lua_type(L, argindex)));
  }
  /* Look it up in the dispatch table */
  lua_rawget(L, vindex);
  entry = lua_touserdata(L, -1);
  if (entry) {
    /* Got it, just call it */
    lua_pop(L, 1);
    return entry->func(L);
  }
  return luaL_typerror(L, argindex, msg);
}

To use this, we need to set up a dispatch table, and
attach it to a method. A helper function comes in
handy. This function expects the stack to end with
the method table followed by 'nups' upvalues, and
puts the dispatch table as upvalue 'nups+1'; it's just
an example.

void my_registermethodwithvtable(
  lua_State *L,
  const char *name,
  lua_CFunction method,
  const luaL_Reg *vtable,
  int nups)
{
  /* Make the vtable */
  int i;
  for (i=0; vtable[i].name, ++i) {}  /* count */
  lua_createtable(L, 0, i);
  for (i=0; vtable[i].name, ++i) {
    lua_pushlightuserdata(L, &vtable[i]);
    lua_setfield(L, -2, vtable[i].name);
  }
  /* Push the closure */
  lua_pushcclosure(L, method, nups+1);
  /* And insert it */
  lua_rawset(L, -2, name);
}

/* If I typed all that correctly, it might be used like this: */

/* Sample vtable: */
static const luaL_Reg test_addstuff_vtable[] = {
  {"string", test_do_string},
  {"apr_table", test_do_apr_table},
  {"nil", test_do_default},
  {NULL, NULL}
};
/* ... in the luaopen_* function; the method table
 * should be on top of the stack (as it will be after
 * luaL_register, for example):
 */
  my_registermethodwithvtable(L, "addstuff", test_addstuff,
test_addstuff_vtable, 0);

/* And the definition of the method itself: */

static int test_addstuff (lua_State *L) {
  my_checkself(L);
  return my_dispatch(L, 2, lua_upvalueindex(1), "optional string or
apr_table");
}

I hope that all makes sense.

>
> This would do arbitrary lookups on userdata as though it were a lua
> table:
>
> r.headers_in, r.status, etc.

This can easily be accomplished by creating a getter/setter dispatch
table, rather like the dispatch table created above. The __index
and __newindex metamethods then need to look the key up both
in the getter/setter vtable, and then in the method table.

Note that the userdata itself is argument 1 to __{,new}index, and it's
unforgeable provided that the metatable is protected by adding
a __metatable key. The following assumes that the method table
is upvalue 1 to both __index and __newindex, and that the relevant
vtable is upvalue 2. Note that both getter and setter tables need
to be filled in for all property keys, even readonly ones; the setter
should either throw an error or silently fail in that case.

int my_standardgetter (lua_State *L) {
  const luaL_Reg *entry;
  /* No need to check self */
  lua_pushvalue(L, 2);  /* dup the key */
  lua_rawget(L, lua_upvalueindex(2));
  entry = lua_touserdata(L, -1);
  lua_pop(L, 1);
  if (entry != NULL)
    return entry->func(L);
  /* Not a property, try method */
  lua_gettable(L, lua_upvalueindex(1));
  /* If that's nil, we can just return it, too */
  return 1;
}

int my_standardsetter (lua_State *L) {
  const luaL_Reg *entry;
  lua_pushvalue(L, 2);
  lua_rawget(L, lua_upvalueindex(1));
  entry = lua_touserdata(L, -1);
  lua_pop(L, 1);
  if (entry != NULL)
    return entry->func(L);
  /* This error message could be improved :) */
  return luaL_error(L, "Attempt to set invalid key");
}

Now to set all this stuff up. We need a method table, a
metatable, and some kind of constructor function. The
metatable is going to be the environment for everything,
and the method table will be upvalue 1 for the getter
and setter methods.

void my_maketype (
  lua_State *L,
  const char  *typename,
  const luaL_reg *methods,
  const luaL_reg *getters,
  const luaL_reg *setters)
{
  /*
    * Possibly should do nothing instead of whining, but
    * whining seems safer
    */
  if (my_newmetatable(L, typename))
    luaL_error(L, "Duplicate registration of %s", typename);
  /* Set the environment */
  lua_pushvalue(L, -1);
  lua_replace(L, LUA_ENVIRONINDEX);
  /* Create the method table */
  luaL_register(L, NULL, methods);
  lua_insert(L, -2);  /* ... methods meta */
  lua_pushvalue(L, -2);
  my_registermethodwithvtable(L, "__index", my_standardgetter, getters, 1);
  lua_pushvalue(L, -2);
  my_registermethodwithvtable(L, "__newindex", my_standardsetter, setters,
1);
  /* Ditch the method table, so the metatable is on top of the stack.
    * The caller can then add additional metamethods, like __gc
    */
  lua_replace(L, -2):
}

I think that covers everything, except that sometimes it's nice to
be able to add arbitrary keys to a userdata; this lets the Lua
script not be limited to the built-in keys. This can be accomplished
by using the environment table of the userdata to store the keys.
Initially, the environment table is set to the metatable (this will
happen automatically with the above setup, because the metatable
is the environment table of all methods, provided that the
constructor is created after a call to my_maketype). We then need
to modify my_standardgetter and my_standardsetter to use the
environment table if it exists (or create it if it doesn't exist).

int my_envgetter (lua_State *L) {
  const luaL_Reg *entry;
  /* No need to check self */
  lua_pushvalue(L, 2);  /* dup the key */
  lua_rawget(L, lua_upvalueindex(2));
  entry = lua_touserdata(L, -1);
  lua_pop(L, 1);
  if (entry != NULL)
    return entry->func(L);
  /* Not a property, try the env if it exists */
  lua_getfenv(L, 1);
  if (!lua_rawequal(L, -1, LUA_ENVIRONINDEX)) {
    lua_pushvalue(L, 2);
    lua_gettable(L, -2);
    if (!lua_isnil(L, -1))
      return 1;
  }
  /* Finally, try the method table */
  lua_pushvalue(L, 2);
  lua_gettable(L, lua_upvalueindex(1));
  /* If this is nil, just return it */
  return 1;
}

int my_envsetter (lua_State *L) {
  const luaL_Reg *entry;
  lua_pushvalue(L, 2);
  lua_rawget(L, lua_upvalueindex(1));
  entry = lua_touserdata(L, -1);
  lua_pop(L, 1);
  if (entry != NULL)
    return entry->func(L);
  /* If there's no env yet, create one */
  lua_getfenv(L, 1);
  if (lua_rawequal(L, -1, LUA_ENVIRONINDEX))
    lua_newtable(L);
  /* Insert the key into the env table */
  lua_pushvalue(L, 2);
  lua_pushvalue(L, 3);
  lua_settable(L, -3);
  return 0;
}


> It may be possible, depending on the exact semantics, to use __index
> and __newindex overloading with the OO syntax:
>
> r:headers_in
> r:status

That won't work because : is only valid in call syntax. Either you use
explicit getter/setter methods, or the above implementation. I prefer
the above implementation for its generality, but it would mean
changing a lot of existing mod_wombat scripts.

Hope all that helps. I'll be away for four weeks on vacation, so I tried
to be as explicit as I could...

Rici

Reply via email to