在Lua中調用C原因


  我們說用Lua可以調用C語言函數,但這並不意味着Lua可以調用所有的C函數。當C語言調用Lua函數時,該函數必須遵循一個簡單的規則來傳遞參數和獲取結果。同樣,當Lua調用C函數時,這個C函數也必須遵循某種規則來獲取參數和返回結果。此外,當Lua調用C函數時,我們必須註冊該函數,即必須以一種恰當的方式爲Lua提供該C函數的地址。
  Lua調用C函數時,也使用一個與C語言調用Lua函數時相同類型的棧,C函數從棧中獲取參數,並將結果壓入棧中。
  此處的重點在於,這個棧不是一個全局結構;每個函數都有其私有的局部棧。當Lua調用一個C函數時,第一個參數總是位於這個局部棧中索引爲1的位置。即使一個C函數調用了Lua代碼,而且Lua代碼又再次調用了同一個C函數,這些調用每一次都只會看到本次調用自己的私有棧,其中索引爲1的位置上就是一個參數。

C函數

  先舉一個例子,讓我們實現一個簡化版本的正弦函數,該函數返回某個給定數的正弦值:

static int l_sin(lua_State *L){
	double d = lua_tonumber(L,1);
	lua_pushnumber(L,sin(d));
	return 1;
}

所有在Lua中註冊的函數都必須使用一個相同的原型,該原型就是定義在lua.h中的lua_CFunction:

typedef int (*lua_CFunction)(lua_State *L);

從C語言的角度看,這個函數只有一個指向Lua狀態類型的指針作爲參數,返回值爲一個整型數,代表壓入棧中的返回值的個數。因此,該函數在壓入結果前無須清空棧。在該函數返回後,Lua會自動保存返回值並清空整個棧。
  在Lua中,調用這個函數前,還必須通過lua_pushcfunction註冊該函數。函數lua_pushcfunction會獲取一個指向C函數的指針,然後在Lua中創建一個"function"類型,代表待註冊的函數。一旦完成註冊,C函數就可以像其他Lua函數一樣行事了。
  一種快速測試函數l_sin的方法是,將其代碼放到簡單解釋器中,並將下列代碼添加到luaL_openlibs調用的後面:

lua_pushcfunction(L,l_sin);
lua_setglobal(L,"mysin");

上述代碼的第一行壓入一個函數類型的值,第二行將這個值賦給全局變量mysin。完成這些修改後,我們就可以在Lua腳本中使用新函數mysin了。
  要編寫一個更專業的正弦函數,必須檢查其參數的類型,而輔助庫可以幫助我們完成這個任務。函數luaL_checknumber可以檢查指定的參數是否爲一個數字:如果出現錯誤,該函數會拋出一個告知性的錯誤信息;否則,返回這個數字。只需對上面這個正弦函數稍作修改:

static int l_sin(lua_State *L){
	double d = luaL_checknumber(L,1);
	lua_pushnumber(L,sin(d));
	return 1;
}

做了上述修改後,如果調用mysin(‘a’)就會出現如下的錯誤:

bad argument #1 to 'mysin' (number expected, got string)

