Lua_第27章 User-Defined Types in C

Lua_第27章  User-Defined Types in C

       在上一章,我們討論了如何使用 C 函數擴展 Lua 的功能現在我們討論如何使用 C 中新創建的類型來擴展 Lua。我們從一個小例子開始,本章後續部分將以這個小例子 爲基礎逐步加入 metamethods 等其他內容來介紹如何使用 C 中新類型擴展 Lua。
       我們的例子涉及的類型非常簡單,數字數組。這個例子的目的在於將目光集中到 API 問題上,所以不涉及複雜的算法。儘管例子中的類型很簡單,但很多應用中都會用到這 種類型。一般情況下,Lua 中並不需要外部的數組,因爲哈希表很好的實現了數組。但 是對於非常大的數組而言,哈希表可能導致內存不足,因爲對於每一個元素必須保存一 個範性的(generic)值,一個鏈接地址,加上一些以備將來增長的額外空間。在 C 中的直接存儲數字值不需要額外的空間,將比哈希表的實現方式節省 50%的內存空間。
我們使用下面的結構表示我們的數組:

typedef struct NumArray {
int size;
double values[1]; /* variable part */
} NumArray;
       我們使用大小 1 聲明數組的 values,由於 C 語言不允許大小爲 0 的數組,這個 1 只 是一個佔位符;我們在後面定義數組分配空間的實際大小。對於一個有 n 個元素的數組 來說,我們需要
sizeof(NumArray) + (n-1)*sizeof(double) bytes
(由於原始的結構中己經包含了一個元素的空間,所以我們從 n  中減去 1)
28.1 Userdata
       我們首先關心的是如何在 Lua 中表示數組的值。Lua 爲這種情況提供專門提供一個基本的類型:userdata。一個 userdatum 提供了一個在 Lua 中沒有預定義操作的 raw 內存區域
        Lua API 提供了下面的函數用來創建一個 userdatum: 
void *lua_newuserdata (lua_State *L, size_t size);
       lua_newuserdata 函數按照指定的大小分配一塊內存,將對應的 userdatum 放到棧內, 並返回內存塊的地址。如果出於某些原因你需要通過其他的方法分配內存的話,很容易 創建一個指針大小的 userdatum,然後將指向實際內存塊的指針保存到 userdatum 裏。我們將在下一章看到這種技術的例子。使用 lua_newuserdata 函數,創建新數組的函數實現如下:
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);
     a->size = n;
     return 1; /* new userdatum is already on the stack */
}<span style="font-size:14px; font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);"> </span>

        (函數 luaL_checkint 是用來檢查整數的 luaL_checknumber 的變體)一旦 newarray 在Lua中被註冊之後,你就可以使用類似 a = array.new(1000)的語句創建一個新的數組 了。
        爲了存儲元素,我們使用類似 array.set(array, index, value)調用,後面我們將看到如 何使用 metatables 來支持常規的寫法 array[index] = value。對於這兩種寫法,下面的函數是一樣的,數組下標從1開始:
 

static int setarray (lua_State *L) {
   NumArray *a = (NumArray *)lua_touserdata(L, 1);
   int index = luaL_checkint(L, 2);
   double value = luaL_checknumber(L, 3);
   luaL_argcheck(L, a != NULL, 1, "`array' expected");
   luaL_argcheck(L, 1 <= index && index <= a->size, 2,"index out of range");
   a->values[index-1] = value;
   return 0;
} 
       luaL_argcheck 函數檢查給定的條件,如果有必要的話拋出錯誤。因此,如果我們使 用錯誤的參數調用 setarray,我們將得到一個錯誤信息: 
array.set(a, 11, 0)
--> stdin:1: bad argument #1 to 'set' ('array' expected)
下面的函數獲取一個數組元素:
static int getarray (lua_State *L) {
  NumArray *a = (NumArray *)lua_touserdata(L, 1);
  int index = luaL_checkint(L, 2); 
  luaL_argcheck(L, a != NULL, 1, "'array' expected"); 
  luaL_argcheck(L, 1 <= index && index <= a->size, 2,"index out of range"); 
  lua_pushnumber(L, a->values[index-1]);
  return 1;
}

我們定義另一個函數來獲取數組的大小:

