Lua腳本在C++下的舞步(入門指引)

轉自:http://www.acejoy.com/forum.php?mod=viewthread&tid=1931

現在,越來越多的C++服務器和客戶端融入了腳本的支持,尤其在網遊領域,腳本語言已經滲透到了方方面面,比如你可以在你的客戶端增加一個腳本,這個腳本將會幫你在界面上顯示新的數據,亦或幫你完成某些任務,亦或幫你查看別的玩家或者NPC的狀態。。。如此等等。

但是我覺得,其實腳本語言與C++的結合,遠遠比你在遊戲中看到的特效要來的迅猛。它可以運用到方方面面的領域,比如你最常見的應用領域。比如,你可以用文本編輯器,寫一個腳本語言,然後用你的程序加載一下,就會產生出很絢麗的界面。亦或一兩句文本語言,就會讓你的程序發送數據給服務器,是不是很酷呢?
本來我想,寫一篇關於主流腳本語言Lua和Python的文章,但是感覺這樣過於乏味,於是分開來一一介紹,相信對C++瞭解的你,看過我的文章後會對腳本語言這種東西產生濃厚的興趣,我想起以前聽的一個故事,當年Java的創造者講課的時候,一開始先拿一個簡單的不能簡單的小例子,不斷的擴展,最後成爲一個複雜而完美的程序。今天我也就這樣實驗一下吧,呵呵。

當然,我本人不敢說對腳本語言瞭如指掌,只能說略微掌握一些,用過幾年,偏頗之處請大家指正。
下面,開始吧,先說LUA!(本文面向初學者)

