Lua使用C++中类的调用方法
背景知识
本以为在lua中使用c++的类一件非常复杂的事情。毕竟c++的类与lua有那么多的不同。但是,困难都是纸老虎,只要想办法问题很容易就解决了。
首先,我们要搞明白c++中类和对象的概念。这个概念想要表述清楚是很难的。但是,下面的表达式却道出了类和对象的本质:
类 = 公共数据 + 公共方法
对象 = 私有数据 + 类
其中,类中的公共方法又分为与对象相关的公共方法和与对象无关的公共方法两种。
具体到语言里面:
- 公共数据:类中的静态成员变量
- 与对象无关的公共方法:类中的静态方法
- 与对象有关的公共方法:类中的方法
当我们创建一个类对象时,其实只是创建了一个跟特定类相关联的“结构体”。
这样看来,c++中的类用c语言实现其实一点困难都没有。所谓公共数据,就是全局变量;所谓公共方法其实就是全局函数;所谓对象其实就是带有特定函数指针的结构体。
当然,c++在语言层面上做了一些工作,帮我们做了一些机械的劳动。但是,这些并不是我们要讨论的重点。回到我们原本的话题。于是,就明白了。调用c++的方法其实就是使用对象指针调用公共方法。当然,这在c++在语言层面上做的限制。这个限制可以通过内嵌汇编来突破。但是,这也不是我们要讨论的。我们的重点就是如何得到这个对象指针。只要可以在lua中得到这个对象指针,调用c++类的方法岂不是像调用c/c++的函数一样的简单了。不是吗。
我们先回顾一下在lua中如何调用c/c++函数:
1. 往lua中注册函数指针,并告诉lua该函数有几个参数,几个返回值。
2. 在lua中调用c/c++注册的函数。
3. 在c/c++的函数中,从栈中依次弹出参数,然后进行数据处理,最后将返回值压入栈
仅此而已,很简单的3步即可完成。现在的问题来了,我们要调用的是c++的类方法。我们不能想调用普通函数一样直接通过函数指针进行调用,而需要有一个上下文环境——对象指针。你或许认为我是在故弄玄虚——直接把对象指针通过栈传递到c++里面调用调用其方法不就完了吗。确实,调用方法时就是这样做的。但是,这个对象指针从何而来?反不能提前在c++里面将lua里面会用到的对象指针全都创建好注册到lua里面去吧?当然是不现实。我们需要在lua里面想使用table一样的使用c++类。也就是说,我们可以想随意创建table一样的创建c++类。更夸张的是,我们要让lua的垃圾收集器帮我们回收那些不再使用的c++对象,而不需要我们再调用那面目可睁的delete。
既然目标已经确定,我们就要踏上了旅程。愿望是美好的,显示缺总是残酷的。我们依然不知道如何到达我们那个美好的终点。我们急需一个地图告诉我们如何到达终点。但是地图不会从天上掉下来的。没办法,我们来研究一下如此类似的一个问题是如何解决的吧——在ua中如何实现类。
大家都知道,lua中是不存在类的概念的。但是,lua提供了一个绝好的东西——元表(metatable)。元表是个什么东东?汉语绝对是博大精深啊。很多不明白的东西,通过联想就能知道它是啥了。反正,我看到“元表”就可以想那些“元x”——元素、元气、元年、元宝、元首、元帅……很简单,都是说“它是根本”。于是,我们这儿的元表也就是表的根本。也就是说它就是一个表的根本。那个元表里面包含了些什么呢?当然是,元数据和元方法了。lua操作table就是通过元方法来实现的。当我们把元方法改变了lua对table的操作也就随之改变啦。而元表是多个table所共有。于是,跟c++的类对应起来了:
c\++对象 = 私有数据 + 类(公共数据 + 公共方法)
lua table = 私有数据 + 元表(元数据 + 元函数)
简直就是一模一样啊。事实也是如此,lua中的类就是采用这个方法来实现的。在元表中创建方法,然后创建使用该元表的table。既然如此,那就好办了。我们只要创建一个元表与c++的公共方法映射起来,那么当在lua中创建一个使用该元表的table时就创建一个c++类,当我们在lua中调用元表中的元函数时就跳转到c++类中,当lua回收table的时候我们就销毁c++类。哈哈,如此而已嘛。
路线已经选定,接下来就开始确定如何迈出脚步了。没啥悬念,当然就研究元函数了。通过查lua的手册就能发现以下元方法对我们很有用:
__index: 当table中找不到某个成员时就会调用该函数。
__gc:垃圾回收元函数,当lua回收表格时就会调用它。
至于类的创建嘛,我们只好注册一个创建类的函数了。
ok,理论基础终于准备完毕了。来看代码吧,我已经迫不及待了。
代码
luauseclass.lua
c = ctest() print("c.add(1, 2) ==> " .. c:add(1, 2)); d = ctest() print("d.add(4, 5) ==> " .. d:add(4, 5));
luauseclass.cpp
#include class ctest { public: ctest(){}; virtual ~ctest(){}; int add(int x, int y) { printf("%p add: x=%d, y=%d\n", this, x, y); return x + y; }; }; static int createctest(lua_state* l) { // 创建一个元表为ctest的table——lua对象 *(ctest**)lua_newuserdata(l, sizeof(ctest*)) = new ctest(); lual_getmetatable(l, "ctest"); lua_setmetatable(l, -2); return 1; } static int destoryctest(lua_state* l) { // 释放对象 delete *(ctest**)lua_topointer(l, 1); return 0; } static int calladd(lua_state* l) { // 调用c\++类方法的跳板函数。 ctest* pt = *(ctest**)lua_topointer(l, 1); lua_pushnumber(l, pt->add(lua_tonumber(l, 2), lua_tonumber(l, 3))); return 1; } int main(int argc, char * argv[]) { lua_state *l = lua_open(); luaopen_base(l); // 往lua中注册类 lua_pushcfunction(l, createctest); // 注册用于创建类的全局函数 lua_setglobal(l, "ctest"); lual_newmetatable(l, "ctest"); // 创建一个元表 lua_pushstring(l, "__gc"); // 垃圾回收 lua_pushcfunction(l, destoryctest); lua_settable(l, -3); // 公共函数调用的实现就在此啊 lua_pushstring(l, "__index"); lua_pushvalue(l, -2); // 注意这一句,其实是将__index设置成元表自己 lua_settable(l, -3); lua_pushstring(l, "add"); // 放元表中增加一个函数。这样所有基于该元表的table就都有add方法了 lua_pushcfunction(l, calladd); lua_settable(l, -3); lua_pop(l,1); lual_dofile(l, "luauseclass.lua"); lua_close(l); }
makefile
cppflag=`pkg-config --cflags lua5.1` ldflag=`pkg-config --libs lua5.1` cc=g++ all: luauseclass luauseclass : luauseclass.cpp $(cc) -o $@ $(cppflag) $(ldflag) $<
测试环境
类型 | 值 |
---|---|
操作 | ubuntu 10.10 |
lua | 5.1.4 |