由于luckyScript引擎接口使用上的不便,我为它实现了一个基于C++的封装库,使用它可以比较方便地实现:类的注册,任意C++函数的注册,调用脚本函数,访问脚本变量等比较核心的功能,虽然,用luckyScript引擎本身也可以做到上述这些,但我想你不会喜欢为每个主程序对象实现一大堆回调处理函数的,那在需要提供给脚本使用的东西数量比较大的时候会是个让人崩溃的工作量,所以,必须在luckyScript上再实现一层封装简化这个过程,考虑到luckyScript只是一个无名小卒,没有人会花时间去专门为它做那么个封装的,所以只好由我自己来完成这个工作了,这个封装库的源码会在发布luckyScript库的时候附带一起发布,下面,我详细介绍下这个封装库最核心的几个功能是如何实现的,虽然是基于luckyScript的封装,但我想对于理解其他些比较流行的脚本(比如lua)的封装库也会是有用的。
一、任意C++函数的注册
在luckyScript中,每个提供给脚本使用的主程序都必须有相同的原型,这在脚本中是为了可以方便的统一处理,但是,方便了开发者的东西通常就是给用户提供了不方便,用户得为每个在脚本中调用的主程序函数实现一个符合脚本规定原型的封装,这会是个烦人的工作,为了避免它,我们必须用一层机制把这些细节隐藏起来,有了luckyScript中UserData的概念,我们很容易就能想到可以把注册给脚本的函数跟真正调用的函数分离开来,事先把需要调用的函数指针当作UserData传给脚本,在脚本call的那个主程序函数里,我们再把这个函数指针取出来,之后就可以进行我们真正所需要的函数调用了,这的确是个办法,但马上我们就会遇到另一个问题,UserData都是void指针类型的,我们如何能在取出来的时候知道它是什么类型的函数指针呢?必须有个办法能把这个函数指针的类型信息传过来,于是,你会想到C++最强大的特性,模板,对了,可以把函数指针类型当作模板参数,这样我们就可以把void指针转换为正确的函数指针了,想到了这一层,我们就可以写出给脚本调用的一个通用主函数的定义了:
1template<typename Func>
2struct Functor
3{
4 static void invoke(RuntimeState* state)
5 {
6
7 }
8};
9
Func就是我们需要的函数指针类型,在这个函数里面我们需要做些什么事情呢?恩,我们必须把函数指针数据取出来,转换为正确的类型,然后再调用它,于是我们就遇到了第二个问题:Func只是一个类型的存贮,我们知道func是一个函数指针,但却无法知道它具体是什么样的一个函数指针,它有多少个参数,有没有返回值我们都不知道,因此,必须把函数的调用再往下移一层,利用模板参数的自动匹配,通过传递Func参数让程序自动转到我们有足够信息来调用这个函数指针的地方去,请先看下面一段代码:
1template <typename RT>
2int call(RT (*func)(), RuntimeState* state)
3{
4}
5template <typename RT, typename P1>
6int call(RT (*func)(P1), RuntimeState* state)
7{
8}
9
RT (*func)()代表没有参数的函数指针类型,RT (*func)(P1)代表有一个参数的函数指针类型,调用这个call函数,我们需要给一个函数指针参数,根据函数指针的类型,编译器会自动寻找匹配的模板函数,如果这个函数指针是没有参数的,那么就会调用第一个call函数,有一个参数则会调用第二个,现在,我们上面的问题是不是已经解决了?在Functor::invoke里面,我们能拿到正确的函数指针,这意味着我们可以调用call函数让程序转到能让我们拥有更多函数信息的地方去,这样就可以写出Functor::invoke函数的代码了
1template<typename Func>
2 class Functor
3 {
4 public:
5 static void invoke(RuntimeState* state)
6 {
7 //函数指针buffer
8 unsigned char* buffer = (unsigned char*)lucky_popValueAsUserData(state);
9
10 //转换为正确的函数指针类型并调用call
11 call((*(Func*)buffer),state);
12 }
13 };
然后在call里面,是不是就可以直接调用了呢?答案是还是不行,因为在call里面,我们还是不知道函数是不是有返回值的,那个RT可能是void或具体的类型,因此,我们得把函数的调用进一步下调:
1//有返回值
2template<typename RT>
3 struct Caller
4 {
5 static int call(RT (*func)(), RuntimeState* state)
6 {
7 }
8
9 template <typename P1>
10 static int call(RT (*func)(P1), RuntimeState* state)
11 {
12 }
13}
14//没有返回值
15template<>
16 struct Caller<void>
17 {
18 static int call(void (*func)(), RuntimeState* state)
19 {
20 }
21
22 template <typename P1>
23 static int call(void (*func)(P1), RuntimeState* state)
24 {
25 }
26}
在第二段代码中的call函数里,我们这样调用:
1template <typename RT>
2 int call(RT (*func)(), RuntimeState* state)
3 {
4 return Caller<RT>::call(func, state);
5 }
6
7
8 template <typename RT, typename P1>
9 int call(RT (*func)(P1), RuntimeState* state)
10 {
11 return Caller<RT>::call(func, state);
12 }
这样就能根据RT的类型进一步转到Caller<RT>::call或者Caller<void>::call上去了,ok,到这一步,我们已经有足够的信息来调用这个函数指针了,调用函数,需要先取出参数信息,我们需要的参数都保存在栈上,luckyScript提供了lucky_popValueAs...样的api来提供得到栈上元素值的功能,但这样我们就会碰到一开头的问题:调用这些API需要你事先知道栈顶元素的类型,可我们有的只是P1这样的参数类型载体,确切的类型在这里是无法知道的,因此,有必要封装参数取出的操作:
1struct Param
2{
3 static inline void get(TypeWrapper<void>, RuntimeState*)
4 { }
5 static inline bool get(TypeWrapper<bool>, RuntimeState* state)
6 {
7 //bool对应的是脚本中的int类型
8 return lucky_popValueAsInt(state) != 0; }
9 static inline char get(TypeWrapper<char>, RuntimeState* state)
10 {
11 //char也对应int
12 return static_cast<char>(lucky_popValueAsInt(state));
13 }
14}
上面的TypeWrapper只是为了区别参数类型而加进去的,这里我只举了void,bool,char,的取出操作作为示范,其实还有很多类型,就不一一列出了,根据不同的类型,我们调用不同的lucky_popValueAs..来取出参数,与此类推,对于有返回值的函数,把返回值传给脚本的操作也是必须封装的:
struct ReturnVal
{
static inline void set(RuntimeState* state, bool value) { lucky_setReturnValue(state,(int)value); }
static inline void set(RuntimeState* state, char value) { lucky_setReturnValue(state,(int)value); }
static inline void set(RuntimeState* state, unsigned char value) { lucky_setReturnValue(state,(int)value); }
..
』
这样就可以用Param::get和ReturnVal::set来取得参数值及设置返回值了,为了让事情能更简单点,我们定义两个宏:
#ifndef __defparam
#define __defparam(arg) P##arg p##arg = Param::get(TypeWrapper<P##arg>(),state);
#endif
#ifndef __return
#define __return(retVal) ReturnVal::set(state,retVal);
#endif
一切准备就绪,可以进行我们真正的函数调用操作了,下面用有一个参数的函数的调用代码做例子,请注意看代码的注释
template<typename RT>
struct Caller
{
template <typename P1>
static int call(RT (*func)(P1), RuntimeState* state)
{
//测试栈上的元素是不是我们需要的类型
__argassert(1,-1);
//这句相当于P1 p1 = Param::get(TypeWrapper<P1>(),state);
__defparam(1);
//调用函数
RT retVal = (*func)(p1);
//把返回值传给脚本,这句是ReturnVal::set(state,retVal);
__return(retVal);
return 1;
}
}
这样就完成了核心的函数调用,我们一开始的设想已经实现,现在该考虑提供给用户注册函数的接口的实现了,我们所需要用户做的就是提供一个任意类型的函数指针跟一个在脚本中使用的函数名,之后我们就会将这个函数指针作为用户数据传进脚本,然后再为这个函数类型注册一个Functor<Func>::invoke函数:
1template<typename Func>
2 void registerFunction(Func func,const char* funcName)
3 {
4 //增加函数指针大小的UserData
5 unsigned char* buffer = (unsigned char*)lucky_addUserData(sizeof(func));
6
7 //复制函数指针数据
8 memcpy(buffer,&func,sizeof(func));
9
10 //得到返回类型名
11 std::string typeName = LuckySystem::ReturnType::name(func);
12
13 //注册Functor<Func>::invoke
14 lucky_registerHostFunc(mRuntimeState,LuckySystem::Functor<Func>::invoke,funcName,typeName.c_str());
15
16 //清空跟主函数绑定的UserData
17 lucky_clearAddedUserData();
18 }
上面只是举0到1个参数的函数指针的调用作为例子,通过定义更多的函数类型,我们就可以让注册函数的适用范围进一步扩宽,在我所做的这个脚本封装中,最多可以支持拥有7个参数的主函数的注册。
二、任意类成员函数的注册
原理跟注册非成员函数是完全一样的,唯一不同的是,我们需要把这个类成员函数所属的对象的一个实例也当作用户数据传给脚本,然后在脚本调用的那个函数里,我们再把这个实例跟函数指针一并取出来进行调用。如果上面的内容你都已经认真看了,那么理解下面的代码是很容易的:
1template<typename Callee,typename Func>
2 struct MemberFunctor
3 {
4 static void invoke(RuntimeState* state)
5 {
6 //取出函数指针buffer
7 unsigned char* funcBuffer = (unsigned char*)lucky_popValueAsUserData(state);
8 //取出类的实例buffer
9 unsigned char* calleeBuffer = (unsigned char*)lucky_popValueAsUserData(state);
10
11 //调用
12 call((*(Callee*)calleeBuffer),(*(Func*)funcBuffer),state);
13 }
14 };
在这个MemberFuncto::invoke里,除了取出成员函数指针外,我们还取出了这个成员函数所属的类的一个实例,并把它们作为参数调用call函数,在这里调用的call函数跟注册非成员函数时的call重载相比,就是多了一个Callee& callee参数
template <typename Callee, typename RT>
int call(Callee& callee, RT (Callee::*func)(), RuntimeState* state)
{
return Caller<RT>::call(callee, func, state);
}
template <typename Callee, typename RT, typename P1>
int call(Callee& callee, RT (Callee::*func)(P1), RuntimeState* state)
{
return Caller<RT>::call(callee, func,state);
}
在Caller<RT>::call中,我们把类的实例跟成员函数指针连接起来进行调用,以一个参数的函数指针调用代码为例:
template <typename Callee, typename P1>
static int call(Callee& callee, RT (Callee::*func)(P1), RuntimeState* state)
{
__argassert(1,-1);
__defparam(1);
//调用成员函数
RT retVal = (callee.*func)(p1);
__return(retVal);
return 1;
}
三、类的注册
要完整地将一个c++的类注册给脚本使用,我们必须为这个类所包含的四个部分:构造函数 ,析构函数,属性,方法提供主程序函数处理,luckyScript采用一套预定义的规范来命名这些主函数,并在合适地方调用它们,同样地,我们不可能为所有类都写单独的处理函数,所以,必须为这四个部分做一个封装,以使得能用一种统一的方式处理这个流程。
1.构造/析构函数的封装
首先你需要了解的是在luckyScript中创建一个主程序对象时,脚本会创建一块跟这个主程序对象大小一样的内存空间,然后再调用跟这个主程序对象同名的主程序函数,并把这块内存压栈传给用户,也就是说,构建类的操作是由用户完成的,用户所需要做的就是用这块内存空间创建用户所需要的对象,如果你对这些一点都不了解,我建议你可以先看看
这篇文章中关于注册对象的部分。ok,那么,我们先来看看统一注册给脚本的构造函数是什么样的:
1template<typename Callee>
2 struct Creator
3 {
4 template<typename CONSTRUCTOR>
5 static void invoke(RuntimeState* state)
6 {
7 void* dataTrunk = lucky_popValueAsUserData(state);
8
9 CONSTRUCTOR::invoke<Callee>(state,dataTrunk);
10 }
11 };
Callee是对象的类型,在这个函数中,我们取出脚本所为我们创建的那块内存空间,并把它传给CONSTRUCTOR::invoke接着处理,这个CONSTRUCTOR指定了构造函数的类型,但注意,它并不是构造函数指针的类型,实际上,类的构造函数是不允许你取址的,在上面的代码中,它转入的是下面这些代码中的invoke函数里面,仍然以拥有0到1个参数的构造函数处理为例:
1template<typename P1>
2 struct Constructor<P1>
3 {
4 template<typename Callee>
5 static void invoke(RuntimeState* state,void* dataTrunk)
6 {
7 __argassert(1,-1);
8
9 __defparam(1);
10
11 new(dataTrunk) Callee(p1);
12 }
13 };
14 template<>
15 struct Constructor<void>
16 {
17 template<typename Callee>
18 static void invoke(RuntimeState* state,void* dataTrunk)
19 {
20 new(dataTrunk) Callee();
21 }
22 };
23}
前面已经多次出现这个__argassert宏,我并不打算具体向你解释它的构成,但你明白它是用来assert栈上的参数类型是否匹配的就行了,到这里,我们都应该明白上面那个CONSTRUCTOR就是具体的struct Constructor,在这个结构体的invoke方法里,我们取出构造函数的参数,并调用构造函数,或许需要向你解释下这个比较少用的new(dataTrunk)..,它所实现的效果就是编译器在我们提供的那块内存中构建对象,这样构建出来的对象是不能直接delete的,我们需要显式调用对象的析构函数。
那么接下来看看类的析构函数的封装,所幸的是析构函数一定是没有参数的,省去了很多麻烦:
1template<typename Callee>
2 struct Destroytor
3{
4 static void invoke(RuntimeState* state)
5 {
void* dataTrunk = lucky_popValueAsUserData(state);
6 Desconstructor<Callee>::invoke(state,dataTrunk);
7 }
8 };
这个Desconstructor<Callee>::invoke调用的就是下面的代码:
1template<typename Callee>
2 struct Desconstructor
3 {
4 static void invoke(RuntimeState* state,void* dataTrunk)
5 {
6 unsigned char* calleeBuffer = (unsigned char*)dataTrunk;
7
8 Callee* classObj = (Callee*)(calleeBuffer);
9
10 //显式调用析构函数
11 classObj->~Callee();
12
13 }
14 };
搞定了这些,我们就可以具体设计我们提供给用户注册类的接口了,至少,它已经可以在脚本中创建与销毁了,对吧?所以不需要有太多顾虑,前面已经提到过,构造函数是不能取址的,所以不能以用户传进构造函数指针的方式来完成类的注册,我们需要提供一系列的模板重载函数,像下面这样:
//没有参数的构造函数
template<typename C>
void registerClass(const char* className)
{
pushClassAndConstructor<C>(className,LuckySystem::Constructor<>());
}
//有一个参数的构造函数
template<typename C,typename P1>
void registerClass(const char* className)
{
pushClassAndConstructor<C>(className,LuckySystem::Constructor<P1>());
}
根据Constructor<P1...>我们就具体指定了构造函数的类型,接下来看这个pushClassAndConstructor做了什么事情:
template<typename Callee,typename CONSTRUCTOR>
void pushClassAndConstructor(const char* className,CONSTRUCTOR)
{
lucky_clearAddedUserData();
LuckySystem::ObjectName<Callee>::name(className);
LuckySystem::ObjectName<Callee*>::name(className);
LuckySystem::ObjectName<Callee&>::name(className);
LuckySystem::ObjectName<const Callee&>::name(className);
LuckySystem::ObjectName<Callee const>::name(className);
LuckySystem::ObjectName<const Callee*>::name(className);
//注册与class同名的主程序构造函数
lucky_registerGlobalHostFunc(LuckySystem::Creator<Callee>::invoke<CONSTRUCTOR>,className);
//下划线+className,注册主程序析构函数
std::string destroyFuncName = "";
destroyFuncName += "_";
destroyFuncName = destroyFuncName + className;
lucky_registerGlobalHostFunc(LuckySystem::Destroytor<Callee>::invoke,destroyFuncName.c_str());
size_t size = sizeof(Callee);
lucky_registerHostClass(className,size);
}
CONSTRUCTOR指定了构造函数的类型,所以我们注册给脚本的构造函数处理主函数是:Creator<Callee>::invoke<CONSTRUCTOR>,而析构函数为Destroytor<Callee>::invoke,上面的Object<Callee>::name把这一class类型的名字保存了下来,如果没有为name方法指定参数,那么会返回callee这个类型的class的名字(假如之前有存的话),为了限制篇幅,我并不打算贴出它的代码
2.往注册的类添加成员方法
当我们在luckyScript中调用一个主程序对象的方法时,脚本会把这个主程序对象压栈传给用户,并按预定义的规则得到处理主函数名,然后再调用它交给用户处理,所以我们完全可以按照注册类成员函数的方法,事先把类成员函数指针当作用户数据压栈,在脚本调用主程序函数时,一并取出来调用:
1template<typename Callee,typename Func>
2 void addMemFunc(Func func,const char* funcName)
3 {
4 //增加函数指针大小的用户数据
5 unsigned char* funcBuffer = (unsigned char*)lucky_addUserData(sizeof(func));
6 //copy函数指针数据
7 memcpy(funcBuffer,&func,sizeof(Func));
8
9 //得到Callee类型的class的名字
10 const char* className = LuckySystem::ObjectName<Callee>::name();
11
12 //成员函数处理主函数name:类型名 + 下划线 + 成员函数名
13 std::string realFuncName = className;
14 realFuncName = realFuncName + "_" + funcName;
15 lucky_registerGlobalHostFunc(LuckySystem::MemberFunctor<Callee,Func>::invoke,realFuncName.c_str());
16
17 lucky_clearAddedUserData();
18
19 std::string typeName = LuckySystem::ReturnType::name(func);
20
21 lucky_addHostMemberFunc(className,funcName,typeName.c_str());
22 }
注意这个MemberFunctor<Callee,Func>::invoke就是注册类成员函数时使用的那个主函数封装,前面已对其做过介绍,在这个函数里面,我们取出类的实例,再取出类成员函数的指针,合并调用
对类属性变量的存取需要我们提供两个新的主函数封装,同样地,luckyScript在遇到主程序对象的存取时也会调用用户提供的主函数进行处理,并把这个主程序对象跟右操作数压栈传给用户,需要特别说明的是:由于赋值操作符有很多种,当遇到主程序成员变量的赋值时,luckyScript除了会把主程序对象和右操作数压栈传给用户外,还会把操作符也压栈传给用户以给用户足够的信息进行赋值,当然这是在这个成员变量不是对象类型的时候,假如这个成员变量本身也是在脚本中注册过的对象,那么会调用操作符重载主函数进行处理
1//成员变量为右操作数
2 template<typename Callee,typename ValueType>
3 struct MemberValueGettor
4 {
5 static void invoke(RuntimeState* state)
6 {
7 //取出成员变量指针数据
8 unsigned char* valBuffer = (unsigned char*)lucky_popValueAsUserData(state);
9 //取出类的实例
10 unsigned char* calleeBuffer = (unsigned char*)lucky_popValueAsUserData(state);
11
12 ValueType Callee::** val = (ValueType Callee::**)(valBuffer);
13 Callee* classObj = (Callee*)(calleeBuffer);
14
15 //返回成员变量值
16 __return(classObj->**(val));
17 }
18 };
19
20 //成员变量为左操作数
21 template<typename Callee,typename ValueType>
22 struct MemberValueSettor
23 {
24 typedef ValueType P1;
25
26 static void invoke(RuntimeState* state)
27 {
28 ValueType setVal;
29 //取出成员变量指针数据
30 unsigned char* valBuffer = (unsigned char*)lucky_popValueAsUserData(state);
31 //取出操作符
32 int opType = lucky_popValueAsInt(state);
33 if(opType != LUCKY_OP_TYPE_INC && opType != LUCKY_OP_TYPE_DEC)
34 {
35 __argassert(1,-1);
36
37 //取出右操作数的值
38 setVal = Param::get(TypeWrapper<ValueType>(),state);
39 }
40 //取出对象实例
41 unsigned char* calleeBuffer = (unsigned char*)lucky_popValueAsUserData(state);
42
43
44 //转换类型
45 ValueType Callee::** val = (ValueType Callee::**)(valBuffer);
46 Callee* classObj = (Callee*)(calleeBuffer);
47
48 //根据不同的操作符进行赋值处理
49 switch(opType)
50 {
51 case LUCKY_OP_TYPE_INC:
52 classObj->**(val) += 1;
53 break;
54 case LUCKY_OP_TYPE_DEC:
55 classObj->**(val) -= 1;
56 break;
57 //以下略过不贴
58}
在为主程序增加成员变量的接口上,我们同样要求用户提供此成员变量的指针以及在脚本中使用的名字
1template<typename Callee,typename ValueType>
2 void addMemVal(ValueType Callee::* val,const char* valName)
3 {
4 //增加成员变量指针大小的用户数据
5 unsigned char* valBuffer = (unsigned char*)lucky_addUserData(sizeof(val));
6 //copy成员变量指针数据
7 memcpy(valBuffer,&val,sizeof(val));
8
9 //得到Callee类型的class的名字
10 const char* className = LuckySystem::ObjectName<Callee>::name();
11
12 //成员变量处理主函数命名规则:类型名 + 下划线 + set/get + 下划线 + 成员函数名
13 std::string realFuncName = className;
14 realFuncName = realFuncName + "_" + "get_" + valName;
15 lucky_registerGlobalHostFunc(LuckySystem::MemberValueGettor<Callee,ValueType>::invoke,realFuncName.c_str());
16 realFuncName = className;
17 realFuncName = realFuncName + "_" + "set_" + valName;
18 lucky_registerGlobalHostFunc(LuckySystem::MemberValueSettor<Callee,ValueType>::invoke,realFuncName.c_str());
19
20 lucky_clearAddedUserData();
21
22 //得到此成员变量的类型名,假如这个成员变量也是已经注册过的主程序对象类型,那么不为空
23 std::string typeName = LuckySystem::ObjectName<ValueType>::name();
24
25 lucky_addHostMemberVal(className,valName,typeName.c_str());
26 }
在这个函数里,我们把成员变量指针当作用户数据传进脚本,并为此成员变量的存取注册两个主程序处理函数:MemberValueGetter<Callee,ValueType>::invoke,MemberValueSettor<Callee,ValueType>::invoke,当脚本遇到这个成员变量的存取操作时,会调用这两个主函数进行处理。
3.操作符重载
在遇到主程序对象的运算时,luckyScript会把操作符两边的value都压栈,并根据不同的操作符,调用不同的主函数进行处理,我们所需要做的,就是在这些操作符重载处理主函数里,取出这两个值,并进行运算操作:
template<typename Callee,typename ValueType>
struct OperatorOveridor
{
typedef ValueType P1;
static Callee* getClassObject(RuntimeState* state)
{
unsigned char* calleeBuffer = (unsigned char*)lucky_popValueAsUserData(state);
Callee* classObj = (Callee*)(calleeBuffer);
return classObj;
}
//自加操作符重载处理主函数
static void invokeInc(RuntimeState* state)
{
Callee* classObj = getClassObject(state);
//自加操作
(*classObj)++;
}
//赋值操作符重载处理主函数
static void invokeAssign(RuntimeState* state)
{
__argassert(1,-1);
//取出第二个操作数
__defparam(1);
Callee* classObj = getClassObject(state);
//进行赋值操作
(*classObj) = p1;
}
}
在接口方面,我们仅需要用户提供模板参数说明两个操作数的类型,以赋值操作符为例:
template<typename Callee,typename ValueType>
void overideAssignOp()
{
const char* className = LuckySystem::ObjectName<Callee>::name();
std::string realFuncName = className;
//操作符重载处理主函数命名规则:类型名 + 下划线 + "Overide" + 操作符英文符号
realFuncName = realFuncName + "_" + "Overide_" + "Assign";
lucky_registerGlobalHostFunc(LuckySystem::OperatorOveridor<Callee,ValueType>::invokeAssign,realFuncName.c_str());
}
注册的主函数是上面的OperatorOveridor<Callee,ValueType>::invokeAssign,callee和valueType为操作符左右两边操作数类型
写到这里,我已经很累了,就不再对封装调用脚本函数及访问全局变量的操作进行介绍了,之后源码发布后可自行观看,为了展示这个封装库的使用,我把OGRE的一些核心类跟接口注册给脚本,并重写了OGRE的两个例子,本把想脚本源码和截图跟这篇文章放到一起,但考虑到这篇文章篇幅已经很长,还是把它单独放到另一篇文章吧
posted on 2009-04-18 17:37
清風 阅读(1625)
评论(0) 编辑 收藏 引用 所属分类:
LuckyScript