static int getsize (lua_State *L) {
  NumArray *a = (NumArray *)lua_touserdata(L, 1);
   luaL_argcheck(L, a != NULL, 1, "`array' expected"); 
   lua_pushnumber(L, a->size);
   return 1;
}

最後,我們需要一些額外的代碼來初始化我們的庫: 

static const struct luaL_reg arraylib [] = {
  {"new", newarray},
  {"set", setarray},
  {"get", getarray},
  {"size", getsize},
  {NULL, NULL}
};

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

}

       這兒我們再次使用了輔助庫的 luaL_openlib 函數,他根據給定的名字創建一個表, 並使用 arraylib 數組中的 name-function 對填充這個表。
       打開上面定義的庫之後,我們就可以在 Lua 中使用我們新定義的類型了:

a = array.new(1000)
print(a)	--> userdata: 0x8064d48 print(array.size(a))	--> 1000
   for i=1,1000 do
   array.set(a, i, 1/i)
end
print(array.get(a, 10)) --> 0.1
       在一個 Pentium/Linux  環境中運行這個程序,一個有 100K  元素的數組大概佔用800KB 的內存,同樣的條件由 Lua 表實現的數組需要 1.5MB 的內存。

28.2 Metatables

       我們上面的實現有一個很大的安全漏洞。假如使用者寫了如下類似的代碼: array.set(io.stdin, 1, 0)。io.stdin  中的值是一個帶有指向流(FILE*)的指針的 userdatum。因爲它是一個 userdatum,所以 array.set 很樂意接受它作爲參數,程序運行的結果可能導致內存 core dump(如果你夠幸運的話,你可能得到一個訪問越界(index-out-of-range)錯 誤)。這樣的錯誤對於任何一個 Lua 庫來說都是不能忍受的。不論你如何使用一個 C 庫, 都不應該破壞 C 數據或者從 Lua 產生 core dump。
       爲了區分數組和其他的 userdata,我們單獨爲數組創建了一個 metatable(記住 userdata也可以擁有 metatables)。下面,我們每次創建一個新的數組的時候,我們將這個單獨的 metatable 標記爲數組的 metatable。每次我們訪問數組的時候,我們都要檢查他是否有一 個正確的 metatable。因爲 Lua 代碼不能改變 userdatum 的 metatable,所以他不會僞造我 們的代碼。
       我們還需要一個地方來保存這個新的 metatable,這樣我們才能夠當創建新數組和檢 查一個給定的 userdatum 是否是一個數組的時候,可以訪問這個 metatable。正如我們前 面介紹過的,有兩種方法可以保存 metatable:在 registry 中,或者在庫中作爲函數的 upvalue。在 Lua 中一般習慣於在 registry 中註冊新的 C 類型,使用類型名作爲索引, metatable 作爲值。和其他的 registry 中的索引一樣,我們必須選擇一個唯一的類型名, 避免衝突。我們將這個新的類型稱爲 "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 函數創建一個新表(將用作 metatable),將新表放到棧頂並建立表和 registry 中類型名的聯繫。這個關聯是q雙向的:使用類型名作爲表的 key;同時使用 表作爲類型名的 key(這種雙向的關聯,使得其他的兩個函數的實現效率更高)。 luaL_getmetatable 函數獲取 registry 中的 tname 對應的 metatable。最後,luaL_checkudata 檢查在棧中指定位置的對象是否爲帶有給定名字的 metatable 的 usertatum。如果對象不 存在正確的 metatable,返回 NULL(或者它不是一個 userdata);否則,返回 userdata 的 地址。

下面來看具體的實現。第一步修改打開庫的函數,新版本必須創建一個用作數組metatable 的表: 

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

}

第二步,修改 newarray,使得在創建數組的時候設置數組的 metatable:

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 函數將表出棧,並將其設置爲給定位置的對象的 metatable。在我們 的例子中,這個對象就是新的userdatum。
       最後一步,setarray、getarray 和 getsize 檢查他們的第一個參數是否是一個有效的數 組。因爲我們打算在參數錯誤的情況下拋出一個錯誤信息,我們定義了下面的輔助函數:
 
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;
}
使用 checkarray,新定義的 getsize 是更直觀、更清楚: 
static int getsize (lua_State *L) { 
  NumArray   *a = checkarray(L);
  lua_pushnumber(L, a->size); 
  return 1;
}

