lua绑定C++对象系列四——luna模板
在系列文章二三中描述的绑定C++对象基础篇和进阶篇,都有一个很大的问题,就是每个类需要写大量的代码,从类的元表创建、方法注册到实例创建,都需要自己重复写类似的代码。如果涉及N个不同类,会有大量重复的代码,能否创建一个模板类,把这些重复的代码进行简化,通过模板的方式绑定成不同的类?下面的luna<T>就是完成这样一个壮举,例如针对Car类,只需要luna<Car>::regist(L)即可完成注册。在lua层面 local car = Car()就能自动创建Car对象,然后方便的通过car.xxx()调用成员方法。
代码文件luna.h
1 #include <iostream>
2 #include <cstring>
3 extern "C" {
4 #include <lua.h>
5 #include <lualib.h>
6 #include <lauxlib.h>
7 }
8
9 using namespace std;
10
11 #define DECLARE_LUNA_CLASS(obj) \
12 static const char *name;\
13 static luna<obj>::TMethod methods[];
14
15 #define EXPORT_LUNA_FUNCTION_BEGIN(obj) \
16 const char* obj::name = #obj;\
17 luna<obj>::TMethod obj::methods[] = {
18
19 #define EXPORT_LUNA_MEMBER_INT(obj, member) \
20 {#member, nullptr},
21
22 #define EXPORT_LUNA_FUNCTION(obj, func) \
23 {#func, &obj::func},
24
25 #define EXPORT_LUNA_FUNCTION_END(obj) \
26 {nullptr, nullptr}\
27 };
28
29 template<typename T>
30 class luna
31 {
32 public:
33 typedef struct {T* _u;} TObject;
34 typedef int (T::*TPfn)(lua_State* L);
35 typedef struct {const char* name; TPfn pf;} TMethod;
36 public:
37 static int regist(lua_State* L);
38 static int create(lua_State* L);
39 static int call(lua_State* L);
40 static int gc(lua_State* L);
41 };
42
43 template<typename T>
44 int luna<T>::regist(lua_State* L)
45 {
46 //原表Shape
47 if (luaL_newmetatable(L, T::name))
48 {
49 //注册Shape到全局
50 lua_newtable(L);
51 lua_pushvalue(L, -1);
52 lua_setglobal(L, T::name);
53
54 //设置Shape的原表,主要是__call,使其看起来更像C++初始化
55 lua_newtable(L);
56 lua_pushcfunction(L, luna<T>::create);
57 lua_setfield(L, -2, "__call");
58 lua_setmetatable(L, -2);
59 lua_pop(L, 1); //这时候栈只剩下元表
60
61 //设置元表Shape index指向自己
62 lua_pushvalue(L, -1);
63 lua_setfield(L, -2, "__index");
64 lua_pushcfunction(L, luna<T>::gc);
65 lua_setfield(L, -2, "__gc");
66 }
67 return 0;
68 }
69
70 template<typename T>
71 int luna<T>::create(lua_State* L)
72 {
73 lua_remove(L, 1);
74 TObject* p = (TObject*)lua_newuserdata(L, sizeof(TObject));
75 p->_u = new T();
76
77 luaL_getmetatable(L, T::name);
78 lua_setmetatable(L, -2);
79
80 luaL_getmetatable(L, T::name);
81 for (auto* l = T::methods; l->name; l++)
82 {
83 lua_pushlightuserdata(L,(void*)l);
84 lua_pushlightuserdata(L,(void*)p);
85 lua_pushcclosure(L, luna<T>::call, 2);
86 lua_setfield(L, -2, l->name);
87 }
88
89 lua_pop(L, 1);
90
91 return 1;
92 }
93
94 template<typename T>
95 int luna<T>::call(lua_State* L)
96 {
97 TMethod* v = (TMethod*)lua_topointer(L, lua_upvalueindex(1));
98 cout<<"luna<T>::call:"<<v->name<<endl;
99
100 TObject* p = (TObject*)lua_topointer(L, lua_upvalueindex(2));
101
102
103 return ((p->_u)->*(v->pf))(L);
104 }
105
106 template<typename T>
107 int luna<T>::gc(lua_State* L)
108 {
109 TObject* p = (TObject*)lua_touserdata(L, 1);
110 (p->_u)->~T();
111 return 0;
112 }
通过上述代码发现:luna<T>模板类,把一些行为固化下来了。主要改进有几点:
1、 通过EXPORT_LUNA_XXX系列宏定义,把每个业务类需要name和methods列表固化下来,尽可能减少业务层工作量,避免出错。
2、 通过模板的方式,抽象出regist、create、call、gc几个公共接口,流程极为简单。所有不同类都遵循这样的原则。其中:
- Luna<T>::regist: 注册T::name元表和T::name全局表,成员函数注册到元表,统一通过闭包的方式注册到一个公共的调用函数call进行分发调用。全局表T::name只保留__call方法,只是为了保留类似local car = Car() 这种类C++的初始化方式。
- Luna<T>::create:在lua层使用local car = Car()创建对象实例时,注册成员函数,并且通过闭包的形式把成员method地址和对象指针都通过pushcclosure绑定在一起
- Luna<T>::call: 当通过car.xxx()调用成员函数时,通过触发call函数,因为闭包的upvalue不同,通过upvalue包含的不同的method信息,也能取到实例句柄,就能触发不同的成员函数调用,相当于统一通过call进行派发。
- Luna<T>::gc:注册元表的__gc方法,当跟对象实例绑定的userdata被gc回收时,会触发gc调用。
整体结构如如下:
代码文件r_luna.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
16 class Car
17 {
18 public:
19 Car(){}
20 ~Car(){cout<<"Delete Car,Length:"<<length<<" Width:"<<width<<endl;}
21
22 int getLength(lua_State *L){
23 lua_pushinteger(L, length);
24 return 1;
25 }
26 int getWidth(lua_State *L){
27 lua_pushinteger(L, width);
28 return 1;
29 }
30 int setLength(lua_State *L)
31 {
32 int val = lua_tointeger(L, -1);
33 length = val;
34 return 0;
35 }
36 int setWidth(lua_State *L)
37 {
38 int val = lua_tointeger(L, -1);
39 width = val;
40 return 0;
41 }
42 int print(lua_State *L)
43 {
44 cout <<"print length:"<<length<<" width:"<<width<<endl;
45 }
46 public:
47 DECLARE_LUNA_CLASS(Car);
48 public:
49 int length = 100;
50 int width = 200;
51 };
52
53 EXPORT_LUNA_FUNCTION_BEGIN(Car)
54 EXPORT_LUNA_FUNCTION(Car, getLength)
55 EXPORT_LUNA_FUNCTION(Car, getWidth)
56 EXPORT_LUNA_FUNCTION(Car, setLength)
57 EXPORT_LUNA_FUNCTION(Car, setWidth)
58 EXPORT_LUNA_FUNCTION(Car, print)
59 EXPORT_LUNA_FUNCTION_END(Car)
60
61 int main(int argc, char* argv[])
62 {
63 lua_State *L = luaL_newstate();
64 luaL_openlibs(L);
65
66 luaL_dofile(L, "tree.lua");
67
68 //use luna template and bind to object
69 luna<Car>::regist(L);
70
71 luaL_dofile(L, "r_luna.lua");
72 print_stack(L);
73 lua_settop(L, 0);
74 cout << endl;
75
76 lua_close(L);
77 return 0;
78 }
可以看到,定义一个业务类Car,把一些共性的东西通过EXPORT_LUNA_XXX宏固化下来,这样定义一个业务类就只用关注自己的成员函数,并且把自己需要导出到lua的成员函数通过EXPORT_LUNA_FUNCTION宏处理一下即可,方便快捷,代码极其简洁。
代码文件r_luna.lua
1 do
2 local car = Car();
3 print_tree(car)
4 print_metatable(car)
5 print “”
6 car.print();
7 car.setLength(11);
8 car.setWidth(22);
9 car.print();
10 print(car.getLength(), car.getWidth(), car.x, car.y);
11 end
12 collectgarbage("collect");
运行结果:
Car: 0xec8d68
not a table
table: 0xec88c0
print function: 0xec8f20
setWidth function: 0xec6c60
getLength function: 0xec7e10
getWidth function: 0xec8b70
__gc function: 0x402cce
__index table: 0xec88c0+
setLength function: 0xec8bc0
__name Car
luna<T>::call:print
print length:100 width:200
luna<T>::call:setLength
luna<T>::call:setWidth
luna<T>::call:print
print length:11 width:22
luna<T>::call:getLength
luna<T>::call:getWidth
11 22 nil nil
Delete Car,Length:11 Width:22
==========Total:1==========
idx:-1 type:5(table) 0xec88c0
===========================
由上,红色标记的部分是元表。print(car.getLength(), car.getWidth(), car.x, car.y);这行的输出是11 22 nil nil,因为变量x和变量y没有定义,所以打印出来是nil。那么能不能继续扩展luna框架,使其不仅能够导出成员函数,也能导出成员变量?这个就是下一节加强版的lunar的特性。