User Data Environment Examples

lua-users home
wiki

Here's some sample code which allows the use of environment tables as "on-demand" storage of unknown keys, something I've gotten used to doing because it allows Lua programs to add attributes to C objects implemented as full userdata. The code is extracted from a quick&dirty vector library which can be downloaded from [1] and [2], with sample output at [3]. I (RiciLake) just threw it together last night, so no guarantees or anything.

Here's the basic userdata structure:

// How do you get the wiki to syntax colour C?

typedef struct lvec_s {

#ifdef VERSION51

  int has_env;

#else

  int len;

#endif

  lua_Number d[1];

} lvec_t;

Note the use of the has_env member. Aside from padding the userdata header to a doubleword boundary on x86 architecture, which prevents the doubles from crossing cache boundaries, this is used as a flag to indicate whether or not the object has a useful environment table. (In version 5.0, the len member would throw alignment off. It would be better to use union {int len; lua_Number dummy} u; )

The Lua API does not allow the creation of a userdata without an environment table and all the other solutions I found to check for its existence were slower.

Here's the getter method, which expects to find the object's method table at upvalue index 2 (I use upvalue 1 for the metatable in all methods):


/* getter and setter, allowing overriding in 5.1. 5.0 doesn't have environment tables */

static int lvec_meta_index (lua_State *L) {

  int len = 0;

  lvec_t *u = getveclen(L, 1, &len); 

First, we check to see if the key is a reasonable numeric index, and returns nil if it isn't. (My view is that getters should never throw an error, they should simply return nil. This is consistent with usual table semantics, and also makes it easier to write code which doesn't generate random errors.)

  if (lua_type(L, 2) == LUA_TNUMBER) {

    int idx = checkkey(L, len);

    if (idx) {

      lua_pushnumber(L, u->d[idx-1]);

      return 1;

    }

    else

      return 0;

  }

Normally, I would check for getter methods at this point. I'll find a good example of such code later, but in this particular problem domain, the check is somewhat idiosyncratic:

  if (lua_type(L, 2) == LUA_TSTRING) {

    size_t slen;

    const char *key = lua_tolstring(L, 2, &slen);

    if (slen == 1) {

      int idx = key[0] - 'x';

      if (len <= 3 && idx >= 0 && idx < len) {

        lua_pushnumber(L, u->d[idx]);

        return 1;

      }

    }

  }

Now, if it's not a built-in attribute, we check to see if there is an environment table created, and if so look the key up in it. We haven't yet checked for instance methods, which allows instance methods to be overridden for particular objects. In fact, had I not been aiming at 5.0 semi-compatibility in this particular code, I would have used a different solution where the method table is made the __index metamethod of the environment table, once the environment table is created, which means that the final lookup can be done only once, and also allows the possibility to create subclasses of the object in Lua.

#ifdef VERSION51

  if (u->has_env) {

    lua_getfenv(L, 1);

    lua_pushvalue(L, 2);

    lua_gettable(L, -2);

    if (!lua_isnil(L, -1))

      return 1;

    lua_pop(L, 2);

  }

#endif

Finally, the key is looked up in the instance method table, which is attached as upvalue 2 to the __index function (for speed of lookup). If it's not found in this table, we just return the nil

  lua_gettable(L, lua_upvalueindex(2));

  return 1;

}



The __newindex method has slightly different logic. Again, the first check is for numeric and short string keys, but in this case, errors are thrown both for bad numeric keys and for bad values. (The call to luaL_checknumber is not correct, I was being lazy: it will get fixed at some point. It produces hard-to-interpret error messages.) If that fails, then an environment table is created if necessary, and the key/value pair inserted in the newly-created table. (As mentioned above, I would normally attach the method table as an __index meta when creating the new environment table.)


static int lvec_meta_newindex (lua_State *L) {

  int len = 0;

  lvec_t *u = getveclen(L, 1, &len);

  if (lua_type(L, 2) == LUA_TNUMBER) {

    int idx = checkkey(L, len);

    if (idx)

      u->d[idx-1] = luaL_checknumber(L, 3);

    else

      luaL_error(L, "Vector index not integer or out of range");

    return 0;

  }

  if (lua_type(L, 2) == LUA_TSTRING) {

    size_t slen;

    const char *key = lua_tolstring(L, 2, &slen);

    if (slen == 1) {

      int idx = key[0] - 'x';

      if (len <= 3 && idx >= 0 && idx < len) {

        u->d[idx] = luaL_checknumber(L, 3);

        return 0;

      }

    }

  }

#ifdef VERSION51

  if (!u->has_env) {

    lua_newtable(L); 

    lua_pushvalue(L, -1);

    lua_setfenv(L, 1);

    u->has_env = 1;

  }

  else

    lua_getfenv(L, 1);

  lua_replace(L, 1);

  lua_settable(L, 1);

#else

  else

    luaL_error(L, "Vector index not integer or out of range");

#endif

  return 0;

}



Discussion

Rici, this is a good/practical example. BTW the sample output link is broken. It took me some time to figure out the main purpose of the page though. Even though it's stated at top 'environment tables as "on-demand" storage of unknown keys', I was a bit clueless as to in what context this was and how 5.0 v.s. 5.1 issues were a factor. But, I see now (though I haven't fully processed it) that this is an approach to implementing the __index and __newindex metamethods for userdata that handles metatables nicely with apparently a lazy/on-demand construction. It's affected by how 5.1 allows individual environment tables in C functions. The stated aim of 5.0/5.1 compatibility might be provided earlier. Heavy use of parenthesis () tends to decreases readability. This might be placed in Lua Fu in the LuaDirectory. I think there is a place there for what might be called design patterns. --DavidManura

I fixed the link. You're right -- it definitely needs editting. I always parenthesize too much on first drafts, probably the influence of LISP on my youth :) -- RiciLake

RecentChanges · preferences
edit · history
Last edited September 26, 2006 8:50 am GMT (diff)