您的位置:首页 > 编程语言 > Lua

利用luabind将Lua嵌入到C++项目中

2010-03-05 09:42 381 查看
         开篇——环境假设
                                                                                                                                         By : HengStar(欣恒)
原文地址:http://blog.csdn.net/gongxinheng/archive/2009/07/25/4380526.aspx
(虽然本人文笔不好,水平也不高,不过下定决心要写一篇文章和大家分享一些小小的成就也不容易~所以望各位同行人士想要转载的请注明出处:谢谢合作^_^顺便打个小广告,嘿嘿,有兴趣想一起交流的可以加QQ群: 31249966,觉得我文章有问题的也欢迎指出..QQ私聊吧-.- QQ:9292492 同时也感谢大虾linkerlin翻译的luabind文档给了英文水平不高的我很多帮助^^ 文档在这:http://blog.csdn.net/linkerlin/archive/2008/04/06/2254725.aspx)
开场白(PS:如果你觉得啰唆可以跳过它-.-#):本人是网游开发人员,初次接触Lua脚本,由于原项目中的脚本技术比较落后,开发人员使用起来诸多不便,所以想引入一套好的脚本机制,在了解到了Lua的强大以及在游戏行业的广泛运用后,我毫不犹豫的选择了它。在一段时间的Lua学习下来后发现要在一个大型项目并且是已经上市运营的项目中加入这套东西实属不容易,要考虑的东西太多,毕竟C++和Lua语言上的差异还是比较大的,要完全的实现C++和Lua之间的方便交互还是要费很大的功夫的,特别是在一些面向对象的特性诸如多态、继承等大范围在项目中使用的,而且给自己的时间并不多,好在从同事那了解到有luabind这号角色的存在,而且也是开源的,我马上兴起准备开始一起研究,由于旧代码中大部分都是无法移植的代码,在嵌入工作中遇到了一系列的麻烦-.-随后慢慢道来,不过经过几天的努力钻研,已经成功投入到项目中开始使用了,不过新版本暂时还未发布…废话先不说了,切入正题)
一.   将Luabind加入到工程
必备:Luabind源码、Lua SDK及源码、Boost 源码
VS中新建静态链接库工程,将Luabind源码路径下的src目录中所有文件添加到工程,编译为Luabind静态链接库(需要包含Luabind源码解压包后的根目录、Lua的Include文件目录、Boost 源码根目录,注意工程选项,因为当时我们的项目工程选项的结构对齐方式是采用“一字节对齐”,而编译Luabind静态链接库时用的是默认字节对齐方式,所以总发生莫名其妙的错误),将编译完的静态链接库加载到C++项目中;
二.   将Lua加入到工程
方法同上
三.   创建LuaManager类
需要包含的头文件:
创建一个专门用于管理与Lua脚本引擎相关的管理器类,可以采用Singleton(单件)       技术(详见《设计模式》)
class LuaManager
{
  lua_State* m_pL; // Lua状态,初始为NULL
  … 成员方法见下
};
大致需要实现如下方法(仅供参考,根据不同的工程需求可以用更适合自己工程的方式实现):
1. init(初始化):用于初始化Lua相关内容,代码示例:
  bool LuaManager::init()
  {
    if( !m_pL )  // 确保只会初始化一次
    {
      m_pL = luaL_newstate(); // 创建Lua状态
      if( !m_pL ) return false;
      luaL_openlibs( L ); // 为该Lua状态打开所有Lua库
      luabind::open( L ); // 为该Lua状态打开luabind库
    }
    return registerAll(); // 见下
  }
2. registerAll(注册所有需要的内容):
   学过Lua的朋友一定知道C/C++中的函数等要在Lua中调用需要注册,如果直接用Lua提供的方法注册会很麻烦,随着工程的规模的扩大还会提升管理难度,这就是我选择使用luabind的原因,它提供十分方便的机制封装了C/C++类的绑定,已经函数的注册,甚至C/C++中声明的全局变量也能直接注册到Lua中使用,(由于我们的项目代码量比较庞大,为了方便维护,我将此方法分为了两个,一个专门用来注册C++类的registerClasses和用于注册全局函数的registerFunctions,还会考虑加入一个注册全局变量的registerVars),代码示例:
  bool LuaManager::registerAll()
  {
  // 以下是全局函数的注册
   module( L )
       [
          def( "myfunc", &myfunc ),
          def( "specialfunc", &Specialfunc )
       ]
  // 以下是类的注册
   module( L )
       [
           class_("testclass") // 基类
           .def(constructor()) // 构造函数
           .def("print_string", &testclass::print_string) // 虚成员函数
           ,
          // 子类,注意基类也要在模板参数中注明
          class_("myclass")
          .def(constructor()) // 构造函数
          .def("print_string", &myclass::print_string) // 成员函数
        ]
     return true; // 目前还没研究注册失败的情况,暂时先假设始终会成功吧
   }
   更多细节请参见luabind文档
http://blog.csdn.net/linkerlin/archive/2008/04/06/2254725.aspx
3. loadScript(加载脚本):用于加载写好的Lua脚本文件(建议用编译后的二进制Luac脚本文件,效率比较高),代码示例:
  bool LuaManager:: loadScript ( const string& fname )
  {
    if( luaL_loadfile(L, fname.c_str()) ) // 如果需要马上执行Lua脚本可以用luaL_dofile宏
    {
      cerr << lua_tostring( L, -1 ) << endl; // 输出错误信息
      return false;
    }
    return true;
  }
四.   初始化LuaManager
如此简单的工作就不多说了…直接调用init并且用loadScript加载相应的脚本文件即可
小结:
本篇的内容比较简单,快速概括了luabind嵌入到C++工程的环境假设问题,很多信息能在文档里面找到,我这里也就不想再多说,如果有不明白的可以找我,一定竭力为大家解答^^
这里可以展现Lua的一个强大之处也就是支持动态加载脚本的功能,我目前的设想是准备把加载Lua脚本的接口提供给一个游戏中的GM指令,以后有脚本的更新发布,只需要简单的用GM指令重新加载这些脚本即可以完成服务器的脚本更新,而不需要重启服务器甚至于再编译一次服务器(目前项目中旧的脚本逻辑一直是和服务器代码相关联的,所以每次改完脚本都需要编译服务器,非常烦人…)
关于下一篇,我会提到一些自己在项目中遇到的C++语言特性造成和Lua的一些交互困难以及自己的解决方案和根据luabind源码了解到的一些简单的交互机制,敬请期待^^
本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/gongxinheng/archive/2009/07/25/4380526.aspx
 
第二篇——语言差异及解决方案(初级篇)
By : HengStar(欣恒)
原文链接:http://blog.csdn.net/gongxinheng/archive/2009/07/26/4381217.aspx
通过上一篇的介绍,我们已经了解到了luabind的基本架设方法了,本篇将会用我自己在实践中的经验,通过举例来说明一些实际开发中可能会遇到的语言差异上带来的一些问题及个人尝试过的解决方案。
一.   默认参数 VS 参数个数严格匹配
众所周知,C++中的默认参数能给我们带来很大程度上的便利,观察以下代码:
view plaincopy to clipboardprint?
#define FOOD_DOGFOOD 0  
#define FOOD_RICE 1  
bool feedDog( const char* dogName, int foodType = FOOD_DOGFOOD ); 
#define FOOD_DOGFOOD 0
#define FOOD_RICE 1
bool feedDog( const char* dogName, int foodType = FOOD_DOGFOOD );
PS:这个函数声明只是突发地想到的,没什么特别的含义(因为我自己也养了条可爱的小狗^^),假设我们养了很多狗,也可能是开了一个宠物培训基地^^我们给狗喂食的时候通过dogName来确定给哪只狗喂,喂的食物类型foodType有默认参数FOOD_DOGFOOD(因为我通常只给它吃狗粮),然后我要调用此函数的时候经常会这样写feedDog( “豆豆” );使用缺省食物类型参数,由于习惯问题,我通过luabind的def("feedDog", &feedDog)将该函数注册到Lua中使用仍然用feedDog( “豆豆” )调用,结果在执行Lua脚本时候出现错误:”No matching overload found, candidates : bool feedDog(char const*, int)” 也就是该调用没有找到匹配的重载函数,但是有候选的两个参数的feedDog函数,好了,到这里大家应该看出来是怎么回事了吧,暂且不说在C++中定义的宏FOOD_DOGFOOD不能在Lua中使用,即使你用数字0也无济于事,因为luabind中做了严格的参数个数匹配判定,注册的函数有几个参数(包括默认参数),在lua脚本中调用时就必须写几个参数。
解决方案:必须在Lua中调用时写上所有参数(包括默认参数)
view plaincopy to clipboardprint?
feedDog( “豆豆”,0 ) 
feedDog( “豆豆”,0 )
当然你也可以在Lua中自己定义伪枚举(我自己命名的^^)
view plaincopy to clipboardprint?
FOOD_DOGFOOD = 0 
feedDog( “豆豆”, FOOD_DOGFOOD ) 
FOOD_DOGFOOD = 0
feedDog( “豆豆”, FOOD_DOGFOOD )
谨记:别指望C++语言带来的所有便利都能同样在Lua中使用。
二.   字符串指针 VS nil类型
好了,接下来我们要开始训练狗狗们了,首先我需要叫来我要训练的狗狗,我们有以下函数,通过名字来呼唤狗狗
view plaincopy to clipboardprint?
bool callDog( const char* dogName )  
{  
   if( dogName == NULL ) return callAll();  
   else  return call(dogName);  

bool callDog( const char* dogName )
{
   if( dogName == NULL ) return callAll();
   else  return call(dogName);
}
也许我还希望可以一次性叫来所有的狗,所以我将该函数实现为传递NULL作为参数的时候是表示叫来所有的狗,大家知道,在Lua中是用nil指代空值,但是Lua中的nil和C++中NULL还是有一些差异的,C++中的NULL通常被定义为数字0,而在Lua中nil是一个独立的类型,nil ~= 0,但是我们还是怀着侥幸的心理期盼它能转换为C++中的NULL值,所以我们尝试这样在Lua中调用:
view plaincopy to clipboardprint?
callDog( nil ) 
callDog( nil )
我们失望的从输出结果中看到了” No matching overload found, candidates : bool callDog(char const*)”
Oh!shit!(这是我当时看到这个结果的第一反应-.-#),看来结果并不是我们预期那般完美,Lua中将nil试为一种类型,在luabind中会做严格的参数类型判定,nil不是一个string,而且Lua中并不存在指针类型,所以用指针的方式去操作变量必定导致了失败的结果(但并不是所有的情况都是这样,字符指针是一个特殊的类型,稍后会提到一些允许的转换情况)。是不是有人会这样想:“好吧,既然你说在C++中NULL通常都定义为0,那我这样调用总可以了吧”。
view plaincopy to clipboardprint?
callDog( 0 ) 
callDog( 0 )
恩…看来我们想到一块儿了,但执行后依然从错误输出中看到了同样的文字…在此强调,luabind会更多的把字符串用string的特征看待(如std::string)。
解决方案:在使用有const char*参数的注册函数中判断该参数是否为NULL的同时还要判断是否是空串,即””,并且在Lua中调用该函数时,为NULL的参数改用空串””代替,改造后的C++代码如下所示:
view plaincopy to clipboardprint?
bool callDog( const char* dogName )  
{  
   // dogName[0] == ‘/0’可以用 strcmp( dogName, “” ) == 0代替  
   if( dogName == NULL || dogName[0] == ‘/0’ ) return callAll();  
   else return call(dogName);  

bool callDog( const char* dogName )
{
   // dogName[0] == ‘/0’可以用 strcmp( dogName, “” ) == 0代替
   if( dogName == NULL || dogName[0] == ‘/0’ ) return callAll();
   else return call(dogName);
}
在LUA中的调用:
view plaincopy to clipboardprint?
callDog( "" ) 
callDog( "" )
谨记:不要在Lua中传递nil或0给字符串参数,在给Lua注册使用的函数接口中字符串参数尽可能的用std::string代替,如果非要用const char*,记得把空串和NULL判断平级
三.   C++对象指针VS nil类型
前面提到:字符指针是一个特殊的类型,可以从和C++对象指针的差异上证明这一点。
狗狗的主人要来接训练完成的狗狗回家咯~每个狗狗脖子上都挂着主人的标志牌,姑且把它当做是一个主人的指针吧,我们有如下代表主人的类型,已经注册到Lua中
view plaincopy to clipboardprint?
class Person  
{   
public:  
    Person(const string& n) : name(n){}  
    string name;   
}; 
class Person
{
public:
    Person(const string& n) : name(n){}
    string name;
};
在Lua脚本中有如下表示狗狗和主人关系的table
view plaincopy to clipboardprint?
HengStar= Person("HengStar") --某主人  
DaQiang= Person("大强")  --某主人  
dogs={ "贝贝", "豆豆", "强强" } --狗狗序号表  
dog_Master = { nil, HengStar, DaQiang } --狗狗序号对应的主人 
HengStar= Person("HengStar") --某主人
DaQiang= Person("大强")  --某主人
dogs={ "贝贝", "豆豆", "强强" } --狗狗序号表
dog_Master = { nil, HengStar, DaQiang } --狗狗序号对应的主人
管理员要帮狗狗的主人找到他的狗狗,所以我们又注册了一个用于判定狗狗-主人匹配的函数
view plaincopy to clipboardprint?
bool checkMaster( Person* realMaster, Person* dogMaster )  
{  
  assert( realMaster != NULL ); // 这是不允许的  
  if( dogMaster == NULL ) // 注意这里  
  {  
    cout << “某狗狗没有主人!” << endl;  
    return false;  
  }  
  return realMaster->name == dogMaster->name; // 假定姓名没有重复  

bool checkMaster( Person* realMaster, Person* dogMaster )
{
  assert( realMaster != NULL ); // 这是不允许的
  if( dogMaster == NULL ) // 注意这里
  {
    cout << “某狗狗没有主人!” << endl;
    return false;
  }
  return realMaster->name == dogMaster->name; // 假定姓名没有重复
}
好了,准备工作完毕了,下面我们开始狗狗大搜查吧,在Lua脚本中执行以下代码
view plaincopy to clipboardprint?
for i = 1, #dog_Master do  
   if( checkMaster(HengStar,dog_Master[i]) ) then 
      print(dogs[i] .. "找到主人了!")  
      break 
   end 
end 
for i = 1, #dog_Master do
   if( checkMaster(HengStar,dog_Master[i]) ) then
      print(dogs[i] .. "找到主人了!")
      break
   end
end
输出结果:
某狗狗没有主人
豆豆找到主人了
至此,示例完毕,细心的读者一定观察到了我想说明的结论了~没错,那就是有C++注册类对象的指针作为参数的注册函数,在Lua中调用时允许从nil转换到C++中的NULL,这正是和字符指针不同的地方,也是luabind的强大之处。
以上示例代码中for i = 1, #dog_Master会遍历dog_Master中所有元素,并把对应的值传给checkManager函数的第二个参数,而dog_Master第一个元素的值就是nil,在luabind中转化为NULL并传递给checkMaster,所以输出了某狗狗没有主人。
PS:此代码示例的展示方式可能有点牵强,不过能说明我要讲述的问题也就达到目的了。
谨记:在luabind中会判定Lua中传进来的参数类型和C++注册函数的参数类型匹配,如果参数是C++注册类对象指针,则会将nil转换为NULL使用
本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/gongxinheng/archive/2009/07/26/4381217.aspx
 
第三篇——语言差异及解决方案(进阶篇)
By : HengStar(欣恒)
通过前面两篇我们对luabind做了进一步的了解,接下来让我们提升点难度吧:)下面讲的例子可能有点违背常理,但我确实在项目中遇到了,所以在此提出一下,也能更好的了解luabind的机制。
一.   欺骗编译器?——狗狗们的特技
好了,女士们先生们,激动人心的时刻到来了,现在是狗狗的开心演出时间,可爱的狗狗们要表演各自的绝活了~让我们拭目以待吧…
先看看下面的代码:
#include
using namespace std;
class Dog // 狗狗基类
{};
class Chihuahua : public Dog // 吉娃娃
{
public:
    bool wallow() // 打滚
    {
        cout << "I'm wallowing!" << endl;
        return true;
    }
};
class Gundogs: public Dog //枪猎犬
{
public:
    bool hunt() // 打猎
    {
        cout << "I'm hunting!" << endl;
        return true;
    }
};
class Poodle: public Dog // 贵妇
{
public:
    bool highjump() // 高跳
    {
        cout << "I'm jumping!" << endl;
        return true;
    }
};
typedef bool(Dog::*dogact)(); // 狗狗特技的成员函数指针
bool Dogshow_impl( Dog* dog, dogact act )
{
    return (dog->*act)();
}
int main()
{
    Dog* myDog = new Poodle;
    Dogshow_impl ( myDog, &Poodle:: highjump ); // 贵妇狗狗开始表演吧
    return 0;
}
好了,以上是全部代码,大家先猜猜输出结果吧…
很不幸的,驯兽师(编译器)开始抱怨了,“我希望看到的是狗(Dog)的表演,而不是贵妇狗(Poodle)的表演”,可能这个驯兽师是个傻子,但确实设计是这样的,他只是负责执行的一方而已,Dogshow_impl函数需要接受的第二个参数是一个Dog基类的成员函数指针,而你传递一个子类Poodle的成员函数指针,这是不合法的(如果不能理解,请参阅相关C++书籍,如《C++ Primer》),因为基类根本无法了解子类里面会出现哪些类似的(函数原型相同的)成员函数,好吧,既然如此,我想个办法蒙混过关吧~我们来给表演者盖上一层面纱,让它伪装(强制转换)成所谓的狗(基类)
#define dogshow(dogpt, act) Dogshow_impl( dogpt, (dogact)act )
然后我把
Dogshow_impl ( myDog, &Poodle:: highjump ); // 贵妇狗狗开始表演吧
改成
dogshow ( myDog, &Poodle:: highjump ); // 贵妇狗狗开始表演吧
“很好”,驯兽师赞道,并让myDog执行了它的highjump方法,所以输出了:
I'm jumping!
OK,我们的目的达到了(不过我不得不承认这种设计模式是非常失败的…),我现在可以任意的传递任何狗狗的指针和它的任意签名对应的成员函数作为参数了,但这始终是一种欺骗行为,驯兽师的眼睛逃过了,但它不一定逃得过评委(luabind)的审核。
首先,宏定义是不被lua所支持的,所以我们不能用dogshow来注册了,眼下也只能通过直接注册Dogshow_impl函数在lua中使用了,让我们注册所有类和方法试试吧。
我们怀着侥幸的心理在lua中这样调用
myDog = Poodle()
Dogshow_impl( myDog, myDog.highjump )
很遗憾,执行到上面的代码时失败了,原因如下:
首先,luabind中有严格的类型判断,这个跟最初的C++代码产生编译错误的原理一样,就不再重复了,但本质问题不在此,myDog.highjump在lua中是一个function类型,并非需要的成员函数指针类型,所以在参数类型判断上就失败了,怎么办?总不能因为这个原因就不用了吧,开动脑筋想想办法吧…
解决方案:
提供一种我自己的解决方案:
首先如果你非得照这种模式设计的话,我只能在基类中把所有子类需要用到lua中的成员函数声明为虚函数,如此一来,Dog类中就多了以下方法
class Dog // 狗狗基类
{
public:
   virtual bool hunt();
   virtual bool highjump ();
   …….
};
显然这样并不是很好的方法,建议在基类中把这些方法抽象出来,比如改名叫perform,如此一来基类只需要一个 virtual bool perform()的声明(当然这个基类的虚函数也得注册),子类可以自己实现它,最重要的是要能在lua中正确的调用,那么我们还需要把Dogshow_impl函数封装一层,第二个成员函数指针的参数可以用const luabind::object&取代,luabind::object可以接收一个lua中的function类型,并且可以用call_function直接调用它并用参数一(基类狗狗指针)作为call_function的参数(具体参照luabind文档),接下来我们还需要在lua中声明一个全局基类对象,专门用于“选择”调用的虚函数,看看改造后的代码吧~
首先在C++中封装一层
bool Lua_DogShow( Dog* dog, const luabind::object& func )
{
        using namespace luabind;
        call_function( func, dog ); // 这个func是lua函数,dog是调用者
        return true;
}
将此函数注册到Lua中
然后在lua中写以下代码
DOG = Dog() // 全局的,用来“选择”调用的虚函数用的
function myFunc()
      local myDog = Poodle()
      --注意这里的DOG.highjump相当于成员函数指针,这个highjump是通过基类注册的
Lua_DogShow( myDog, DOG.highjump )
End
在C++中调用Lua中的myFunc函数就会输出:
    I'm jumping!
本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/gongxinheng/archive/2009/08/04/4409530.aspx
 
-----------------------------------------
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: