Lua和C語言的交互

本文轉自http://www.grati.org/?p=662

Lua生來就是爲了和C交互的,因此使用C擴展Lua或者將Lua嵌入到C當中都是非常流行的做法。要想理解C和Lua的交互方式,首先要回顧一下C語言是如何處理函數參數的。

C函數和參數
大家知道C語言是用匯編實現的,在彙編語言中可沒有函數的概念,與函數對應的是叫做子過程的東西,子過程就是一段指令,一個子過程與它調用的子過程之間通過棧來進行參數的傳遞交互。在一個子過程在調用別的子過程之前,會按照約定的格式將要調用的子過程需要的參數入棧,在被調用的子過程中,可以按照約定的規則將參數從棧中取出。同理,對於返回值的傳遞也同樣是通過堆棧進行的。C語言約定的參數放入棧中的格式,就是“調用慣例”。C語言的函數原型則決定了壓入棧中的參數的數量和類型。

Lua的虛擬堆棧
Lua和C之間的交互巧妙的模擬了C語言的堆棧,Lua和C語言之間的相互調用和訪問都通過堆棧來進行,巧妙的解決了不同類型之間變量相互訪問的問題。具體的,我們想象如下一個圖

  +-------+                      +-------+
  |       |                      |       |
  |       |      +-------+       |       |
  |   C   | <==> |       | <==>  |  Lua  |
  | Space |      |Virtual|       | Space |
  |       |      | Stack |       |       |
  |       |      |       |       |       |
  +-------+      +-------+       +-------+


由於C和Lua是不同層次的語言,因此C語言的變量和Lua中的變量以及函數不能直接的交互,我們假定C語言和Lua都有自己的“空間(C Space和Lua Space)”。而這兩個空間之間的交互就通過上圖中的這個虛擬堆棧來解決。爲何採用虛擬堆棧的方式來進行交互呢?其目的是在提供強大的靈活性的同時避免交互時兩種語言變量類型的組合爆炸。

C語言讀寫Lua全局變量(基本類型)
C語言讀取Lua的全局變量是一種最簡單的操作。通過上圖我們可以猜測到,如果通過C語言讀取Lua中的全局變量需要兩步:1、將全局變量從Lua Space壓入虛擬堆棧;2、從堆棧將全局變量讀取到C語言Space中。在Lua和C的交互中,Lua無法看到和操作虛擬堆棧,僅在C語言中有操作堆棧的權利,因此前面說到的兩步全都是在C語言中完成的。我們看一個簡單的例子


Lua代碼:
global_var1 = 5;
print("Print global varb from lua", global_var1);


C代碼:
......
void get_global(lua_State *L)
{
int global_var1;
lua_getglobal(L, "global_var1"); /* 從lua的變量空間中將全局變量global_var1讀取出來放入虛擬堆棧中 */
global_var1 = lua_tonumber(L, -1); /* 從虛擬堆棧中讀取剛纔壓入堆棧的變量,-1表示讀取堆棧最頂端的元素 */

printf("Read global var from C: %d\n", global_var1);
}
......

執行結果:

pi@raspberrypi ~/Programming/article_lua $ ./a.out global_var.lua
Print global var from lua  5
Read global_var: 5

Lua中對堆棧的操作都是通過索引來進行的,索引爲1表示從棧底數第一個元素,索引爲2表示從棧底數第二個元素;同樣也可以使用負數從棧頂開始計算,-1表示從棧頂數第一個元素,-2表示從棧頂數第二個元素。更多堆棧的操作函數請參考lua的官方手冊http://www.lua.org/manual/5.2/manual.html。 同樣從堆棧中獲取元素,除了我們使用的lua_tonumber之外,還有lua_tolstring,lua_toboolean等。
通常情況下在讀取變量之前還需要對堆棧中元素的實際類型做出檢查:

C代碼:
......
void get_global(lua_State *L)
{
int global_var1;
lua_getglobal(L, "global_var1"); /* 從lua的變量空間中將全局變量global_var1讀取出來放入虛擬堆棧中 */
if (!lua_isnumber(L, -1))        /* 檢查堆棧中棧頂第一個元素是否是數字 */
error(L, "Is not number.");
global_var1 = lua_tonumber(L, -1); /* 從虛擬堆棧中讀取剛纔壓入堆棧的變量,-1表示讀取堆棧最頂端的元素 */
}
......

