lua绑定C++对象系列三——进阶模型
在系列第二篇文章lua绑定C++对象基础模型中,代码处理上较为麻烦。针对Student.setAge()或者Student.getAge(), 必须包装两个set_age()和get_age()的函数,首先取到对象student的实例指针,再进行调用,整个代码书写较为复杂。一旦这样需要绑定的类很多时,代码量巨大,能否省掉set_age()和get_age()这种多余的包装?
如果要减少包装,能否在一个地方把要注册的C++方法统一起来,抽象一个call_func的公共接口出来,所有的C++成员函数都注册成call_func函数,通过call_func的不同参数来区分调用不同的成员函数。
修改代码如下r_oo2.cpp
1 #include <iostream>
2 #include <cstring>
3 #include <stdlib.h>
4 extern "C" {
5 #include <lua.h>
6 #include <lualib.h>
7 #include <lauxlib.h>
8 }
9 #include "comm.h"
10 #include "luna.h"
11 #include "lunar.h"
12
13 using namespace std;
14
15 class Shape;
16 typedef int (Shape::*ShapePF)(lua_State* L);
17
18 struct method
19 {
20 const char *name;
21 ShapePF pf;
22 };
23
24 class Shape{
25 public:
26 Shape(int i, int j):x(i),y(j){}
27 int setX(lua_State* L){
28 int val = lua_tointeger(L, -1);
29 x = val;
30 return 0;
31 }
32 int setY(lua_State* L){
33 int val = lua_tointeger(L, -1);
34 y = val;
35 return 0;
36 }
37 int getX(lua_State* L){
38 lua_pushinteger(L, x);
39 return 1;
40 }
41 int getY(lua_State* L){
42 lua_pushinteger(L, y);
43 return 1;
44 }
45 int print(lua_State* L){
46 cout<<"x: " << x << " y: " << y << endl;
47 return 0;
48 }
49 int autoDelete(lua_State* L){
50 cout<<"auto gc:" << x <<", "<< y << endl;
51 return 0;
52 }
53 public:
54 static char name[32];
55 static method methods[100];
56 public:
57 int x = 10;
58 int y = 11;
59 };
60
61 char Shape::name[] = "Shape";
62 method Shape::methods[] = {
63 {"setX", &Shape::setX},
64 {"setY", &Shape::setY},
65 {"getX", &Shape::getX},
66 {"getY", &Shape::getY},
67 {"print", &Shape::print},
68 {"delete", &Shape::autoDelete},
69 {NULL, NULL},
70 };
71
72 int delete_shape(lua_State *L)
73 {
74 Shape** p = (Shape**)lua_touserdata(L, 1);
75 (*p)->autoDelete(L);
76 return 0;
77 }
78
79 int call_shape(lua_State *L)
80 {
81 Shape** p = (Shape**)lua_touserdata(L, 1);
82 lua_remove(L, 1);
83
84 method* v = (method*)lua_topointer(L, lua_upvalueindex(1));
85 cout<<"call_shape:"<<v->name<<endl;
86
87
88 return ((*p)->*(v->pf))(L);
89 }
90
91 int n_call_shape(lua_State *L)
92 {
93 method* v = (method*)lua_topointer(L, lua_upvalueindex(1));
94 cout<<"n_call_shape:"<<v->name<<endl;
95
96 Shape* p = (Shape*)lua_topointer(L, lua_upvalueindex(2));
97
98
99 return ((p)->*(v->pf))(L);
100 }
101
102 int create_shape(lua_State *L)
103 {
104 lua_remove(L, 1);
105 int x = lua_tointeger(L, 1);
106 int y = lua_tointeger(L, 2);
107 Shape** p = (Shape**)lua_newuserdata(L, sizeof(Shape*));
108 *p = new Shape(x, y);
109
110 luaL_getmetatable(L, "MetaShape");
111 lua_setmetatable(L, -2);
112
113 luaL_getmetatable(L, "MetaShape");
114 for (method* l = Shape::methods; l->name; l++)
115 {
116 char s[32] = "n_";
117 lua_pushlightuserdata(L,(void*)l);
118 lua_pushlightuserdata(L,(void*)*p);
119 lua_pushcclosure(L, n_call_shape, 2);
120 lua_setfield(L, -2, strcat(s, l->name));
121 }
122
123 lua_pop(L, 1);
124
125 return 1;
126 }
127
128 int lua_openShape(lua_State *L)
129 {
130 //原表Shape
131 if (luaL_newmetatable(L, "MetaShape"))
132 {
133 //注册Shape到全局
134 lua_newtable(L);
135 lua_pushvalue(L, -1);
136 lua_setglobal(L, "Shape");
137
138 //设置Shape的原表,主要是__call,使其看起来更像C++初始化
139 lua_newtable(L);
140 lua_pushcfunction(L, create_shape);
141 lua_setfield(L, -2, "__call");
142 lua_setmetatable(L, -2);
143 lua_pop(L, 1); //把全局Shape pop 出去,这时stack只有MetaShape
144
145
146 //设置Shape的方法列表
147 for(method* l = Shape::methods; l->name; l++)
148 {
149 lua_pushlightuserdata(L,(void*)l);
150 lua_pushcclosure(L, call_shape, 1);
151 lua_setfield(L, -2, l->name);
152 }
153
154 //设置MetaShape指向自己
155 lua_pushvalue(L, -1);
156 lua_setfield(L, -2, "__index");
157 lua_pushcfunction(L, delete_shape);
158 lua_setfield(L, -2, "__gc");
159 }
160 }
161
162 int main(int argc, char* argv[])
163 {
164 cout<<"hello world"<<endl;
165 lua_State *L = luaL_newstate();
166 luaL_openlibs(L);
167 luaL_dofile(L, “tree.lua”);
168
169 lua_openShape(L);
170 print_stack(L);
171
172 luaL_dofile(L, "r_oo2.lua");
173 print_stack(L);
174 lua_settop(L, 0);
175 cout << endl;
176 }
如上对比基础版本,最明显的区别有几点:
1、抽象公共的接口call_shape,把所有的需要注册的函数统一到Shape::methods进行预定义,这样所有的函数注册可以统一到call_shape里面。当然call_shape为了区分不同的函数调用,需要通过参数区分。所以student版本只需要lua_pushcfunction进行简单调用即可,而shape版本使用upvalue + lua_pushcclosure的方式(等于function + param绑定在一起)。元表结构如下:
2、这里多了一个全局Shape表,并且注册了全局Shape表的元方法__call = create_shape,这主要是为了local shape = Shape(100, 200);这种调用自动触发全局表Shape的元方法__call调用create_shape, 使其初始化更接近C++的初始化方法。不过这里只是一个演示作用,其实这种做法会理解起来稍微复杂一点,每次regist 一个C++类的时候,会创建一个元表MetaShape和全局表Shape。也可以像student类那样,使用luaL_register简单注册一个create_shape函数,显示调用创建类实例即可。
其中r_oo2.lua:
1 do
2 local shape = Shape(100, 200);
3 print_tree(shape)
4 print_metatable(shape)
5
6 print(shape:getX(),shape:getY())
7 shape:setX(101)
8 shape:setY(201)
9 shape:print();
10 print(shape:getX(), shape:getY())
11 end
12 collectgarbage("collect");
13
14 do
15 print ""
16 print "new method......"
17 local shape1 = Shape(1000, 2000);
18 print(shape1.n_getX(), shape1.n_getY())
19 shape1.n_setX(1001)
20 shape1:n_setY(2001)
21 shape1.n_print();
22 print(shape1.n_getX(), shape1:n_getY())
23 end
24 collectgarbage("collect");
进一步优化:
因为每次在进行shape.setX 和 shape:setX调用时,后者会默认传入shape对象本身,在进行call_shape调用时需要首先取到自身实例对象再使用。我们这里同时注册了n_call_shape函数,在pushcclosure时,传入两个参数,实例对象指针+method地址,这样n_call_shape函数中,只需要通过upvalue就能取到实例对象。这样shape.n_setX(100)和shape:n_setX(100)虽然传入的第一个参数有区别,但n_call_shape可以同等对待,让业务层调用更加简单,无需关注过多细节。
缺点:
但要注意的是,因为n_call_shape使用的closure包含了实例对象指针本身,所以在regist的时候并不能pushcclosure,这时候实例对象还没有创建,只能推迟到create_shape调用时去注册成员方法。
这样做的弊端也很明显,我们知道一种C++类共享一个MetaShape是为了避免创建多份table,节省内存。如果使用call_shape的方式,所有实例共享一套注册函数。但使用call_shap_n的方式,因为闭包里面含有不同实例对象的地址,那么每个实例对象的注册函数都不相同,会相互覆盖。
运行结果如下:
==========Total:1==========
idx:-1 type:5(table) 0x8c68c0
===========================
MetaShape: 0x8c6a38
not a table
table: 0x8c68c0
__index table: 0x8c68c0+
delete function: 0x8c4db0
setX function: 0x8c4c90
n_setY function: 0x8c6ce0
setY function: 0x8c4d00
n_delete function: 0x8c6e20
n_print function: 0x8c6dd0
n_getY function: 0x8c6d80
n_setX function: 0x8c6c90
n_getX function: 0x8c6d30
__gc function: 0x402734
print function: 0x8c4d70
__name MetaShape
getX function: 0x8c4e00
getY function: 0x8c6900
call_shape:getX
call_shape:getY
100 200
call_shape:setX
call_shape:setY
call_shape:print
x: 101 y: 201
call_shape:getX
call_shape:getY
101 201
auto gc:101, 201
new method......
n_call_shape:getX
n_call_shape:getY
1000 2000
n_call_shape:setX
n_call_shape:setY
n_call_shape:print
x: 1001 y: 2001
n_call_shape:getX
n_call_shape:getY
1001 2001
auto gc:1001, 2001
==========Total:1==========
idx:-1 type:5(table) 0x8c68c0
===========================
通过运行结果:红色标记部分是MetaShape元表定义。同时也可以看到shape1类使用:或者.两种不同的符号进行引用也都正常。所以r_oo2.cpp尝试了两种不同的方式注册call_shape和call_shape_n,这里总结整体结构如下:
这里的的call_shape和n_call_shape只是为了增加对比性,他们的差异在于后者的upvalue多了一个对象指针地址,方便获取对象本身,这种差异性也就决定了元表成员方法设置的时机不同。前者在regist即可完成元表成员注册,后者必须在创建对象实例后再注册。在实际应用中,其实只需要根据需要选用一种模型即可。