Lua語言(http://www.lua.org/),想必不少程序員都聽過,據我所知,由於《魔獸世界》裏面對它的加載,它一下子變成了很多遊戲開發者競相研究的對象,至於這個巴西創造者麼,我不過多介紹,大家有興趣可以谷歌一下。其實網上有很多關於lua的教材和例子,說真的,對於當年的我而言,幾乎看不懂,當時很鬱悶,感覺Lua複雜的要命,有些懼怕,後來沉下心來一點點研究,覺得其實還是蠻簡潔的。只是網上的資料或許偏向於某些功能,導致了邏輯和代碼的複雜。後來總結,其實學習一種腳本語言,完全可以抱着放鬆的心態一點點的研究,反而效果會更好。

在講代碼之前,我要說Lua的一些特點,這些特點有利於你在複雜的代碼調用中,清晰的掌握中間的來龍去脈。實際上,你能常常用到的lua的API,不過超過10個,再複雜的邏輯。基本上也是這麼多API組成的。至於它們是什麼,下面的文章會介紹。另外一個重要之重要的概念,就是棧。Lua與別的語言交互以及交換數據,是通過棧完成的。其實簡單的解釋一下,你可以把棧想象成一個箱子,你要給他數據,就要按順序一個個的把數據放進去,當然,Lua執行完畢,可能會有結果返回給你,那麼Lua還會利用你的箱子,一個個的繼續放下去。而你取出返回數據呢,要從箱子頂上取出,如果你想要獲得你的輸入參數呢?那也很簡單,按照頂上返回數據的個數,再按順序一個個的取出,就行了。不過這裏提醒大家,關於棧的位置,永遠是相對的,比如-1代表的是當前棧頂,-2代表的是當前棧頂下一個數據的位置。棧是數據交換的地方,一定要有一些棧的概念。

好了,基礎的lua語法不在這裏講,百度一下有很多。
先去http://www.lua.org/ 去下載一個最新的Lua代碼(現在穩定版是lua-5.1.4)。它的代碼是用C寫的,所以很容易兼容很多平臺。
在linux下,目錄src下就有專門的Makefile。很簡單,啥都不用做,指定一下位置編譯即可。
在windows下,以VS2005爲例,建立一個空的靜態庫工程(最好不使用預編譯頭,把預編譯頭的選項勾去掉),然後把src下的所有文件(除了Makefile)一股腦拷到工程中去。然後將這些文件添加到你的工程中,編譯,會生成一個*.llib(*是你起的lua庫名),行了,建立一個目錄lib,把它拷過去,然後再建立一個include的文件夾,把你工程目錄下的lua.h,lualib.h,lauxlib.h,拷貝過去。行了,拿着這兩個文件夾,你就可以在你的工程裏使用lua了。
行了,材料齊了,我們來看看怎麼寫一個簡單的lua程序吧。

建立一個文件,起名Sample.lua
裏面添加這樣的代碼。

function func_Add(x, y)
   return x+y;
end

這是一個標準的lua語法,一個函數,實現簡單的a+b操作,並返回操作結果。
保存退出。
多一句嘴,在Lua裏面,是可以支持多數據返回的。
比如你這麼寫:

function func_Add(x, y)
   return x+y, x-y;
end

意思是返回第一個參數是相加的結果,第二個是相減的結果,也是可以的。在lua裏面沒有類型的概念。當然,在C++接受這樣的返回值的時候,也很簡單,請往下看。
好了,材料齊備了,咱們來看看C++程序怎麼調用它。
首先,建立一個類,負責加載這個lua文件,並執行函數操作,我們姑且叫做CLuaFn
要加載這個lua文件,按照正常的思路,我們應該先加載,然後再調用不同的函數。恩,對了,咱們就這麼做。

extern C
{
        #include lua.h
        #include lualib.h
        #include lauxlib.h
};

class CLuaFn
{
public:
        CLuaFn(void);
        ~CLuaFn(void);

        void Init();            //初始化Lua對象指針參數
        void Close();         //關閉Lua對象指針

        bool LoadLuaFile(const char* pFileName);                              //加載指定的Lua文件
        bool CallFileFn(const char* pFunctionName, int nParam1, int nParam2);        //執行指定Lua文件中的函數

private:
        lua_State* m_pState;   //這個是Lua的State對象指針,你可以一個lua文件對應一個。
};

恩,頭文件就這麼多,看看,一點也不復雜吧,看了cpp我想你會更高興,因爲代碼一樣很少。我一個個函數給你們介紹。

void CLuaFn::Init()
{
        if(NULL == m_pState)
        {
                m_pState = lua_open();
                luaL_openlibs(m_pState);
        }
}

初始化函數,標準代碼,沒啥好說的,lua_open()是返回給你一個lua對象指針,luaL_openlibs()是一個好東西,在lua4,初始化要做一大堆的代碼,比如加載lua的string庫,io庫,math庫等等等等,代碼洋洋灑灑一大堆,其實都是不必要的,因爲這些庫你基本都需要用到,除了練習你的打字能力別的意義不大,因爲代碼寫法都是固定的。於是在5以後,Lua的創造者修改了很多,這就是其一,一句話幫你加載了所有你可能用到的Lua基本庫。

void CLuaFn::Close()
{
        if(NULL != m_pState)
        {
                lua_close(m_pState);
                m_pState = NULL;
        }
}

顧名思義,我用完了,關閉我的Lua對象並釋放資源。呵呵,標準寫法,沒啥好說的。

bool CLuaFn:: LoadLuaFile(const char* pFileName)
{
        int nRet = 0;
        if(NULL == m_pState)
        {
                printf(“[CLuaFn:: LoadLuaFile]m_pState is NULL./n”);
                return false;
        }

        nRet = luaL_dofile(m_pState, pFileName);
        if (nRet != 0)
        {
                printf(“[CLuaFn:: LoadLuaFile]luaL_loadfile(%s) is file(%d)(%s)./n”, pFileName, nRet, lua_tostring(m_pState, -1));
                return false;
        }

        return true;
}

呵呵,這個有點意思,加載一個Lua文件。
這裏我要詳細的說一下,因爲Lua是腳本語言,加載lua文件本身的時候纔會編譯。
所以,推薦大家在加載文件的時候儘量放在程序的初始化中,因爲當你執行luaL_dofile()函數的時候,Lua會啓用語法分析器,去分析你的腳本語法是否符合Lua規則,如果你胡亂的傳一個文件過去,Lua就會告訴你文件語法錯誤,無法加載。如果你的Lua腳本很大,函數很多,語法分析器會比較耗時,所以,加載的時候,儘量放在合適的地方,而且,對於一個Lua文件而言,反覆加載luaL_dofile()除了會使你的CPU變熱沒有任何意義。

或許你對printf(“[CLuaFn:: LoadLuaFile]luaL_loadfile(%s) is file(%d)(%s)./n”, pFileName, nRet, lua_tostring(m_pState, -1));這句話很感興趣,這個在幹什麼?這裏我先說lua_tostring(m_pState, -1)這是在幹什麼,還記得我說的Lua是基於棧傳輸數據的麼?那麼,如果報錯,我怎麼知道錯誤是什麼?luaL_dofile標準返回一個int,我總不能到lua.h裏面遍歷這個nRet 是啥意思吧,恩,Lua創造者早就爲你想好了,只不過你需要稍微動一下你的腦筋。Lua的創造者在語法分析器分析你的語法的時候,發現錯誤,會有一段文字告訴你是什麼錯誤,它會把這個字符串放在棧頂。那麼,怎麼取得棧頂的字符串呢?lua_tostring(m_pState, -1)就可以,-1代表的是當前棧的位置是相對棧頂。當然,你也可以看看棧裏面還有一些什麼其他古怪的數據,你可以用1,2,3(這些是絕對位置,而-1是相對位置)去嘗試,呵呵。不過,相信你得到的也很難看懂,因爲一個Lua對象執行的時候,會用很多次棧進行數據交換,而你看到的,有可能是交換中的數據。那麼,話說回來,這句話的意思就是”[CLuaFn:: LoadLuaFile]luaL_loadfile(文件名) is file(錯誤編號)(錯誤具體描述文字)./n”

bool CLuaFn::CallFileFn(const char* pFunctionName, int nParam1, int nParam2)
{
        int nRet = 0;
        if(NULL == m_pState)
        {
                printf(“[CLuaFn::CallFileFn]m_pState is NULL./n”);
                return false;
        }

        lua_getglobal(m_pState, pFunctionName);

        lua_pushnumber(m_pState, nParam1);
        lua_pushnumber(m_pState, nParam2);

        nRet = lua_pcall(m_pState, 2, 1, 0);
        if (nRet != 0)
        {
                printf(“[CLuaFn::CallFileFn]call function(%s) error(%d)./n”, pFunctionName, nRet);
                return false;
        }

        if (lua_isnumber(m_pState, -1) == 1)
        {
                int nSum = lua_tonumber(m_pState, -1);
                printf(“[CLuaFn::CallFileFn]Sum = %d./n”, nSum);
        }

        return true;
}

這個函數是,傳入函數名稱和參數,去你的Lua文件中去執行。
lua_getglobal(m_pState, pFunctionName);
這個函數是驗證你的Lua函數是否在你當前加載的Lua文件中,並把指針指向這個函數位置。

lua_pushnumber(m_pState, nParam1); //—對應你的x參數
lua_pushnumber(m_pState, nParam2);//—對應你的y參數

這就是著名的壓棧操作了,把你的參數壓入Lua的數據棧。供Lua語法器去獲得你的數據。
lua_pushnumber()是一個壓入數字,lua_pushstring()是壓入一個字符串。。。

那麼你會問,如果我有一個自己的類型,一個類指針或者別的什麼,我怎麼壓入?彆着急,方法當然是有的,呵呵,不過你先看看如果簡單的如何做,在下幾講中,我會告訴你更強大的Lua壓棧藝術。
這裏需要注意的是,壓棧的順序,對,簡單說,就是從左到右的參數,左邊的先進棧,右邊的最後進棧。
nRet = lua_pcall(m_pState, 2, 1, 0);
這句話的意思是,執行這個函數,2是輸入參數的個數,1是輸出參數的個數。當然,如果你把Lua函數改成
return x+y, x-y;
代碼需要改成nRet = lua_pcall(m_pState, 2, 2, 0);
明白了吧,呵呵,很簡單吧。
當然,如果函數執行失敗,會觸發nRet,我這裏偷了個懶,如果你想得到爲什麼錯了?可以用lua_tostring(m_pState, -1)去棧頂找,明白?是不是有點感覺了?

lua_isnumber(m_pState, -1)
這句話是判定棧頂的元素是不是數字。因爲如果執行成功,棧頂就應該是你的數據返回值。

int nSum = lua_tonumber(m_pState, -1);
printf(“[CLuaFn::CallFileFn]Sum = %d./n”, nSum);

這個nSum就是返回的結果。
當然,你會問,如果 return x+y, x-y;我該怎麼辦?

int nSum = lua_tonumber(m_pState, -1);
int nSub = lua_tonumber(m_pState, -2);

搞定,看見沒。按照壓棧順序。呵呵,是不是又有感覺了,對,棧就是數據交互的核心。對Lua的理解程度和運用技巧,其實就是對棧的靈活運用和操作。
好了。你的第一個Lua程序大功告成!竟然不是Hello world,呵呵。
好了,我們看看Main函數怎麼寫吧,相信大家都會寫。

#include LuaFn.h

int _tmain(int argc, _TCHAR* argv[])
{
        CLuaFn LuaFn;

        //LuaFn.InitClass();

        LuaFn.LoadLuaFile(“Sample.lua”);
        LuaFn.CallFileFn(“func_Add”, 11, 12);
        getchar();

        return 0;
}

行了,Build一下,看看,是不是你要的結果?如果是,賀喜你,你已經邁出了Lua的第一步。
洋洋灑灑寫了一個小時,喝口水吧,呵呵,下一講,我將強化這個LuaFn類,讓它給我做更多的事情。呵呵,最後,我會讓你打到,用Lua文件直接畫出一個Windows窗體來。並在上面畫出各種按鈕,列表,以及複選框。是不是感覺很酷?用文本去創造一個程序?很激動吧,恩,確實,Lua能給你做到。只要你有耐心看下去。。。

上一節講了一些基本的Lua應用,或許你會說,還是很簡單麼。呵呵,恩,是的,本來Lua就是爲了讓大家使用的方便快捷而設計的。如果設計的過爲複雜,就不會有人使用了。
下面,我要強調一下,Lua的棧的一些概念,因爲這個確實很重要,你會經常用到。熟練使用Lua,最重要的就是要時刻知道什麼時候棧裏面的數據是什麼順序,都是什麼。如果你能熟練知道這些,實際你已經是Lua運用的高手了。
說真的,第一次我接觸棧的時候,沒有把它想的很複雜,倒是看了網上很多的關於Lua的文章讓我對棧的理解雲裏霧裏,什麼元表,什麼User,什麼局部變量,什麼全局變量位移。說的那叫一個暈。本人腦子笨,理解不了這麼多,也不知道爲什麼很多人喜歡把Lua棧弄的七上八下,代碼晦澀難懂。後來實在受不了了,去Lua網站下載了Lua的文檔,寫的很清晰。Lua的棧實際上幾句話足以。
當你初始化一個棧的時候,它的棧底是1,而棧頂相對位置是-1,說形象一些,你可以把棧想象成一個環,有一個指針標記當前位置,如果-1,就是當前棧頂,如果是-2就是當前棧頂前面一個參數的位置。以此類推。當然,你也可以正序去取,這裏要注意,對於Lua的很多API,下標是從1開始的。這個和C++有些不同。而且,在棧的下標中,正數表示絕對棧底的下標,負數表示相對棧頂的相對地址,這個一定要有清晰的概念,否則很容易看暈了。
讓我們看一些例子,加深理解。

lua_pushnumber(m_pState, 11);
lua_pushnumber(m_pState, 12);

int nIn = lua_gettop(m_pState);
/*–這裏加了一行, lua_gettop()這個API是告訴你目前棧裏
元素的個數。如果僅僅是Push兩個參數,那麼nIn的數值是2,
對。沒錯。那麼咱們看看棧裏面是怎麼放的。我再加兩行代碼。
*/
lua_pushnumber(m_pState, 11);
lua_pushnumber(m_pState, 12);

int nIn = lua_gettop(m_pState)
//–讀取棧底第一個絕對座標中的元素
int nData1 = lua_tonumber(m_pState, 1);
//–讀取棧底第二個絕對座標中的元素
int nData2 = lua_tonumber(m_pState, 2);
printf(“[Test]nData1 = %d, nData2 = %d./n”);

如果是你,憑直覺,告訴我答案是什麼?
現在公佈答案,看看是不是和你想的一樣。

[Test]nData1 = 11, nData2 = 12

呵呵,那麼,如果我把代碼換成

lua_pushnumber(m_pState, 11);
lua_pushnumber(m_pState, 12);

int nIn = lua_gettop(m_pState)

//–讀取棧頂第一個相對座標中的元素
int nData1 = lua_tonumber(m_pState, -1); 
//–讀取棧頂第二個相對座標中的元素
int nData2 = lua_tonumber(m_pState, -2);
printf(“[Test]nData1 = %d, nData2 = %d./n”);

請你告訴我輸出是什麼?
答案是

[Test]nData1 = 12, nData2 = 11

呵呵,挺簡單的吧,對了,其實就這麼簡單。網上其它的高階運用,其實大部分都是對棧的位置進行調整。只要你抓住主要概念,看懂還是不難的。什麼元表,什麼變量,其實都一樣,抓住核心,時刻知道棧裏面的樣子,就沒有問題。
好了,回到我上一節的那個代碼。

bool CLuaFn::CallFileFn(const char* pFunctionName, int nParam1, int nParam2)
{
        int nRet = 0;
        if(NULL == m_pState)
        {
                printf(“[CLuaFn::CallFileFn]m_pState is NULL./n”);
                return false;
        }

        lua_getglobal(m_pState, pFunctionName);

        lua_pushnumber(m_pState, nParam1);
        lua_pushnumber(m_pState, nParam2);

        int nIn = lua_gettop(m_pState); <–在這裏加一行。

        nRet = lua_pcall(m_pState, 2, 1, 0);
        if (nRet != 0)
        {
                printf(“[CLuaFn::CallFileFn]call function(%s) error(%d)./n”, pFunctionName, nRet);
                return false;
        }

        if (lua_isnumber(m_pState, -1) == 1)
        {
                int nSum = lua_tonumber(m_pState, -1);
                printf(“[CLuaFn::CallFileFn]Sum = %d./n”, nSum);
        }

        int nOut = lua_gettop(m_pState); <–在這裏加一行。

        return true;
}

nIn的答案是多少?或許你會說是2吧,呵呵,實際是3。或許你會問,爲什麼會多一個?其實我第一次看到這個數字,也很詫異。但是確實是3。因爲你調用的函數名稱佔據了一個堆棧的位置。其實,在獲取nIn那一刻,堆棧的樣子是這樣的(函數接口地址,參數1,參數2),函數名稱也是一個變量入棧的。而nOut輸出是1,lua_pcall()函數在調用成功之後,會自動的清空棧,然後把結果放入棧中。在獲取nOut的一刻,棧內是這幅摸樣(輸出參數1)。
這裏就要再遷出一個更重要的概念了,Lua不是C++,對於C++程序員而言,一個函數會自動創建棧,當函數執行完畢後會自動清理棧,Lua可不會給你這麼做,對於Lua而言,它沒有函數這個概念,一個棧對應一個lua_State指針,也就是說,你必須手動去清理你不用的棧,否則會造成垃圾數據佔據你的內存。
不信?那麼咱們來驗證一下,就拿昨天的代碼吧,你用for循環調用100萬次。看看nOut的輸出結果。。我相信,程序執行不到100萬次就會崩潰,而你的內存也會變的碩大無比。而nOut的輸出也會是這樣的 1,2,3,4,5,6。。。。。
原因就是,Lua不會清除你以前棧內的數據,每調用一次都會給你生成一個新的棧元素插入其中。
那麼怎麼解決呢?呵呵,其實,如果不考慮多線程的話,在你的函數最後退出前加一句話,就可以輕鬆解決這個問題。(Lua棧操作是非線程安全的!)

lua_settop(m_pState, -2);
這句話的意思是什麼?lua_settop()是設置棧頂的位置,我這麼寫,意思就是,棧頂指針目前在當前位置的-2的元素上。這樣,我就實現了對棧的清除。仔細想一下,是不是這個道理呢?

bool CLuaFn::CallFileFn(const char* pFunctionName, int nParam1, int nParam2)
{
        int nRet = 0;
        if(NULL == m_pState)
        {
                printf(“[CLuaFn::CallFileFn]m_pState is NULL./n”);
                return false;
        }

        lua_getglobal(m_pState, pFunctionName);

        lua_pushnumber(m_pState, nParam1);
        lua_pushnumber(m_pState, nParam2);

        int nIn = lua_gettop(m_pState); <–在這裏加一行。

        nRet = lua_pcall(m_pState, 2, 1, 0);
        if (nRet != 0)
        {
                printf(“[CLuaFn::CallFileFn]call function(%s) error(%d)./n”, pFunctionName, nRet);
                return false;
        }

        if (lua_isnumber(m_pState, -1) == 1)
        {
                int nSum = lua_tonumber(m_pState, -1);
                printf(“[CLuaFn::CallFileFn]Sum = %d./n”, nSum);
        }

        int nOut = lua_gettop(m_pState); <–在這裏加一行。
        lua_settop(m_pState, -2);             <–清除不用的棧。

        return true;
}

好了,再讓我們運行100萬次,看看你的程序內存,看看你的程序還崩潰不?
如果你想打印 nOut的話,輸出會變成1,1,1,1,1。。。。
最後說一句,lua_tonumber()或lua_tostring()還有以後我們要用到的lua_touserdata()一定要將數據完全取出後保存到你的別的變量中去,否則會因爲清棧操作,導致你的程序異常,切記!

呵呵,說了這麼多,主要是讓大家如何寫一個嚴謹的Lua程序,不要運行沒兩下就崩潰了。好了,基礎棧的知識先說到這裏,以後還有一些技巧的運用,到時候會給大家展示。
下面說一下,Lua的工具。(爲什麼要說這個呢?呵呵,因爲我們下一步要用到其中的一個幫助我們的開發。)
呵呵,其實,Lua裏面有很多簡化開發的工具,你可以去http://www.sourceforge.net/去找一下。它們能夠幫助你簡化C++對象與Lua對象互轉之間的代碼。
這裏說幾個有名的,當然可能不全。

(lua tinker)如果你的系統在windows下,而且不考慮移植,那麼我強烈推薦你去下載一個叫做lua tinker的小工具,整個工具非常簡單,一個.h和一個.cpp。直接就可以引用到你的工程中,連獨立編譯都不用,這是一個韓國人寫的Lua與 C++接口轉換的類,十分方便,代碼簡潔(居家旅行,必備良藥)。它是基於模板的,所以你可以很輕鬆的把你的C++對象綁定到Lua中。代碼較長,呵呵,有興趣的朋友可以給我留言索要lua tinker的例子。就不貼在這裏了。不過我個人不推薦這個東西,因爲它在Linux下是編譯不過去的。它使用了一種g++不支持的模板寫法,雖然有人在嘗試把它修改到Linux下編譯,但據我所知,修改後效果較好的似乎還沒有。不過如果你只是在 windows下,那就沒什麼可猶豫的,強烈推薦,你會喜歡它的。

(Luabinder)相信用過Boost庫的朋友,或許對這個傢伙很熟悉。它是一個很強大的Linux下Lua擴展包,幫你封裝了很多Lua的複雜操作,主要解決了綁定C++對象和Lua對象互動的關係,非常強大,不過嘛,對於freeeyes而言,還是不推薦,因爲freeeyes很懶,不想爲了一個Lua還要去編譯一個龐大的boost庫,當然,見仁見智,如果你的程序本身就已經加載了boost,那麼就應該毫不猶豫的選擇它。

(lua++)呵呵,這是我最喜歡,也是我一直用到現在的庫,比較前兩個而言,lua++的封裝性沒有那麼好,很多東西還是需要一點代碼的,不過之所以我喜歡,是因爲它是用C寫的,可以在windows下和linux下輕鬆轉換。如果魚與熊掌不能兼得,那麼我寧願選擇一個兼顧兩者的東西,如果有的話,呵呵。當然,lua++就是這麼一個東西,如果你繼續看我的文章,或許你也會喜歡它的。

好了,廢話少說,就讓我選擇lua++作爲我們繼續進行下去的墊腳石吧。
說到Lua++(http://www.codenix.com/~tolua/),這個東西還是挺有淵源的,請你先下載一個。我教你怎麼編譯。

還記得我昨天說過如何編譯Lua麼,現在請你再做一遍,不同的是,請把lua++的程序包中的src/lib中的所有h和cpp,還有include下的那個.h拷貝到你上次建立的lua工程中。然後全部添加到你的靜態鏈接庫工程中去,重新編譯。會生成一個新的lua.lib,這個lua就自動包含了lua++的功能。最後記得把tolua++.h放在你的Include文件夾下。
行了,我們把上次CLuaFn類稍微改一下。

extern C
{
        #include lua.h
        #include lualib.h
        #include lauxlib.h
        #include tolua++”   //這裏加一行
};

class CLuaFn
{
public:
        CLuaFn(void);
        ~CLuaFn(void);

        void Init();            //初始化Lua對象指針參數
        void Close();         //關閉Lua對象指針

        bool LoadLuaFile(const char* pFileName);                              //加載指定的Lua文件
        bool CallFileFn(const char* pFunctionName, int nParam1, int nParam2);        //執行指定Lua文件中的函數

private:
        lua_State* m_pState;   //這個是Lua的State對象指針,你可以一個lua文件對應一個。
};

行了,這樣我們就能用Lua++下的功能了。
昨天,大家看到了 bool CallFileFn(const char* pFunctionName, int nParam1, int nParam2);這個函數的運用。演示了真麼調用Lua函數。
下面,我改一下,這個函數。爲什麼?還是因爲freeeyes很懶,我可不想每有一個函數,我都要寫一個C++函數去調用,太累!我要寫一個通用的!支持任意函數調用的接口!
於是我創建了兩個類。支持任意參數的輸入和輸出,並打包送給lua去執行,說幹就幹。

#ifndef _PARAMDATA_H
#define _PARAMDATA_H

#include <vector>

#define MAX_PARAM_200 200

using namespace std;

struct _ParamData
{
public:
        void* m_pParam;
        char  m_szType[MAX_PARAM_200];
        int   m_TypeLen;

public:
        _ParamData()
        {
                m_pParam    = NULL;
                m_szType[0] = ‘/0′;
                m_TypeLen   = 0;
        };

        _ParamData(void* pParam, const char* szType, int nTypeLen)
        {
                SetParam(pParam, szType, nTypeLen);
        }

        ~_ParamData() {};

        void SetParam(void* pParam, const char* szType, int nTypeLen)
        {
                m_pParam = pParam;
                sprintf(m_szType, “%s”, szType);
                m_TypeLen = nTypeLen;
        };

        bool SetData(void* pParam, int nLen)
        {
                if(m_TypeLen < nLen)
                {
                        return false;
                }

                if(nLen > 0)
                {
                        memcpy(m_pParam, pParam, nLen);
                }
                else
                {
                        memcpy(m_pParam, pParam, m_TypeLen);
                }
                return true;
        }

        void* GetParam()
        {
                return m_pParam;
        }

        const char* GetType()
        {
                return m_szType;
        }

        bool CompareType(const char* pType)
        {
                if(0 == strcmp(m_szType, pType))
                {
                        return true;
                }
                else
                {
                        return false;
                }
        }
};

class CParamGroup
{
public:
        CParamGroup() {};
        ~CParamGroup()
        {
                Close();
        };

        void Init()
        {
                m_vecParamData.clear();
        };

        void Close()
        {
                for(int i = 0; i < (int)m_vecParamData.size(); i++)
                {
                        _ParamData* pParamData = m_vecParamData;
                        delete pParamData;
                        pParamData = NULL;
                }
                m_vecParamData.clear();
        };

        void Push(_ParamData* pParam)
        {
                if(pParam != NULL)
                {
                        m_vecParamData.push_back(pParam);
                }
        };

        _ParamData* GetParam(int nIndex)
        {
                if(nIndex < (int)m_vecParamData.size())
                {
                        return m_vecParamData[nIndex];
                }
                else
                {
                        return NULL;
                }
        };

        int GetCount()
        {
                return (int)m_vecParamData.size();
        }

private:
        typedef vector<_ParamData*> vecParamData;
        vecParamData m_vecParamData;
};

#endif

#endif

我創建了兩個類,把Lua要用到的類型,數據都封裝起來了。這樣,我只需要這麼改寫這個函數。
bool CallFileFn(const char* pFunctionName, CParamGroup& ParamIn, CParamGroup& ParamOut);
它就能按照不同的參數自動給我調用,嘿嘿,懶到家吧!
其實這兩個類很簡單,_ParamData是參數類,把你要用到的參數放入到這個對象中去,標明類型的大小,類型名稱,內存塊。而CParamGroup負責將很多很多的_ParamData打包在一起,放在vector裏面。

好了,讓我們看看CallFileFn函數裏面我怎麼改的。

bool CLuaFn::CallFileFn(const char* pFunctionName, CParamGroup& ParamIn, CParamGroup& ParamOut)
{
        int nRet = 0;
        int i    = 0;
        if(NULL == m_pState)
        {
                printf(“[CLuaFn::CallFileFn]m_pState is NULL./n”);
                return false;
        }

        lua_getglobal(m_pState, pFunctionName);

        //加載輸入參數
        for(i = 0; i < ParamIn.GetCount(); i++)
        {
                PushLuaData(m_pState, ParamIn.GetParam(i));
        }

        nRet = lua_pcall(m_pState, ParamIn.GetCount(), ParamOut.GetCount(), 0);
        if (nRet != 0)
        {
                printf(“[CLuaFn::CallFileFn]call function(%s) error(%s)./n”, pFunctionName, lua_tostring(m_pState, -1));
                return false;
        }

        //獲得輸出參數
        int nPos = 0;
        for(i = ParamOut.GetCount()  1; i >= 0; i–)
        {
                nPos–;
                PopLuaData(m_pState, ParamOut.GetParam(i), nPos);
        }

        int nCount = lua_gettop(m_pState);
        lua_settop(m_pState, -1-ParamOut.GetCount());

        return true;
}

呵呵,別的沒變,加了兩個循環,因爲考慮lua是可以支持多結果返回的,所以我也做了一個循環接受參數。
lua_settop(m_pState, -1-ParamOut.GetCount());這句話是不是有些意思,恩,是的,我這裏做了一個小技巧,因爲我不知道返回參數有幾個,所以我會根據返回參數的個數重新設置棧頂。這樣做可以返回任意數量的棧而且清除乾淨。
或許細心的你已經發現,裏面多了兩個函數。恩,是的。來看看這兩個函數在幹什麼。

bool CLuaFn::PushLuaData(lua_State* pState, _ParamData* pParam)
{
        if(pParam == NULL)
        {
                return false;
        }

        if(pParam->CompareType(“string”))
        {
                lua_pushstring(m_pState, (char* )pParam->GetParam());
                return true;
        }

        if(pParam->CompareType(“int”))
        {
                int* nData = (int* )pParam->GetParam();
                lua_pushnumber(m_pState, *nData);
                return true;
        }
        else
        {
                void* pVoid = pParam->GetParam();
                tolua_pushusertype(m_pState, pVoid, pParam->GetType());
                return true;
        }
}

參數入棧操作,呵呵,或許你會問tolua_pushusertype(m_pState, pVoid, pParam->GetType());這句話,你可能有些看不懂,沒關係,我會在下一講詳細的解釋Lua++的一些API的用法。現在大概和你說一下,這句話的意思就是,把一個C++對象傳輸給Lua函數。
再看看,下面一個。

bool CLuaFn:: PopLuaData(lua_State* pState, _ParamData* pParam, int nIndex)
{
        if(pParam == NULL)
        {
                return false;
        }

        if(pParam->CompareType(“string”))
        {
                if (lua_isstring(m_pState, nIndex) == 1)
                {
                        const char* pData = (const char*)lua_tostring(m_pState, nIndex);
                        pParam->SetData((void* )pData, (int)strlen(pData));
                }
                return true;
        }

        if(pParam->CompareType(“int”))
        {
                if (lua_isnumber(m_pState, nIndex) == 1)
                {
                        int nData = (int)lua_tonumber(m_pState, nIndex);
                        pParam->SetData(&nData, sizeof(int));
                }
                return true;
        }
        else
        {
                pParam->SetData(tolua_tousertype(m_pState, nIndex, NULL), -1);
                return true;
        }
}

彈出一個參數並賦值。pParam->SetData(tolua_tousertype(m_pState, nIndex, NULL), -1);這句話同樣,我在下一講中詳細介紹。
呵呵,好了,我們又進了一步,我們可以用這個函數綁定任意一個Lua函數格式。而代碼不用多寫,懶蛋的目的達到了。
呵呵,這一講主要是介紹了一些基本知識,或許有點多餘,但是我覺得是必要的,在下一講中,我講開始詳細介紹如何綁定一個C++對象給Lua,並讓Lua對其修改。然後返回結果。休息一下,休息一下先。

我把Lua基本的棧規則講了一下,然後完善了一下我的CLuaFn類。讓它可以支持任意參數數量和函數名稱的傳值。當然,這些功能是爲了今天這篇文章而鋪路的。
看了七貓的回帖,呵呵,確實應該說一下SWIG這個工具,說真的,我對這個工具理解不深,因爲沒有怎麼用過,讀過一些關於它的文章,似乎是幫你把C++的功能封裝成一個Lua基本庫的東西,但是後來研究,他可以很輕鬆幫你把公用函數封裝成一個Lua的基本庫(類似C++的dll),但是對於我的需求而言,可能不太一樣。因爲我大量的是需要在C++裏面進行數據傳輸以及變量的交互,所以爲了緊貼C++,我需要很多關聯數據的處理。
我是一名C++程序員,所以在很多時候,不想過多的使用Lua的特性,因爲個人感覺,Lua的語法要比C++的更加靈活。而我更希望,在函數調用的某些習慣上,遵循一些C++的規則。
好了,廢話少說,我們先來看一個類(頭文件)。假設我們要把這個對象,傳輸給Lua進行調用。

#ifndef _TEST_H
#define _TEST_H

class CTest
{
public:
        CTest(void);
        ~CTest(void);

        char* GetData();
        void SetData(const char* pData);

private:
        char m_szData[200];
};
#endif

這個類裏面有兩個函數,一個是GetData(),一個是SetData(),之所以這麼寫,我要讓Lua不僅能使用我的類,還可以給這個類使用參數。
那麼,cpp文件,我們姑且這樣寫。(當然,你可以進行修改,按照你喜歡的方式寫一個方法,呵呵)

char* CTest::GetData()
{
        printf(“[CTest::GetData]%s./n”, m_szData);
        return m_szData;
}

void CTest::SetData(const char* pData)
{
        sprintf(m_szData, “%s”, pData);
}

這是一個標準的類,我需要這個類在Lua裏面可以創造出來,並賦予數值,甚至我可以把CTest作爲一個Lua函數參數,傳給Lua函數讓它去給我處理。讓我們來看看怎麼做。如果使用標準的Lua語法,有點多,所以我就借用一下上次提到的tolua來做到這一切,我一句句的解釋。姑且我們把這些代碼放在LuaFn.cpp裏面。

static int tolua_new_CTest(lua_State* pState)
{
        CTest* pTest = new CTest();
        tolua_pushusertype(pState, pTest, CTest”);
        return 1;
}

static int tolua_delete_CTest(lua_State* pState)
{
        CTest* pTest = (CTest* )tolua_tousertype(pState, 1, 0);
        if(NULL != pTest)
        {
                delete pTest;
        }
        return 1;
}

static int tolua_SetData_CTest(lua_State* pState)
{
        CTest* pTest = (CTest* )tolua_tousertype(pState, 1, 0);
        const char* pData = tolua_tostring(pState, 2, 0);

        if(pData != NULL && pTest != NULL)
        {
                pTest->SetData(pData);
        }

        return 1;
}

static int tolua_GetData_CTest(lua_State* pState)
{
        CTest* pTest = (CTest* )tolua_tousertype(pState, 1, 0);

        if(pTest != NULL)
        {
                char* pData = pTest->GetData();
                tolua_pushstring(pState, pData);
        }

        return 1;
}

看看這幾個靜態函數在幹什麼。
我要在Lua裏面使用CTest,必須讓Lua裏這個CTest對象能夠順利的創造和銷燬。tolua_new_CTest()和tolua_delete_CTest()就是幹這個的。
tolua_pushusertype(pState, pTest, “CTest”); 這句話的意思是,將一個已經在Lua註冊的”CTest”對象指針,壓入數據棧。
同理,CTest* pTest = (CTest* )tolua_tousertype(pState, 1, 0);是將數據棧下的對象以(CTest* )的指針形式彈出來。
tolua_SetData_CTest()函數和tolua_GetData_CTest分別對應CTest的SetData方法和GetData()方法。因爲我們的SetData方法裏面存在變量,那麼同樣,我們需要使用const char* pData = tolua_tostring(pState, 2, 0);將參數彈出來,然後輸入到pTest->SetData(pData);對象中去,當然,你可以有更多若干個參數。隨你的喜好。這裏只做一個舉例。
好了,你一定會問,這麼多的靜態函數,用在哪裏?呵呵,當然是給Lua註冊,當你把這些數據註冊到Lua裏面,你就可以輕鬆的在Lua中使用它們。
讓我們看看,註冊是怎麼做到的。
還是在CLuaFn類裏面,我們增加一個函數。比如叫做bool InitClass();

bool CLuaFn::InitClass()
{
        if(NULL == m_pState)
        {
                printf(“[CLuaFn::InitClass]m_pState is NULL./n”);
                return false;
        }

        tolua_open(m_pState);
        tolua_module(m_pState, NULL, 0);
        tolua_beginmodule(m_pState, NULL);

        tolua_usertype(m_pState, CTest”);
        tolua_cclass(m_pState, CTest”, CTest”, “”, tolua_delete_CTest);

        tolua_beginmodule(m_pState, CTest”);
        tolua_function(m_pState, new”, tolua_new_CTest);
        tolua_function(m_pState, SetData”, tolua_SetData_CTest);
        tolua_function(m_pState, GetData”, tolua_GetData_CTest);
        tolua_endmodule(m_pState);

        tolua_endmodule(m_pState);

        return true;
}

上面的代碼,就是我把上面的幾個靜態函數,綁定到Lua的基礎對象中去。
tolua_beginmodule(m_pState, “CTest”);是隻註冊一個模塊,比如,我們管CTest叫做”CTest”,保持和C++的名稱一樣。這樣在Lua的對象庫中就會多了一個CTest的對象描述,等同於string,number等等基本類型,同理,你也可以用同樣的方法,註冊你的MFC類。是不是有點明白了?這裏要注意,tolua_beginmodule()和tolua_endmodule()對象必須成對出現,如果出現不成對的,你註冊的C++類型將會失敗。
tolua_function(m_pState, “SetData”, tolua_SetData_CTest);指的是將Lua裏面CTest對象的”SetData”綁定到你的tolua_SetData_CTest()函數中去。

好的,讓我們來點激動人心的東西。還記得我們的Simple.lua的文件麼。我們來改一下它。

function func_Add(x, y)
  local test = CTest:new();
  test:SetData(“Im freeeyes!”);
  test:GetData();
  return x..y;
end

我在這個函數裏面,New了一個CTest對象,並進行賦值操作,最後把結果打印在屏幕上。你或許會問,最後一句不是x+y麼,怎麼變成了x..y,呵呵,在Lua中,..表示聯合的意思,就好比在C++裏面, string strName += “freeeyes”。原來覺得x+y有點土,索性返回一個兩個字符串的聯合吧。
好了,我們已經把我們的這個CTest類註冊到了Lua裏面,讓我們來調用一下吧。修改一下Main函數。變成以下的樣子。

int _tmain(int argc, _TCHAR* argv[])
{
        CLuaFn LuaFn;

        LuaFn.InitClass();

        LuaFn.LoadLuaFile(“Sample.lua”);

        CParamGroup ParamIn;
        CParamGroup ParamOut;

        char szData1[20] = {‘/0′};
        sprintf(szData1, “[freeeyes]“);
        _ParamData* pParam1 = new _ParamData(szData1, string”, (int)strlen(szData1));
        ParamIn.Push(pParam1);

        char szData2[20] = {‘/0′};
        sprintf(szData2, “[shiqiang]“);
        _ParamData* pParam2 = new _ParamData(szData2, string”, (int)strlen(szData2));
        ParamIn.Push(pParam2);
        char szData3[40] = {‘/0′};
        _ParamData* pParam3 = new _ParamData(szData3, string”, 40);
        ParamOut.Push(pParam3);

        LuaFn.CallFileFn(“func_Add”, ParamIn, ParamOut);

        char* pData = (char* )ParamOut.GetParam(0)->GetParam();
        printf(“[Main]Sum = %s./n”, pData);

        getchar();

        return 0;
}

如果你完全按照我的,你就可以編譯你的工程了,運行一下,看看是啥結果?

[CTest::GetData]Im freeeyes!.
[Main]Sum = [freeeyes][shiqiang]. 

看看,是不是和我輸出的一樣?

呵呵,有意思吧,你已經可以在Lua裏面用C++的函數了,那麼咱們再增加一點難度,比如,我有一個CTest對象,要作爲一個參數,傳輸給func_Add()執行,怎麼辦?
很簡單,如果你對上面的代碼仔細閱讀,你會發現下面的代碼一樣簡潔。爲了支持剛纔要說的需求,我們需要把Sample.lua再做一點修改。

function func_Add(x, y, f)
  f:SetData(“Im freeeyes!”);
  f:GetData();
  return x..y;
end

f假設就是我們要傳入的CTest對象。我們要在Lua裏面使用它。(我們的CLuaFn都不用改,把main函數稍微改一下即可,來看看怎麼寫。)

// LuaSample.cpp : 定義控制檯應用程序的入口點。
//

#include stdafx.h
#include LuaFn.h

int _tmain(int argc, _TCHAR* argv[])
{
        CLuaFn LuaFn;

        LuaFn.InitClass();

        LuaFn.LoadLuaFile(“Sample.lua”);

        CParamGroup ParamIn;
        CParamGroup ParamOut;

        char szData1[20] = {‘/0′};
        sprintf(szData1, “[freeeyes]“);
        _ParamData* pParam1 = new _ParamData(szData1, string”, (int)strlen(szData1));
        ParamIn.Push(pParam1);

        char szData2[20] = {‘/0′};
        sprintf(szData2, “[shiqiang]“);
        _ParamData* pParam2 = new _ParamData(szData2, string”, (int)strlen(szData2));
        ParamIn.Push(pParam2);

        //只追加了這裏
        CTest* pTest = new CTest();
        _ParamData* pParam3 = new _ParamData(pTest, CTest”, sizeof(CTest));
        ParamIn.Push(pParam3);
       //追加結束
        char szData4[40] = {‘/0′};
        _ParamData* pParam4 = new _ParamData(szData4, string”, 40);
        ParamOut.Push(pParam4);

        LuaFn.CallFileFn(“func_Add”, ParamIn, ParamOut);

        char* pData = (char* )ParamOut.GetParam(0)->GetParam();
        printf(“[Main]Sum = %s./n”, pData);

        getchar();

        return 0;
}

好了,就這麼點代碼,改好了,我們再Build一下,然後點擊運行。看看輸出結果,是不是和以前的一樣?
恩,是不是有點興奮了?你成功的讓Lua開始調用你的C++對象了!並且按照你要的方式執行!還記得我曾在第一篇文章裏面許諾過,我會讓你畫出一個MFC窗體麼?呵呵,如果你到現在依然覺得很清晰的話,說明你的距離已經不遠了。

既然已經到了這裏,我們索性再加點難度,如果我要把CTest作爲一個對象返回回來怎麼做?很簡單,且看。

int _tmain(int argc, _TCHAR* argv[])
{
        CLuaFn LuaFn;

        LuaFn.InitClass();

        LuaFn.LoadLuaFile(“Sample.lua”);

        CParamGroup ParamIn;
        CParamGroup ParamOut;

        char szData1[20] = {‘/0′};
        sprintf(szData1, “[freeeyes]“);
        _ParamData* pParam1 = new _ParamData(szData1, string”, (int)strlen(szData1));
        ParamIn.Push(pParam1);

        char szData2[20] = {‘/0′};
        sprintf(szData2, “[shiqiang]“);
        _ParamData* pParam2 = new _ParamData(szData2, string”, (int)strlen(szData2));
        ParamIn.Push(pParam2);

        CTest* pTest = new CTest();
        _ParamData* pParam3 = new _ParamData(pTest, CTest”, sizeof(CTest));
        ParamIn.Push(pParam3);
        CTest* pTestRsult = NULL;
        _ParamData* pParam4 = new _ParamData(pTestRsult, CTest”, sizeof(pTestRsult));
        ParamOut.Push(pParam4);

        LuaFn.CallFileFn(“func_Add”, ParamIn, ParamOut);

        //接受Lua返回參數爲CTest類型,並調用其中的方法。
        pTestRsult = (CTest* )ParamOut.GetParam(0)->GetParam();
        pTestRsult->GetData();

        getchar();

        return 0;
}

好,編譯,執行。呵呵,看到了吧。

看到這裏,如果你能看的明白,說明你已經對Lua如何調用C++接口,以及C++如何調用Lua有了一定的理解。當然,我寫的這個類也不是很完善,不過做一半的Lua開發,應該是夠用了。以以上的方式,你可以使用Lua駕馭你的C++代碼。
好了,咱們既然已經說到這裏了,再深一步,如果我的類是繼承的,怎麼辦?呵呵,很好的問題。
比如,我的CTest繼承了一個CBase,我的CBase又繼承了一個。。。
在Lua裏面,一樣簡單,我拿MFC的例子來舉例吧,想必大家更喜歡看。
比如 CCmdTarget繼承自CObject。
那麼我在註冊的時候可以這麼寫。

tolua_cclass(tolua_S, “CCmdTarget”, ”CCmdTarget”, ”CObject”, NULL);
這個表示CCmdTarget繼承自CObject對象。
當然,MFC裏面還會有很多類型,比如常數,Lua一樣能處理。
舉個例子說。

tolua_constant(tolua_S, “ES_AUTOHSCROLL”, ES_AUTOHSCROLL);
這樣註冊,你就可以在 Lua裏面使用ES_AUTOHSCROLL這個常數,它會自動綁定ES_AUTOHSCROLL這個C++常數對象。

呵呵,說了這麼多,讓我們來點實際的。我給大家一個我以前寫的MFC封裝類(由於代碼太多,我變成附件給大家),你們可以調用,當然,如果你有興趣,就用我的MFC類,來做一個你喜歡的窗體吧,當然,你必須要用Lua腳本把它畫出來,作爲最後的考驗,呵呵。

附帶全部工程(附帶Lua及tolua++)

/Files/hmxp8/HelloLua_01_03.rar

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