寫入全局變量也一樣簡單:
首先將數據壓入堆棧,然後再將堆棧中的數據存入全局變量。

C代碼:
void set_global(lua_State *L)
{
lua_pushinteger(L, 9);
lua_setglobal(L, "global_var1");
printf("set global var from C:9\n");
}

執行結果:

pi@raspberrypi ~/Programming/article_lua $ ./a.out global_var.lua
set global var from C:9
Print global var from lua       9

C調用Lua函數 不要懷疑,對Lua函數的調用也是通過棧來進行的。請看如下代碼:

Lua代碼: function lua_func (x, y) print("Parameters are: ", x, y) return (x^2 * math.sin(y))/(1-x) end

C代碼: double c_func(lua_State *L, double x, double y){ double z;

lua_getglobal(L, "lua_func");    /* 首先將lua函數從Lua Space放入虛擬堆棧中 */ lua_pushnumber(L, x);            /* 然後再把所需的參數入棧 */ lua_pushnumber(L, y);

if (lua_pcall(L, 2, 1, 0) != 0){ /* 使用pcall調用剛纔入棧的函數,pcall的參數的含義爲:pcall(Lua_state, 參數個數, 返回值個數, 錯誤處理函數所在的索引),最後一個參數暫時先忽略 */ error(L, "error running lua function: $s", lua_tostring(L, -1)); }

z = lua_tonumber(L, -1);         /* 將函數的返回值讀取出來 */ lua_pop(L, 1);                   /* 將返回值彈出堆棧,將堆棧恢復到調用前的樣子 */

printf("Return from lua:%f\n", z);

 

return z; }

執行結果: pi@raspberrypi ~/Programming/article_lua $ ./a.out lua_func.lua Parameters are: 9 2 Return from lua:-9.206636

Lua調用C函數 Lua調用C函數其實就是用C編寫Lua的擴展,使用C爲Lua編寫擴展也非常簡單。所有C擴展的函數都有一個固定的函數原型,如下所示: C代碼: static int l_sin (lua_State *L) { double d = lua_tonumber(L, 1);      /* 不出意外,Lua中的參數也是通過虛擬堆棧傳遞的。因此C函數必須自己從堆棧中讀取參數。注意在Lua中調用函數時是不會做原型檢查的,Lua代碼調用C函數時傳遞幾個參數,虛擬堆棧中就會有幾個參數,因此C代碼在從堆棧中讀取參數的時候最好自己檢查一下堆棧的大小和參數類型是否符合預期。這裏爲了簡化起見我們就不做類型檢查了 */ d = sin(d); /* 這裏是C函數實現自己功能的代碼 */ lua_pushnumber(L, d);              /* 在完成計算後,只需將結果重新寫入虛擬堆棧即可(寫入的這個值就是函數的返回值) */ return 1; /* 函數的返回值是函數返回參數的個數。沒錯,Lua函數可以有多個返回值。 */ }

static void regist_func(lua_State *l) /* 這個函數將C函數寫入Lua的命名空間中。 */ { lua_pushcfunction(l, l_sin); lua_setglobal(l, "mysin"); }

 

將函數寫入Lua全局命令空間的代碼很簡單,和寫入全局變量的代碼一樣,都是先將C函數壓入堆棧,然後再將虛擬堆棧中的函數指針寫入Lua全局命名空間並將其命名爲”mysin”。之後在Lua中就可以使用”ret = mysin(30)”這樣的形式調用我們的C函數了。

C語言讀取Lua中的表 C語言讀取Lua table會稍微複雜一點,不過Lua的table是一種重要的數據結構,因此對table的讀寫也是很重要的內容。讀取Table基本需要如下幾步: 1、使用lua_getglobal將表從Lua命名空間讀取到虛擬堆棧中; 2、使用lua_pushstring將要讀取的字段的名稱壓入堆棧; 3、使用函數lua_gettable,這個函數會將table和key出棧,然後把對應字段的值入棧; 4、最後使用lua_toXXXX從堆棧中讀取值並使用lua_pop將數值出棧將堆棧恢復到調用前的樣子; 不要糾結,我也覺得複雜,而且十分懷疑性能問題,但是Lua作者說Lua是一門快速的語言。好吧,暫且聽他的等,回來理解深入之後再讀下代碼一探究竟。

