使用tolua++实现C++与LUA相互调用
LUA是一种目前很流行的高效精简的脚本语言。LUA一个特点是比较方便的与C通讯。
然而要在脚本中使用C++类使用基本的LUA方法还是比较麻烦,纯手工暴露一个类的接口到LUA工作量还是很大的,而且都是一些简单的重复劳动。
好在有tolua++这个工具,可以让程序员从简单的重复劳动解脱出来。
tolua++包含两个部分,一个EXE,一个LIB,EXE用来通过package文件生成C函数的胶水函数,而LIB则用来为生成的胶水函数中用到的辅助函数提供实现。
在lua中要调用一个已有的C函数,大体上可以包含2步:
1、在C程序中实现一个胶水函数,该函数只有一个参数lua_State *。在这个胶水函数中,从lua的参数栈中逐个取出参数,再调用原有的C函数,最后将C函数的返回结果通过LUA的栈传回给LUA环境。
2、将该胶水函数注册到LUA的全局表中。
虽然这两个过程都很简单,但是当要暴露的是C++类大量的成员函数时,为每个函数编写胶水函数的工作可想而知。
有了tolua++,这个工作就很简单了,只需要做很少的工作就可以让tolua++自动为每一个待暴露的函数实现胶水函数。
如下面这样一个C++类,类的实现在一个命名空间TstNS中,类中包含有同一函数名的不同重载及自定义的结构体参数:
//file:export.h
#pragma once
#include
using namespace std;
namespace TstNS
{
struct RECT
{
int left,top,right,bottom;
};
class CExport
{
public:
CExport(void);
~CExport(void);
int foo(int a,int b);
int StrLen(const string & str);
int StrLen(const char *pstr);
int area(RECT rc);
int area(RECT *prc);
RECT setrect(int l,int t,int r,int b);
};
}
为了使用tolua++导出这个类到lua中,我们需要写一个package文件,在该文件中定义哪里函数需要导出,我这里的实现如下:
//file:export.pkg
$#include "export.h"
namespace TstNS
{
struct RECT
{
int left,top,right,bottom;
};
class CExport
{
CExport();
~CExport();
int foo(int a,int b);
int StrLen(const string & str);
int StrLen @ StrLen2(const char *pstr);
int area(RECT rc);
int area @ area2(RECT *prc); //将对area(void * prect)的调用重命名为area2
RECT setrect(int l,int t,int r,int b);
};
}
整 体上export.pkg中的内容基本与export.h文件类似,需要注意的是第一行:$#include "export.h"。tolua++根据这个文件来为其中的每个函数生成胶水函数于一个C文件中,由于胶水函数需要调用原来的函数,所以需要在生成的C 文件中需要包含export.h这个文件。第一行就是说在生成的C文件中插入一行#include “export.h"
第二个需要注意的地方在于函数的重载,LUA并不能根据参考类型自动选择调用哪一个重载函数,为此我们可以为不同的重载重命名。”@“正是为这一目的设计的。通过重命名,在LUA脚本中我们就可以显示的为不同的参数调用不同的重载函数。
我们需要使用tolua++的EXE来生成胶水函数代码。它是一个命令行程序,可以多个参数:
..\..\tolua++-1.0.93\bin\tolua++ -n export -o ..\lua_export.cpp export.pkg
这一行的意思是说调用tolua++.exe根据export.pkg生成一个..\lua_export.cpp的C++文件。-n export是说文件中一个需要被外面调用的C接口为:
int tolua_
export_open (lua_State* tolua_S);
下一步,我们看一下在lua脚本中如何调用我们的导出的函数。
--file:test.lua
exp=TstNS.CExport:new();--首先我实例化一个CExport的全局对象。
function foo(a,b)
return exp:foo(a,b);
end
function StrLen(str)
return exp:StrLen(str);
end
function StrLen2(str)
return exp:StrLen2(str);
end
function area(rc)
return exp:area(rc);
end
function area2(prc)
--指针参数使用lua_call.hpp将以void*方式传入,需要做类型转换后才能调用C++中的代码。
return exp:area2(tolua.cast(prc,"TstNS::RECT"));
end
function setrect(l,t,r,b)
return exp:setrect(l,t,r,b);
end
在这个test.lua中我们实现了几个函数,在每一个函数中会调用CExport对应的接口。
到此lua调用C++的基本框架就已经实现了。
下面再看这个demo如何工作。
找到代码中的luatest目录,并打开luatest.sln这个VC工程,上面提到的代码在工程中都可以找到。
直接编译可以查看每一个函数是如何被调用的。
这里要推荐一个我改写的在C++中调用LUA函数的辅助模板类:lua_call.hpp
采用这个辅助类调用LUA函数就和调用普通的C函数一样简单。
下面是luatest.cpp中的代码:
// luatest.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include "Export.h"
#include "tolua++.h"
extern int tolua_export_open (lua_State* tolua_S);
#include "lua_call.hpp"
//使用模板技术对C++调用lua函数进行包装,使用方法参见_tmain
//对于自定义的数据类型,需要特化push_value,及value_extractor两个接口。
template<>
void lua_function_base::push_value(TstNS::RECT rc)
{
void* tolua_obj = Mtolua_new((TstNS::RECT)(rc));
tolua_pushusertype(m_vm,tolua_obj,"TstNS::RECT");
tolua_register_gc(m_vm,lua_gettop(m_vm));
}
template <>
TstNS::RECT lua_function_base::value_extractor()
{
TstNS::RECT * val = (TstNS::RECT *) tolua_tousertype(m_vm, -1,0);
lua_pop(m_vm, 1);
return *val;
}
int _tmain(int argc, _TCHAR* argv[])
{
lua_State *L=lua_open();
int nret=0;
luaL_openlibs(L);
nret=tolua_export_open(L);
nret=luaL_dofile(L,"lua/test.lua");
{
lua_function lua_area(L,"area");
TstNS::RECT rc={0,0,10,20};
int a1=lua_area(rc);
lua_function lua_area2(L,"area2");
TstNS::RECT rc2={0,0,100,20};
int a2=lua_area2(&rc2);
lua_function lua_setarea(L,"setrect");
rc=lua_setarea(5,6,7,8);
lua_function lus_strlen(L,"StrLen");
int nsize1=lus_strlen(std::string("abcdefghij"));
lua_function lus_strlen2(L,"StrLen2");
int nsize2=lus_strlen("abcdefg");
lua_function foo(L,"foo");
int sum=foo(5,6);
}
lua_close(L);
return 0;
}
在_main()中,首先当然是实例化lua环境。
调用tolua_export_open(L);注册tolua++实现的胶水函数到LUA。
调用luaL_dofile(L,"lua/test.lua");执行test.lua脚本。
调用lua脚本中实现的函数。
最后释放LUA环境:lua_close(L);
这其中lua脚本中实现的函数是比较取巧的。利用lua_call.hpp,我们只需要两行代码就可以实现:
第一步,定义一个lua_function对象,找到LUA中的函数。
第二步,为函数传入参数。
需要注意的是,当为CExport新增加一个需要导出的函数时,要记得将新的函数名加到export.pkg文件中,并调用tolua++重新生成胶水函数。在demo中有一个build.bat文件,每次更新后执行一次即可。