This first edition was written for Lua 5.0. While still largely relevant for later versions, there are some differences.
The fourth edition targets Lua 5.3 and is available at Amazon and other bookstores.
By buying the book, you also help to support the Lua project.


28.2 – Metatables

Our current implementation has a major security hole. Suppose the user writes something like array.set(io.stdin, 1, 0). The value in io.stdin is a userdatum with a pointer to a stream (FILE*). Because it is a userdatum, array.set will gladly accept it as a valid argument; the probable result will be a memory corruption (with luck you can get an index-out-of-range error instead). Such behavior is unacceptable for any Lua library. No matter how you use a C library, it should not corrupt C data or produce a core dump from Lua.

To distinguish arrays from other userdata, we create a unique metatable for it. (Remember that userdata can also have metatables.) Then, every time we create an array, we mark it with this metatable; and every time we get an array, we check whether it has the right metatable. Because Lua code cannot change the metatable of a userdatum, it cannot fake our code.

We also need a place to store this new metatable, so that we can access it to create new arrays and to check whether a given userdatum is an array. As we saw earlier, there are two common options for storing the metatable: in the registry, or as an upvalue for the functions in the library. It is customary, in Lua, to register any new C type into the registry, using a type name as the index and the metatable as the value. As with any other registry index, we must choose a type name with care, to avoid clashes. We will call this new type "LuaBook.array".

As usual, the auxiliary library offers some functions to help us here. The new auxiliary functions we will use are

    int   luaL_newmetatable (lua_State *L, const char *tname);
    void  luaL_getmetatable (lua_State *L, const char *tname);
    void *luaL_checkudata (lua_State *L, int index,
                                         const char *tname);
The luaL_newmetatable function creates a new table (to be used as a metatable), leaves the new table in the top of the stack, and associates the table and the given name in the registry. It does a dual association: It uses the name as a key to the table and the table as a key to the name. (This dual association allows faster implementations for the other two functions.) The luaL_getmetatable function retrieves the metatable associated with tname from the registry. Finally, luaL_checkudata checks whether the object at the given stack position is a userdatum with a metatable that matches the given name. It returns NULL if the object does not have the correct metatable (or if it is not a userdata); otherwise, it returns the userdata address.

Now we can start our implementation. The first step it to change the function that opens the library. The new version must create a table to be used as the metatable for arrays:

    int luaopen_array (lua_State *L) {
      luaL_newmetatable(L, "LuaBook.array");
      luaL_openlib(L, "array", arraylib, 0);
      return 1;
    }

The next step is to change newarray so that it sets this metatable in all arrays that it creates:

    static int newarray (lua_State *L) {
      int n = luaL_checkint(L, 1);
      size_t nbytes = sizeof(NumArray) + (n - 1)*sizeof(double);
      NumArray *a = (NumArray *)lua_newuserdata(L, nbytes);
    
      luaL_getmetatable(L, "LuaBook.array");
      lua_setmetatable(L, -2);
    
      a->size = n;
      return 1;  /* new userdatum is already on the stack */
    }
The lua_setmetatable function pops a table from the stack and sets it as the metatable of the object at the given index. In our case, this object is the new userdatum.

Finally, setarray, getarray, and getsize have to check whether they got a valid array as their first argument. Because we want to raise an error in case of wrong arguments, we define the following auxiliary function:

    static NumArray *checkarray (lua_State *L) {
      void *ud = luaL_checkudata(L, 1, "LuaBook.array");
      luaL_argcheck(L, ud != NULL, 1, "`array' expected");
      return (NumArray *)ud;
    }
Using checkarray, the new definition for getsize is straightforward:
    static int getsize (lua_State *L) {
      NumArray *a = checkarray(L);
      lua_pushnumber(L, a->size);
      return 1;
    }

Because setarray and getarray also share code to check the index as their second argument, we factor out their common parts in the following function:

    static double *getelem (lua_State *L) {
      NumArray *a = checkarray(L);
      int index = luaL_checkint(L, 2);
    
      luaL_argcheck(L, 1 <= index && index <= a->size, 2,
                       "index out of range");
    
      /* return element address */
      return &a->values[index - 1];
    }
After the definition of getelem, setarray and getarray are straightforward:
    static int setarray (lua_State *L) {
      double newvalue = luaL_checknumber(L, 3);
      *getelem(L) = newvalue;
      return 0;
    }
    
    static int getarray (lua_State *L) {
      lua_pushnumber(L, *getelem(L));
      return 1;
    }
Now, if you try something like array.get(io.stdin, 10), you will get a proper error message:
    error: bad argument #1 to `getarray' (`array' expected)