由於 setarray 和 getarray 檢查第二個參數 index 的代碼相同,我們抽象出他們的共同 部分,在一個單獨的函數中完成:
 

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,函數 setarray 和 getarray 更加直觀易懂:
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)

28.3 訪問面向對象的數據
    下面我們來看看如何定義類型爲對象的 userdata,以致我們就可以使用面向對象的 語法來操作對象的實例,比如:

a = array.new(1000) 
print(a:size())	--> 1000
a:set(10, 3.4)
print(a:get(10))	--> 3.4
     記住 a:size()等價於 a.size(a)。所以,我們必須使得表達式 a.size 調用我們的 getsize 函數。這兒的關鍵在於__index 元方法Cmetamethod)的使用。對於表來說,不管什麼 時候只要找不到給定的 key,這個元方法就會被調用。對於 userdata 來講,每次被訪問 的時候元方法都會被調用,因爲 userdata 根本就沒有任何 key。
     假如我們運行下面的代碼:
   local metaarray = getmetatable(array.new(1)) 
   metaarray. index = metaarray
   metaarray.set = array.set
   metaarray.get = array.get metaarray.size = array.size

     第一行,我們僅僅創建一個數組並獲取他的 metatable,metatable 被賦值給 metaarray (我們不能從 Lua 中設置 userdata 的 metatable,但是我們在 Lua  中無限制的訪問 metatable)。接下來,我們設置 metaarray.    index 爲 metaarray。當我們計算 a.size 的時候, Lua 在對象 a 中找不到 size 這個鍵值,因爲對象是一個 userdatum。所以,Lua 試着從對 象 a  的 metatable 的__index 域獲取這個值,正好 index 就是 metaarray。但是 metaarray.size 就是 array.size,因此 a.size(a)如我們預期的返回 array.size(a)。
    當然,我們可以在 C 中完成同樣的事情,甚至可以做得更好:現在數組是對象,他 有自己的操作,我們在表數組中不需要這些操作。我們實現的庫唯一需要對外提供的函 數就是 new,用來創建一個新的數組。所有其他的操作作爲方法實現。C 代碼可以直接 註冊他們。
     getsize、getarray 和 setarray 與我們前面的實現一樣,不需要改變。我們需要改變的 只是如何註冊他們。也就是說,我們必須改變打開庫的函數。首先,我們需要分離函數 列表,一個作爲普通函數,一個作爲方法:

static const struct luaL_reg arraylib_f [] = {
{"new", newarray},
{NULL, NULL}
};
static const struct luaL_reg arraylib_m [] = {
{"set", setarray},
{"get", getarray},
{"size", getsize},
{NULL, NULL}
};
新版本打開庫的函數 luaopen_array,必須創建一個 metatable,並將其賦值給自己的_index  域,在那兒註冊所有的方法,創建並填充數組表:
 
int luaopen_array (lua_State *L) { luaL_newmetatable(L, "LuaBook.array");
   lua_pushstring(L, " index");
   lua_pushvalue(L, -2);	/* pushes the metatable */
   lua_settable(L, -3); /* metatable. index = metatable */
   luaL_openlib(L, NULL, arraylib_m, 0); 
   luaL_openlib(L, "array", arraylib_f, 0);
   return 1;
}
       這裏我們使用了 luaL_openlib 的另一個特徵,第一次調用,當我們傳遞一個 NULL 作爲庫名時,luaL_openlib    並沒有創建任何包含函數的表;相反,他認爲封裝函數的表 在棧內,位於臨時的 upvalues 的下面。在這個例子中,封裝函數的表是 metatable 本身, 也就是 luaL_openlib 放置方法的地方。第二次調用 luaL_openlib 正常工作:根據給定的 數組名創建一個新表,並在表中註冊指定的函數(例子中只有一個函數 new)。
       下面的代碼,我們爲我們的新類型添加一個  tostring 方法,這樣一來 print(a)將打 印數組加上數組的大小,大小兩邊帶有圓括號(比如,array(1000)):
int array2string (lua_State *L) { 
  NumArray *a = checkarray(L);
  lua_pushfstring(L, "array(%d)", a->size);
  return 1;
}
      函數 lua_pushfstring 格式化字符串,並將其放到棧頂。爲了在數組對象的 metatable中包含 array2string,我們還必須在 arraylib_m 列表中添加 array2string:
