此第一版是為 Lua 5.0 編寫的。儘管在很大程度上仍然與後續版本相關,但仍有一些差異。
第四版針對 Lua 5.3,可在 Amazon 和其他書店購買。
購買本書,您也可以協助 支援 Lua 專案


28.2 – 元表

我們目前的實作有一個重大的安全性漏洞。假設使用者寫了類似 array.set(io.stdin, 1, 0) 的內容。io.stdin 中的值是一個使用者資料,指向一個串流 (FILE*)。由於它是一個使用者資料,array.set 會很樂意接受它作為一個有效的引數;可能的結果將是記憶體毀損 (運氣好的話,您可能會收到一個索引超出範圍的錯誤)。這種行為對於任何 Lua 函式庫來說都是不可接受的。無論您如何使用 C 函式庫,它都不應毀損 C 資料或從 Lua 產生核心傾印。

為了區分陣列和其他的使用者資料,我們為它建立一個唯一的元表。(請記住,使用者資料也可以有元表。) 然後,每次我們建立一個陣列時,我們都會用這個元表標記它;每次我們取得一個陣列時,我們都會檢查它是否具有正確的元表。由於 Lua 程式碼無法變更使用者資料的元表,因此它無法偽造我們的程式碼。

我們還需要一個地方來儲存這個新的元表,以便我們可以存取它來建立新的陣列,並檢查給定的使用者資料是否為陣列。正如我們先前所見,有兩個常見的選項可以儲存元表:在註冊表中,或作為函式庫中函式的非局部變數。在 Lua 中,通常會使用註冊表中的 類型名稱 作為索引,並將元表作為值,來註冊任何新的 C 類型。與任何其他註冊表索引一樣,我們必須小心選擇類型名稱,以避免衝突。我們將稱呼這個新的類型為 "LuaBook.array"

與往常一樣,輔助函式庫提供了一些函式來協助我們。我們將使用的新的輔助函式為

    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);
luaL_newmetatable 函數會建立一個新表格(用作元表格),將新表格留在堆疊頂端,並將表格與註冊表中的指定名稱關聯起來。它會進行雙重關聯:它使用名稱作為表格的鍵,以及使用表格作為名稱的鍵。(此雙重關聯讓其他兩個函數可以有更快的實作。)luaL_getmetatable 函數會從註冊表中擷取與 tname 關聯的元表格。最後,luaL_checkudata 會檢查給定堆疊位置的物件是否為具有與給定名稱相符元表格的使用者資料。如果物件沒有正確的元表格(或它不是使用者資料),它會傳回 NULL;否則,它會傳回使用者資料位址。

現在我們可以開始實作。第一步是變更開啟函式庫的函數。新版本必須建立一個表格,用作陣列的元表格

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

下一步是變更 newarray,以便它在它建立的所有陣列中設定此元表格

    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 */
    }
lua_setmetatable 函數會從堆疊中彈出一個表格,並將它設定為給定索引處物件的元表格。在我們的案例中,此物件是新的使用者資料。

最後,setarraygetarraygetsize 必須檢查它們是否取得一個有效的陣列作為它們的第一個引數。因為我們想要在引數錯誤的情況下引發錯誤,我們定義下列輔助函數

    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;
    }
使用 checkarraygetsize 的新定義很簡單
    static int getsize (lua_State *L) {
      NumArray *a = checkarray(L);
      lua_pushnumber(L, a->size);
      return 1;
    }

因為 setarraygetarray 也共用檢查索引作為它們的第二個引數的程式碼,我們在下列函數中將它們的共用部分抽離出來

    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];
    }
在定義 getelem 之後,setarraygetarray 很簡單
    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;
    }
現在,如果你嘗試類似 array.get(io.stdin, 10) 的東西,你會取得一個適當的錯誤訊息
    error: bad argument #1 to `getarray' (`array' expected)