函數luaL_checknumber會自動用參數的編號(#1)、函數名(“mysin”)、期望的參數類型及實際的參數類型來填寫錯誤信息。
  下面是一個更復雜的示例,編寫一個函數返回指定目錄下的內容。由於ISO C中沒有具備這種功能的函數,因此Lua沒有在標準庫中提供這樣的函數。這裏,我們假設使用一個POSIX兼容的操作系統。這個函數以一個目錄路徑字符串作爲參數,返回一個列表,列出該目錄下的內容。例如,調用dir("/home/lua")會得到形如{".","…",“src”,“bin”,“lib”}的表。該函數的完整代碼如下:

一個讀取目錄的函數

#include <dirent.h>
#include <errno.h>
#include <string.h>

#include "lua.h"
#include "luaxlib.h"

static int l_dir(lua_State *L){
	DIR *dir;
	struct dirent *entry;
	int i;
	const char *path = lual_checkstring(L,1);

	dir = opendir(path);
	if (dir == NULL){
		lua_pushnil(L);
		lua_pushstring(L,strerror(error));
		return 2;
		}
		lua_newtable(L);
		i = 1;
		while ((entry = readdir(dir)) != NULL){
			lua_pushinteger(L,i++);
			lua_pushstring(L,entry -> d_name);
			lua_settable(L,3);
			}
			closedir(dir);
			return 1;
}

  該函數先使用與luaL_checknumber類似的函數luaL_checkstring檢查目錄路徑是否爲字符串,然後使用函數opendir打開目錄。如果無法打開目錄,該函數會返回nil以及一條用函數strerror獲取的錯誤信息。在打開目錄後,該函數會創建一張新表,然後用目錄中的元素填充這張新表。最後,該函數關閉目錄並返回1,在C語言中即表示該函數將其棧頂的值返回給了Lua。
  在某些情況中,l_dir的這種實現可能會造成內存泄露。該函數調用的三個Lua函數均可能由於內存不足而失敗。這三個函數中的任意一個執行失敗都會引發錯誤,並中斷函數l_dir的執行,進而也就無法調用closedir了。

延續

  通過lua_pcall和lua_call,一個被Lua調用的C函數也可以回調Lua函數。標準庫中有一些函數就是這麼做的:table.sort調用了排序函數,string.gsub調用了替換函數,pcall和xpcall以保護模式來調用函數。如果你還記得Lua代碼本身就是被C代碼調用的,那麼你應該知道調用順序類似於:C調用Lua,Lua又調用了C,C又調用了Lua。
  通常,Lua語言可以處理這種調用順序;畢竟,與C語言的集成是Lua的一大特點。但是,有一種情況下,這種相互調用會有問題,那就是協程。
  Lua語言中的每個協程都有自己的棧,其中保存了該協程所掛起調用的信息。具體地說,就是該棧中存儲了每一個調用的返回地址、參數及局部變量。對於Lua函數的調用,解釋器只需要這個棧即可,我們將其成爲軟棧。然而,對於C函數的調用,解釋器必須使用C語言棧。畢竟,C函數的返回地址是局部變量都位於C語言棧中。
  對於解釋器來說,擁有多個軟棧並不難;然而,ISO C的運行時環境卻只能擁有一個內部棧。因此,Lua中的協程不能掛起C函數的執行:如果一個C函數位於從resume到對應yield的調用路徑中,那麼Lua無法保存C函數的狀態以便在下次resume時恢復狀態。請考慮如下的示例:

co = coroutine.wrap(function()
	print(pcall(coroutine.yield))
end)
co()

-- false attempt to yield across metamethod/C-call boundary

函數pcall是一個C語言函數;因此,Lua5.1不能將其掛起,因爲ISO C無法掛起一個C函數並在之後恢復其運行。
  在Lua5.2及後續版本中,用延續改善了對這個問題的處理。Lua5.2使用長跳轉實現了yield,並使用相同的方式實現了錯誤信息處理。長跳轉簡單地丟棄了C語言棧中關於C函數的所有信息,因而無法resume這些函數。但是,一個C函數foo可以指定一個延續函數foo_k,該函數也是一個C函數,在要恢復foo的執行時它就會被調用。也就是說,當解釋器發現它應該恢復函數foo的執行時,如果長調轉已經丟棄了C語言棧中有關foo的信息,則調用foo_k來替代。
  爲了說得更具體些,我們將pcall的實現作爲示例。在Lua5.1中,該函數的代碼如下:

static int luaB_pcall(lua_State *L){
	int status;
	luaL_checkany(L,1);
	status = lua_pcall(L,lua_gettop(L) - 1, LUA_MULTRET,0);
	lua_pushboolean(L,(status == LUA_OK));
	lua_insert(L,1);
	return lua_gettop(L);
}

如果程序正在通過lua_pcall被調用的函數yield,那麼後面就不能恢復luaB_pcall的執行。因此,如果我們在保護模式的調用下試圖yield時,解釋器就會拋出異常。Lua5.3使用基本類似於下面示例中的方式實現了pcall。

使用延續實現pcall

static int finishpcall (lua_State *L, int status, intptr_t ctx){
	(void)ctx;
	status = (status != LUA_OK && status != LUA_YIELD);
	lua_pushboolean (L,(status == 0 ));
	lua_insert(L,1);
	return lua_gettop(L);
}
static int luaB_pcall(lua_State *L){
	int status;
	luaL_checkany(L,1);
	status = lua_pcall(L,lua_gettop(L) - 1, LUA_MULTERT,0,0,finishpcall);
	return finsihpcall(L,status,0);
}

  與Lua5.1中的版本相比,上述實現有三個重要的不同點:首先,新版本用lua_pcallk替換了lua_pcall;其次,新版本在調用完lua_pcallk後把完成的狀態傳給了新的輔助函數finishpcall;第三,lua_pcallk返回的狀態除了LUA_OK或者一個錯誤外,還可以是LUA_YIELD。
  如果沒有發生yield,那麼lua_pcallk的行爲與lua_pcall的行爲完全一樣。但是,如果發生yield,情況則大不相同。如果一個被原來lua_pcall調用的函數想要yield,那麼Lua5.3會像Lua5.1版本一樣引發錯誤。但當被新的lua_pcallk調用的函數yield時,則不會出現發生錯誤:Lua會做一個長跳轉並且丟棄C語言棧中有關luaB_pcall的元素,但是會在協程軟棧中保存傳遞給函數lua_pcallk的延續函數的引用。後來,當解釋器發現應該返回到luaB_pcall時,它就會調用延續函數。
  當發生錯誤時,延續函數finishpcall也可能會被調用。與原來的luaB_pcall不同,finishpcall不能獲取lua_pcallk所返回的值。因此,finishpcall通過額外的參數status獲取這個結果。當沒有錯誤時,status是LUA_YIELD而不是LUA_OK,因此延續函數可以檢查它是如何被調用的。當發生錯誤時,status還是原來的錯誤碼。
  除了調用的狀態,延續函數還接收一個上下文。lua_pcallk的第5個參數是一個任意的整型數,這個參數被當做延續函數的最後一個參數來傳遞。這個值允許原來的函數直接向延續函數傳遞某些任意的信息。
  Lua5.3的延續體系是一種爲了支持yield而設計的精巧機制,但它也不是萬能的。某些C函數可能會需要它們的延續傳遞相當多的上下文。例如,table.sort將C語言棧用於遞歸,而string.gsub則必須跟蹤捕獲,還要跟蹤和一個用於存放部分結果的緩衝區。雖然這些函數能以"yieldbale"的方式重寫,但與增加的複雜性和性能損失相比,這樣做似乎並不值得。

C模塊

  Lua模塊就是一個代碼段,其中定義了一些Lua函數並將其存儲在恰當的地方。爲Lua編寫的C語言模塊可以模仿這種行爲。除了C函數的定義外,C模塊還必須定義一個特殊的函數,這個特殊的函數相當於Lua庫中的主代碼段,用於註冊模塊中所有的C函數,並將它們存儲在恰當的地方。與Lua的主代碼段一樣,這個函數還應該初始化模塊中所有需要初始化的其他東西。
  Lua通過註冊過程感知到C函數。一旦一個C函數用Lua表示和存儲,Lua就會通過對其地址的直接引用來調用它。換句話說,一旦一個C函數完成註冊,Lua調用它時就不再依賴於其函數名、包的位置以及可見性規則。通常,一個C模塊中只有一個用於打開庫的公共函數;其他所有的函數都是私有的,在C語言中被聲明爲static。
  當我們使用C函數來擴展Lua程序時,將代碼設計爲一個C模塊是個不錯的想法。因爲即使我們現在只想註冊一個函數,但遲早總會需要其他的函數。通常,輔助庫爲這項工作提供了一個輔助函數。宏luaL_newlib接收一個由C函數及其對應函數名組成的數組,並將這些函數註冊到一個新表中。舉個例子,假設我們要用之前定義的函數l_dir創建一個庫。首先,必須定義這庫函數:

static int l_dir(lua_State *L){
	同前
}

然後,聲明一個數組,這個數組包含了模塊中所有的函數及其名稱。數組元素的類型爲luaL_Reg,該類型是由兩個字段組成的結構體,這兩個字段分別是函數名和函數指針。

static const struct luaL_Reg mylib[] = {
	{"dir",l_dir},
	{NULL,NULL}
};

在上述例子中,只聲明瞭一個函數。數組的最後一個元素永遠是是{NULL,NILL},並以此標識數組的結尾。最後,我們使用函數luaL_newlib聲明一個主函數:

int luaopen_mylib(lua_State *L){
	luaL_newlib(L,mylib);
	return 1;
}

對函數luaL_newlib的調用會新創建一個表,並使用由數組mylib指定的"函數名-函數指針"填充這個新創建的表。當luaL_newlib返回時,它把這個新創建的表留在棧中,在表中它打開了這個庫。然後,函數luaopen_mylib返回1,表示將這個表返回給Lua。
  編寫完這個庫以後,我們還必須將其鏈接到解釋器。如果Lua解釋器支持動態鏈接的話,那麼最簡便的方法是使用動態鏈接機制。在這種情況下,必須將這個庫放到C語言路徑中的某個地方。在完成了這些步驟後,就可以使用require在Lua中直接加載這個模塊了:

local mylib = requrire "mylib"

上述的語句會將動態庫mylib鏈接到Lua,查找函數luaopen_mylib,將其註冊爲一個C語言函數,然後調用它以打開模塊。
  動態鏈接器必須知道函數luaopen_mylib的名字才能找到它。它總是尋找名爲"luaopen + 模塊名"這樣的函數。因此,如果我們的模塊名爲mylib,那麼該函數應該命名爲luaopen_mylib。
  如果解釋器不支持動態鏈接,就必須連同新庫一起重新編譯Lua語言。除了重新編譯,還需要以某種方式告訴獨立解釋器,它應該在打開一個新狀態時打開這個庫。一個簡答的做法是把luaopen_mylib添加到由lua_openlibs打開的標住庫列表中,這個列表位於文件linit.c中。

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