Lua代碼中定義如下的table: BLUE = {r=0, g=0, b=1} background = BLUE

C語言中使用如下方法讀取table: ... static void read_table(lua_State *L) { double resault;

lua_getglobal(L, "background");     /* 將表從lua空間複製到虛擬堆棧(應該是僅拷貝索引,否則速度無法保證) */ lua_pushstring(L, "b");             /* 將要讀取的鍵壓入虛擬堆棧 */ lua_gettable(L, -2);                /* 使用lua_gettable讀取table,其第二個參數爲table在虛擬堆棧中的索引(-1爲key,所以-2爲table) */ resault = lua_tonumber(L, -1);      /* 將讀取出的結果複製到C空間 */ lua_pop(L, 1);                      /* 將結果出棧,將堆棧恢復成調用前的樣子 */

 

printf("Read from lua table: %f\n", resault); } ... 運行結果: pi@raspberrypi ~/Programming/article_lua $ ./a.out table.lua Read from lua table: 1.000000

C語言寫入Lua中的表: 1、將要寫入的table放入堆棧,可以新建也可以寫入現有table; 2、將要寫入的鍵壓入堆棧; 3、將要寫入的值壓入堆棧; 4、調用lua_settable執行table的寫入 5、如果是新建table的話,最後需要使用lua_setglobal,將修改後的table寫會lua全局變量。 Lua代碼: print ("Read talbe.r", background.r) print ("Read talbe.g", background.g) print ("Read talbe.b", background.b) C代碼: static void write_table(lua_State *L) { lua_newtable(L);              /* 新建table並放入堆棧。對於lua空間中沒有table的情況可以使用lua_newtable新建一個table;如果是寫入已有table,則應該使用lua_getglobal將數據從lua空間讀入虛擬堆棧 */

lua_pushstring(L, "r");       /* 將要寫入的鍵壓入堆棧 */ lua_pushnumber(L, (double)0); /* 將要寫入的值壓入堆棧 */ lua_settable(L, -3);          /* 執行table的寫入,函數的第二個參數是table在虛擬堆棧中的位置 */

lua_pushstring(L, "b");       /* 重複三次,一共寫入了"r", "g", "b" 三個成員 */ lua_pushnumber(L, (double)1); lua_settable(L, -3);

lua_pushstring(L, "g"); lua_pushnumber(L, (double)0); lua_settable(L, -3);

 

lua_setglobal(L, "background"); /* 最後將新table寫入lua全局命名空間 */ } 運行結果: pi@raspberrypi ~/Programming/article_lua $ ./a.out print_table.lua Read talbe.r 0 Read talbe.g 0 Read talbe.b 1

自定義數據類型:

