zoukankan      html  css  js  c++  java
  • Lua脚本在C++下的舞步(入门指引)(转)

    Lua脚本在C++下的舞步(一)(入门指引)

    转帖来自:http://www.acejoy.com/bbs/viewthread.php?tid=1931&extra=page%3D1

    现在,越来越多的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脚本在C++下的舞步(二)

    上一节讲了一些基本的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脚本在C++下的舞步(三)

    上一讲我把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(“I’m 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]I’m freeeyes!.
    [Main]Sum = [freeeyes][shiqiang].
    看看,是不是和我输出的一样?

    呵呵,有意思吧,你已经可以在Lua里面用C++的函数了,那么咱们再增加一点难度,比如,我有一个CTest对象,要作为一个参数,传输给func_Add()执行,怎么办?
    很简单,如果你对上面的代码仔细阅读,你会发现下面的代码一样简洁。为了支持刚才要说的需求,我们需要把Sample.lua再做一点修改。

    function func_Add(x, y, f)
      f:SetData(“I’m 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

  • 相关阅读:
    623. Add One Row to Tree 将一行添加到树中
    771. Jewels and Stones 珠宝和石头
    216. Combination Sum III 组合总数三
    384. Shuffle an Array 随机播放一个数组
    382. Linked List Random Node 链接列表随机节点
    向github项目push代码后,Jenkins实现其自动构建
    centos下安装Jenkins
    python提取批量文件内的指定内容
    批处理实现:批量为文件添加注释
    python抓取每期双色球中奖号码,用于分析
  • 原文地址:https://www.cnblogs.com/Zephyroal/p/2259777.html
Copyright © 2011-2022 走看看