static const struct luaL_reg arraylib_m [] = {
   {" tostring", array2string},
   {"set", setarray},
   ...
};
28.4 訪問數組
       除了上面介紹的使用面向對象的寫法來訪問數組以外,還可以使用傳統的寫法來訪 問數組元素,不是 a:get(i),而是 a[i]。對於我們上面的例子,很容易實現這個,因爲我 們的 setarray 和 getarray 函數己經依次接受了與他們的元方法對應的參數。一個快速的解 決方法是在我們的 Lua 代碼中正確的定義這些元方法:
   local metaarray = getmetatable(newarray(1))
   metaarray. index = array.get
   metaarray. newindex = array.set

(這段代碼必須運行在前面的最初的數組實現基礎上,不能使用爲了面向對象訪問的修改的那段代碼)
        我們要做的只是使用傳統的語法:
 

a = array.new(1000)
a[10] = 3.4
-- setarray
 
print(a[10]) 
-- getarray
--> 3.4
如果我們喜歡的話,我們可以在我們的 C 代碼中註冊這些元方法。我們只需要修改 我們的初始化函數:
<pre name="code" class="csharp">int luaopen_array (lua_State *L) {
  luaL_newmetatable(L, "LuaBook.array"); 
  luaL_openlib(L, "array", arraylib, 0);
/* now the stack has the metatable at index 1 and 'array' at index 2 */
 
  lua_pushstring(L, " index");
  lua_pushstring(L, "get"); 
  lua_gettable(L, 2); /* get array.get */
  lua_settable(L, 1); /* metatable. index = array.get */
 
  lua_pushstring(L, " newindex"); 
  lua_pushstring(L, "set");
  lua_gettable(L, 2); /* get array.set */
  lua_settable(L, 1); /* metatable. newindex = array.set */
   
   return 0;
}

28.5 Light Userdata 
       到目前爲止我們使用的 userdata 稱爲 full userdata。Lua 還提供了另一種 userdata: light userdata。一個 light userdatum 是一個表示 C 指針的值(也就是一個 void *類型的值)。由於它 是一個值,我們不能創建他們(同樣的,我們也不能創建一個數字)。可以使用函數 lua_pushlightuserdata 將一個 light userdatum 入棧:

void lua_pushlightuserdata (lua_State *L, void *p);
      儘管都是 userdata,light userdata 和 full userdata 有很大不同。Light userdata 不是一 個緩衝區,僅僅是一個指針,沒有 metatables。像數字一樣,light userdata 不需要垃圾收集器來管理她。
      有些人把 light userdata 作爲一個低代價的替代實現,來代替 full userdata,但是這不 是 light userdata 的典型應用。首先,使用 light userdata 你必須自己管理內存,因爲他們 和垃圾收集器無關。第二,儘管從名字上看有輕重之分,但 full userdata 實現的代價也並 不大,比較而言,他只是在分配給定大小的內存時候,有一點點額外的代價。
Light userdata 真正的用處在於可以表示不同類型的對象。當 full userdata 是一個對象 的時候,它等於對象自身;另一方面,light userdata 表示的是一個指向對象的指針,同 樣的,它等於指針指向的任何類型的 userdata。所以,我們在 Lua 中使用 light userdata 表示 C 對象。
      看一個典型的例子,假定我們要實現:Lua 和窗口系統的綁定。這種情況下,我們 使用 full userdata 表示窗口(每一個 userdatum 可以包含整個窗口結構或者一個有系統創 建的指向單個窗口的指針)。當在窗口有一個事件發生(比如按下鼠標),系統會根據窗 口的地址調用專門的回調函數。爲了將這個回調函數傳遞給 Lua,我們必須找到表示指 定窗口的 userdata。爲了找到這個 userdata,我們可以使用一個表:索引爲表示窗口地址的 light userdata,值爲在 Lua 中表示窗口的 full userdata。一旦我們有了窗口的地址,我們 將窗口地址作爲 light userdata 放到棧內,並且將 userdata 作爲表的索引存到表內。(注意 這個表應該有一個 weak 值,否則,這些 full userdata 永遠不會被回收掉。)
發佈了456 篇原創文章 · 獲贊 223 · 訪問量 112萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章