我們通過使用C語言實現一個Lua數組來演示Lua實現自定義用戶數據。數組的結構如下所示: typedef struct NumArray{ int size; //表示數組的大小 double values[]; //此處的values僅代表一個double*類型的指針,values指向NumArray結構後部緊跟的數據的地址 } NumArray; 我們實現四個函數new,get,set和size,分別用來完成數組的新建,讀取,寫入和獲取大小。在Lua中用來實現自定義數據結構的類型叫做userdata。Lua提供瞭如下接口用來創建userdata: void *lua_newuserdata(lua_State *L, size_t size),該函數分配size大小的內存作爲userdata並將其壓入棧,函數的返回值爲新建立的userdata,可以自由轉換爲所需的數據結構。 實現自定義數據結構的C代碼如下: /* 新建array */ static int newarray (lua_State *L){ int n = luaL_checkint(L, -1); //檢查參數,數組的個數必須是整數 size_t nbytes = sizeof(NumArray) + n*sizeof(double); //計算C語言結構所需的內存空間,nbytes的長度包括數組結構頭和其後部的數據 NumArray *a = (NumArray *)lua_newuserdata(L, nbytes); //新建一個大小爲nbytes的userdata並壓入堆棧。 a->size = n; //初始化NumArray的大小 return 1; }

/* 設置array中的數值 */ static int setarray(lua_State *L) { NumArray *a = (NumArray *)lua_touserdata(L, -3); //將堆棧中的userdata讀取出來 int index = luaL_checkint(L, -2); //讀取索引 double value = luaL_checknumber(L, -1); //讀取數值

luaL_argcheck(L, NULL != a, 1, "'array' expected"); //檢查參數的返回,如果第二個表達式爲假,則拋出最後一個參數指定的錯誤信息 luaL_argcheck(L, index >= 0 && index <= a->size, 1, "index out of range");

a->values[index - 1] = value; //將lua中寫入的數值設置到C數組中

return 0; }

/* 讀取array中的數值 */ static int getarray(lua_State *L) { NumArray *a = (NumArray *)lua_touserdata(L, -2); //前面的步驟和setarray中的相同 int index = luaL_checkint(L, -1);

luaL_argcheck(L, NULL != a, 1, "'array' expected"); luaL_argcheck(L, index >= 1 && index <= a->size, 1, "index out of range");

lua_pushnumber(L, a->values[index - 1]); //將C數組中的數值壓入堆棧

return 1; }

/* 獲取array的大小 */ static int getsize(lua_State *L) { NumArray *a = (NumArray *)lua_touserdata(L, -1); luaL_argcheck(L, NULL != a, 1, "'array' expected");

lua_pushnumber(L, a->size);

return 1; }

/* 將我們定義的四個函數寫成數組的形式,在主函數中可以使用luaL_openlib將四個函數一口氣註冊到lua空間 */ static const struct luaL_Reg arraylib[] = { {"new", newarray}, {"set", setarray}, {"get", getarray}, {"size", getsize}, {NULL, NULL} };

int main(int argc, char* argv[]) { ...... luaL_openlib(L, "array", arraylib, 0); //註冊剛纔實現的四個函數到全局變量array下,其名稱分別爲new,set,get和size。 ...... return 0; }

對應的Lua程序爲: a = array.new(1000) print(a) print(array.size(a)) for i=1,999 do array.set(a, i, 1/i) end

print ("Print first 10 elements") for i=1,10 do print(array.get(a, i) ) end

運行結果是: pi@raspberrypi ~/Programming/article_lua $ ./a.out userdata3.lua userdata: 0x20237b0 1000 Print first 10 elements 1 0.5 0.33333333333333 0.25 0.2 0.16666666666667 0.14285714285714 0.125 0.11111111111111 0.1 但是,上述程序有兩個問題:1、參數中僅檢查了用戶的輸入參數是否是userdata,並沒有區分實際的類型,如果用戶傳遞array.get(io.stdin, 200),就會造成Sagement fault,這樣的行爲是不可接受的。2、用戶僅能使用array.size(a),和array.get(a,40)的形式訪問內容,能否使用類似於面向對象特性的方式如a:size()和a:get(40)的形式呢? 答案是肯定的,因此必須引入metatable的機制。 我們假定讀者已經對lua語言本身和在Lua中使用metatable有一定的瞭解,這裏僅介紹在C語言中如何爲userdata添加metatable。在對於userdata的metatable有如下幾點需要注意: 1、metatable在lua中僅是一個普通的table,但是在lua提供給C語言的接口中,metabtable需要使用專門的接口來創建和讀取。(原因暫時不詳) 2、lua的metatable和javascript的prototype不同。如果訪問對象缺失的方法,javascript會直接從prototype指向的對象中查找缺失的方法,但lua不同,他不會直接從metatable中查找,而是會從metatable的__index域所指向的對象中查找。如果我們設置object.metatable.__index = object.metatable,這樣就和javascript類似了。對於對象缺失的方法,會直接從metatable中查找。 3、對於userdata而言其什麼方法都沒有,因此對他的成員的訪問都是通過訪問metatable.__index來實現的(如果設置metatable.__index = metatable,那麼訪問userdata的成員就相當於訪問userdata的metatable)。

使用metatable修改後的程序如下所示: /* 將要註冊的函數拆分爲兩部分 arraylib_f註冊給全局變量array arraylib_m註冊給metatable,作爲methord */ 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} };

/* C函數的安裝使用如下函數 整個函數中我們創建了一個匿名的metatable,後續註釋中的metatable一詞代指這個新建的metatable的實例 */ static void install_func(lua_State *L) { luaL_newmetatable(L, "LuaBook.array"); //新建一個metatable,一方面壓入堆棧,另一方面將metatable以"LuaBook.array"爲key放入register中(register類似於一個全局哈希表)

lua_pushstring(L, "__index"); lua_pushvalue(L, -2); lua_settable(L, -3); //這三步的作用是將新建的metatable的__index字段賦值爲他自己,相當於metatable.__index = metatable,盡是爲了方便,否則還需要另外建立一個table2,並設置metatable.__index = table2 luaL_openlib(L, NULL, arraylib_m, 0); //將set、get、size這三個函數註冊給metatable(給luaL_openlib函數傳遞NULL時,表示要操作的表已經放在堆棧中)

luaL_openlib(L, "array", arraylib_f, 0); //將new這個函數註冊給全局變量array }

static int newarray (lua_State *L){ int n = luaL_checkint(L, 1); size_t nbytes = sizeof(NumArray) + n*sizeof(double); NumArray *a = (NumArray *)lua_newuserdata(L, nbytes);

luaL_getmetatable(L, "LuaBook.array"); lua_setmetatable(L, -2); //新建userdata的時候將register中名爲LuaBook.array中metatable設置給新建的array對象,從此所有array共用一個metatable

a->size = n; return 1; }

static int setarray(lua_State *L) { NumArray *a = (NumArray *)luaL_checkudata(L, -3, "LuaBook.array"); //調用函數的時候,除了檢查參數是否爲userdata之外,還要判斷該userdata的metatable是否是"LuaBook.array",這樣在使用io.stdin調用我們的函數時就可以檢查出錯誤,不至造成Sagement fault int index = luaL_checkint(L, -2); double value = luaL_checknumber(L, -1);

luaL_argcheck(L, NULL != a, 1, "'array' expected"); luaL_argcheck(L, index >= 0 && index <= a->size, 1, "index out of range");

a->values[index - 1] = value;

return 0; }

static int getarray(lua_State *L) { NumArray *a = (NumArray *)luaL_checkudata(L, -2, "LuaBook.array"); //同樣增加參數類型和metatable的檢查 int index = luaL_checkint(L, -1);

luaL_argcheck(L, NULL != a, 1, "'array' expected"); luaL_argcheck(L, index >= 0 && index <= a->size, 1, "index out of range");

lua_pushnumber(L, a->values[index - 1]);

return 1; }

static int getsize(lua_State *L) { NumArray *a = (NumArray *)luaL_checkudata(L, -1, "LuaBook.array"); //同樣增加參數類型和metatable的檢查

luaL_argcheck(L, NULL != a, 1, "'array' expected"); lua_pushnumber(L, a->size);

return 1; }

int main(int argc, char* argv[]) { ...... install_func(L); //使用install_func將函數註冊到lua空間 ...... }

對應Lua的例子變爲: a = array.new(1000) print(a) print(a:size()) for i=1,999 do a:set(i, 1/i) end

print ("Print first 10 elements") for i=1,10 do print(a:get(i)) end

文章中用到的示例程序:

最後給出一個main函數的例子,在這個函數中我們可以添加前面說到的示例代碼從而組合出完整的示例程序。 int main(int argc, char* argv[]) { char* filename; double ret = 0;

//新建一個lua state lua_State *L = luaL_newstate();

if (argc >=2 ){ filename = argv[1]; } else { printf("usage: %s filename\n", argv[0]); return 1; }

//這個函數加載lua標準庫 luaL_openlibs(L);

/* 在這裏添加代碼註冊C語言實現的函數 */

/* 從指定的文件名加載lua代碼(實際上代碼被編譯成chunk壓入棧中) */ if (luaL_loadfile(L, filename)){ error(L, "cannot run file: %s", lua_tostring(L, -1)); }

/* 執行剛纔讀取的lua代碼 */ if (lua_pcall(L, 0, 0, 0)){ error(L, "cannot run file: %s", lua_tostring(L, -1)); }

/* 如果調用lua函數,要放在這裏 */

return 0; } 寫在最後: 本文假定讀者對Lua的基本語法已經有了一定的瞭解。由於Lua是原型繼承語言,和我們之前使用的基於類型的語言有些區別(倒是和Javascript類似,Javascript也是原型繼承語言)。因此在開始學習的過程一定要跳出類和對象的思維才能真正理解Lua。最後推薦兩個學習Lua的優秀材料: 官方手冊:http://www.lua.org/manual/查起來很方便,如果用它來學習會很困難。 作者的著作:Programming in Lua,第一版免費基於Lua5.0的http://www.lua.org/pil/contents.html,網上可以搜到一箇中譯的版本,個人感覺學習足夠了,當然也可以在amazon上購買英文第三版。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章