(转载自个人新博客:http://www.) A template class for binding C++ to Lua英文原文:A template class for binding C++ to Lua 最近在研究C/C++和Lua的交互问题,顺便看了下luna,自己尝试着翻译了此文,以供分享,初始翻译,权当练习。 摘要此文介绍了一种将C++类绑定到Lua的方法。Lua没有直接提供此方法,而是通过底层的C接口和扩展机制来实现。我所描述的方法使用了Lua的C 接口、C++模板和Lua的扩展机制,构建了一个小巧、简单且高效的提供类注册服务的静态模板类。这个方法对你的类只有一个小小的要求,即只有签名为 int(T::*)(lua_State*) 的成员函数能被注册。但是,正如我将要展示的,这个限制也能被克服。The end result is a clean interface to register classes, and familiar Lua table semantics of classes in Lua。此处描述的解决方案依赖于一个我命名为Luna的模板类。 问题Lua的接口的设计,不能注册C++类到Lua中,只提供了注册签名为 int()(lua_State*) 的C函数。实际上,这是Lua支持注册的唯一C数据类型。为了注册其它类型,你需要使用Lua提供的扩展机制,如tag methods、closures等。为了创建注册C++类到Lua的方案,必须使用这些扩展机制。 方案此方案主要有4个元素:类注册、对象实例化、成员函数调用和垃圾回收。 类注册是通过以类的名字注册一个表构造函数(a table constructor function with the name of the class)。表构造函数是一个在lua_State中注册一个表的静态模板函数。 注释:静态类成员函数是和C函数相兼容的,如果它们的签名相同,则我们可以在Lua中注册它们。下面的代码片段是一个模板类的成员函数,T 是待注册的类。
对象实例化是通过passing any arguments the user passed to the table constructor function to the constructor of the C++ object,创建一个代表对象的表,注册一些类的成员函数到表上,最后将表返回给Lua。对象的指针做为userdata保存在表中,其对应的索引为 0。成员函数的序号做为闭包的值保存在所有函数中。More on the member function map later。
不像C函数,C++成员函数需要类的对象来调用。成员函数的调用是通过thunks函数,它由一个对象指针和成员函数指针来做实际的调用。成员函数 指针是保存在函数的闭包值里,对应成员函数的哈希表中,对象的指针是表的序号0对应的值。注意,在Lua中所有的类函数都是通过下面的函数来注册。
垃圾回收是通过在设置表中userdata的垃圾回收标志方法。当垃圾回收被触发时,'gc'标志方法就会被调用以删除对象。'gc'标志方法是在类注册时通过一个新的标志注册的。在上面的实例化时,userdata就通过一个标签来标志。
注意,有一些规则类需要遵守: 注释:这些要求是我所选择的设计的要求,你可以使用不同的接口,只需要代码做很小的改动。 Luna::RegType 是一个函数哈希表。name 是被注册的 mfunc 函数的名字。
这里有一个注册C++类到Lua中的例子。Luna::Register() 调用会注册这个类,它也是这个模板类唯一要求的公开接口。要在Lua中使用此类,你可通过调用表的构造函数来创建此类的一个实例。
Account实例的表如下:
说明也许有些人不喜欢使用C++模板,但在此处却是合适的。它们提供了一个起初看起来复杂但快速严密的解决方案。作为使用模板的回报,类是类型安全的, 例如,它不可能在成员函数哈希表中混合不同类型的成员函数,因为编译器会报怨的。此外,静态模板类的设计,使它容易使用,在你做完事后没有模板实例需要清 除。 thunk机制是类的核心,因为它转换了函数调用。它通过将表调用的函数对应的对象指针索引到成员函数哈希表中。(Lua表的函数调用 table:function() 是 table.function(table) 调用的语法糖)。这个调用会将表最先压入栈中,接着是参数。成员函数的索引做为一个闭包值压入栈的最后。最开始,我将对象指针也做为一个闭包值,这意味着 有2个闭包值,一个void *的对象指针,一个成员函数的索引,这样花费更多,但访问更快。当然,表中用于垃圾回收的userdata对象还是需要的。最后,我选择了将对象指针索引 到表中,以节省资源,但增加调用的时间消耗。 实际上,这个实现只利用了Lua扩展机制很少的特性,闭包来保持成员函数的索引,'gc'标志成员用于垃圾回收,表的构造函数做函数注册和成员函数的调用。 为什么只允许签名为 int(T::*)(lua_State*) 成员函数被注册?这允许你的成员函数和Lua直接交互,接收Lua传入的参数并返回值到Lua,调用任何的Lua Api函数等。此外,它提供了与注册到Lua中的C函数相同的签名,这使得想使用C++的人更方便。 不足这个模板类方案只能绑定特定签名的成员函数。如果你有之前写的类,或想在Lua和C++的环境中都能使用,这个方案对你来说可能不是最好的。理论上 这不是一个问题。使用代理模式,我们封装实际的类,并且代理任何对目标对象的调用。代理类的成员函数强迫Lua的参数和返回值,并且代理对目标对象的调 用。你将在Lua中注册代理类,而不是实际类。Additionally, you may use inheritance as well where the proxy class inherits from the base class and delegates the function calls up to the base class, but with one caveat, the base class must have a default constructor; you cannot get the constructor arguments from Lua to the base class in the proxy's constructor initializer list。代理模式解决了我们想在Lua和C++环境中使用的问题,但它要求我们写代理类并且维护它们。 对象都是简单的创建,但需要给用户创建对象时更多的控制。比如,用户也许希望注册一个单例类。一个单例需要用户实现一个返回一个对象的静态 create() 成员函数。这种方式,用户可以实现单例模式,简单的通过new分配对象或者其它方法。constructor函数需要做修改来调用create()而不是 使用new来获取一个对象的指针。这对类要求了更多的约束但更灵活。 A "hook" for garbage collection may be of use to some as well. 总结这个文章说明了一个绑定C++类到Lua的简单方法。这个实现如此简单,你可以选择修改它来满足你的目的,也满足了一般的需求。也有一些其它的工具 来绑定C++类到Lua中,如 tolua、SWIGLua和一些其它像本文的简单实现。根据它们的优点、缺点来解决你特定的问题。希望本文能给你一些解决此类问题的提示。 模板类的所有源码大概在70行左右,可以Lua的add-ons面面获取。 参考[1] R. Hickey, Callbacks in C++ using template functors, C++ Report February 95 Last update: Wed Mar 12 11:51:13 EST 2003 by lhf. 欢迎转载,原文地址:http://www./a-template-class-for-binding-cpp-to-lua-translate
posted @ 2014-01-06 23:52 lontoken 阅读(15) 评论(0) 编辑
lua的栈 lua的栈lua中有两种栈:数据栈和调用栈. 栈的定义和结构lua中的数据可以分为两类:值类型和引用类型,值类型可以被任意复制,而引用类型共享一份数据,复制时只是复制其引用,并由GC负责维护其生命期.lua使用一个unine Value来保存数据.
引用类型用一个指针GCObject *gc来间接引用,而其它值类型都直接保存在联合中. 在lua_State中栈的保存如下:
StkId的定义:
在Windows VC下:
这里使用了繁杂的宏定义,TValuefields和numfield是为了应用一个被称为NaN Trick的技巧. lua初始化堆栈:
初始化之后,lua_State栈的情况: lua供C使用的栈相关API是不检查数据栈越界的,因为通常编写C扩展都能把数据栈空间的使用控制在BASIC_STACK_SIZE以内,或是显式扩展.对每次数据栈访问都强制做越界检查是非常低效的.
数据栈扩展的过程,伴随着数据拷贝,这些数据都是可能直接值复制的,所有不需要在扩展之后修正其中的指针.但有此外部对数据栈的引用需要修正为正确的新地址.这些需要修正的位置包括upvalue以及执行对数据栈的引用.此过程由correctstack函数实现.
栈的使用入栈的函数或宏:
lua_pushnumber定义如下:
lua_newtable的定义如下:
示例代码如下:
lua_pushinteger(L, 1)之后,栈的情况: lua_newtable(L)之后,栈的情况: 出栈的函数或宏:
luaL_checknumber的定义如下:
示例代码如下(lua库的luaopen_base函数,用于注册):
luaopen_base函数设置全局注册表的"_G"字段,base_funcs指定的函数列表,"_VERSION"字段. 第二个lua_pushglobaltable(L)之后: lua_setfield(L, -2, "_G")之后: luaL_setfuncs(L, base_funcs, 0)之后: lua_pushliteral(L, LUA_VERSION)之后: lua_setfield(L, -2, "_VERSION") 此时栈顶下的第一个元素为 参考资源PS:个人新干博客地址 http://www./ posted @ 2013-12-24 12:53 lontoken 阅读(12) 评论(0) 编辑
文本文件的编码识别文本文件的编码问题,困扰我很久,在跨平台、源程序中的中文字符、从文本文件中读取中文字符的时候,若对文件编码问题没有弄清楚,难免会走弯路。对此情况,我准备针对几个主题,记录下自己学习的心得,以备日后查阅和分享。 认识文本文件文件分为两种类型:文本文件和二进制文件; 字符编码一般常见的编码格式有:ASCII、UTF-8、UTF-16、GB2312、Big5、GBK、GB18030。 详细的字符编码知识,此处不再多说。 需要知道的事:
文件编码的模式识别知道了字符编码的细节,还不足以正确处理我们所面对的种类繁多的文本文件。 BOM:要识别UTF-8和UTF-16就不得不说到字节顺序标记(byte-order mark,BOM),它用来标识该字节流的字节序,是高位在前还是低位在前。从Unicode3.2开始,BOM只能位于流的开头,只能用于标识字节序。
UTF-8: 若为UTF-8有BOM格式,则文件开头为 EF BB BF; 若为UTF-8无BOM格式,则不能依据上述规则;此时需要依据UTF-8编码格式来判断,其编码如下:
UTF-16: 若为UTF-16(大端序),则文件开头为 FE FF; 若为UTF-16(小端序),则文件开头为 FF FE; GB2312: GB2312中对所收汉字进行了"分区"处理,每区含有94个汉字/符号。这种表示方式也称为区位码。
在使用GB2312的程序通常采用EUC储存方法,以便兼容于ASCII. Big5: Big5码是一套双字节字符集,使用了双八码存储方法,以两个字节来安放一个字。第一个字节称为"高位字节",第二个字节称为"低位字节"。 "高位字节"使用了0x80-0xFE,"低位字节"使用了0x40-0x7E,及0xA1-0xFE。 因Big5相对使用较少,此处不做识别。 GBK: 字符有一字节和双字节编码,00–7F范围内是一位,和ASCII保持一致,此范围内严格上说有96个文字和32个控制符号。 GB18030: 标准采用单字节、双字节和四字节三种方式对字符编码。 面向字节的模式识别UTF-16 直接根据其BOM识别;
PS:个人新干博客地址 http://www./ posted @ 2013-12-12 23:14 lontoken 阅读(9) 评论(0) 编辑
编译器产生的输出文件的MD5值与生成代码的关系背景因发布给用户的产品需要升级,每次升级使用的是版本号加”增量更新“的方法,自动更新服务器保存当前版本号与所有文件的MD5值,用户本地保存本地的版本号。登录时,若用户本地的版本号与服务器上的版本号不一致,则根据嗠器上文件的MD5与本地所有文件计算的MD5值比较,若不同,则更新。但现在在Delphi6中,程序的代码没有作更新,前后再次产生的文件的MD5却不同。Delphi6将生成文件的当前时间戳添加到了输出文件中。从二进制进的角度来看,代码相同的程序,只是编译的时间不一样,产生的文件却是不同的。Delphi6的这种画蛇添足的做法,实在叫人费解。 既然Delphi6这么做,难道这是“业内行规”,为了弄明白,那就只有一一探个明白了。 Delphi6输出文件MD5已经知道了,代码不一样编译时间不同,其输出文件的MD5也不同,但若是在输出文件中,将编译时间戳的影响去除了,是其它因素对它的影响是怎样的呢。 程序如下:
在不修改代码的情况下,编译两次产生的文件:BuildOutput61.exe、BuildOutput62.exe,另外将时间戳强制改为2008-08-08 08:08:08之后分别产生的文件为:BuildOutput610.exe和BuildOutput620.exe; 将代码做略微调整:
此次产生的BuildOutput630.exe的MD5值: 将代码的语句顺序调整: program BuildOutput_6;
此次产生的BuildOutput640.exe的MD5值: 将代码的空白行去除:
此次产生的BuildOutput650.exe的MD5值: JAVA输出文件MD5JAVA代码如下: public class BuildOutput_6 {
两次编译产生的.class文件的MD5如下: BuildOutput61.class 5F50C342B17C8DBFE22E3E71311CB358 VC9.0输出文件MD5代码如下: public class BuildOutput_6 {
两次编译产生的.exe文件的MD5如下: BuildOutput1.exe EB77754D7216FD4AC3FF62EB7A0E3A2B 似乎事情有些线索了,输出为可执行文件时,相同代码两次产生的MD5值会不同。但学需要进一步验证。下次有时间之后继续。 PS:个人新干博客地址 http://www./ posted @ 2013-04-02 18:31 lontoken 阅读(55) 评论(0) 编辑
案例情形:在通过控件的构造函数Create(AOwner: TComponent)创建对象a时传入Application,之后又自作多情的主动调用FreeAndNil释放此对象a,在程序退出时问题就会来了,由于Application会主动释放自己的Components内的元素,而我们自己再次调用FreeAndNil时就会出现对象的多次释放,导致程序无法正常退出!!! 反例代码: //在Create时创建对象 FFoolPan := TPanel.Create(Application); //在Destroy时释放资源 //旁白:不要以为做了Assigned判断就万事大吉了,遇到”悬空指定”你会死得很难看 if Assigned(FFoolPan ) then FreeAndNil(FFoolPan );
好了,现在开始分析问题的原因,为了刨根问底,我们只有深入Delphi的VCL去探险了… //------在Create时将自己插入到组件列表Components当中 Start------// constructor TComponent.Create(AOwner: TComponent); begin FComponentStyle := [csInheritable]; if AOwner <> nil then AOwner.InsertComponent(Self); //将自己插入到AOwner的组件列表中 end; //我们来看看InsertComponent到底做了什么 (PS:由贴出了与此问题相关的代码,下同) procedure TComponent.InsertComponent(AComponent: TComponent); begin AComponent.ValidateContainer(Self); ValidateRename(AComponent, '', AComponent.FName); Insert(AComponent); //Insert就发生在此时 end; procedure TComponent.Insert(AComponent: TComponent); begin if FComponents = nil then FComponents := TList.Create; FComponents.Add(AComponent); AComponent.FOwner := Self; end; //------在Create时将自己插入到组件列表Components当中 Edn------//
//------TComponent释放组件列表Components中的实例 Start------// destructor TComponent.Destroy; begin Destroying; DestroyComponents; //释放Components if FOwner <> nil then FOwner.RemoveComponent(Self); inherited Destroy; end; procedure TComponent.DestroyComponents; var Instance: TComponent; begin while FComponents <> nil do begin Instance := FComponents.Last; if (csFreeNotification in Instance.FComponentState) or (FComponentState * [csDesigning, csInline] = [csDesigning, csInline]) then RemoveComponent(Instance) else Remove(Instance); Instance.Destroy; //只调用了Destroy,却没置为nil,引入悬空指针,情何以堪... end; end; //------TComponent释放组件列表Components中的实例 End------// 现在我们明白了Create(Application)和Create(nil)的一个重要的区别了:使用Create(Application)所创建的对象的释放由Application来做,Create(nil)构造的对象需要自己来做资源的释放。 那程序退出时,Delphi都做了些什么呢? 我们从简单的情况入手,看看在系统的主窗体关闭时,我们的程序都执行了些什么操作。我们来看TCustomForm的WMClose,它的声明如下: procedure WMClose(var Message: TWMClose); message WM_CLOSE; procedure TCustomForm.WMClose(var Message: TWMClose); begin Close; //很简单,只是调用了Close而已 end; 真像会在Close里面吗? procedure TCustomForm.Close; var CloseAction: TCloseAction; begin if fsModal in FFormState then ModalResult := mrCancel else if CloseQuery then begin if FormStyle = fsMDIChild then if biMinimize in BorderIcons then CloseAction := caMinimize else CloseAction := caNone else CloseAction := caHide; DoClose(CloseAction); if CloseAction <> caNone then if Application.MainForm = Self then Application.Terminate //我们找到Application.Terminate了,不错 else if CloseAction = caHide then Hide else if CloseAction = caMinimize then WindowState := wsMinimized else Release; end; end; Application.Terminate的作用时让程序终止执行,即退出应用程序。更详细的说明是Terminate会通过调用PostQuitMessage(0)发送消息WM_QUIT面终止程序。 function TApplication.ProcessMessage(var Msg: TMsg): Boolean; var Handled: Boolean; begin Result := False; if PeekMessage(Msg, 0, 0, 0, PM_REMOVE) then //在消息队列中获取消息 begin Result := True; if Msg.Message <> WM_QUIT then begin Handled := False; if Assigned(FOnMessage) then FOnMessage(Msg, Handled); if not IsHintMsg(Msg) and not Handled and not IsMDIMsg(Msg) and not IsKeyMsg(Msg) and not IsDlgMsg(Msg) then begin TranslateMessage(Msg); DispatchMessage(Msg); end; end else //如果接收到WM_QUIT消息,将退出标志置为true FTerminate := True; end; end; 在中我们可以在TApplication.Run看到,系统会通过HandleMessage调用ProcessMessage处理消息,直到退出标志为true时,才终止。
procedure TApplication.Run; begin FRunning := True; try AddExitProc(DoneApplication); //将DoneApplication添加到TApplication.Run退出之后执行列表中 if FMainForm <> nil then begin case CmdShow of SW_SHOWMINNOACTIVE: FMainForm.FWindowState := wsMinimized; SW_SHOWMAXIMIZED: MainForm.WindowState := wsMaximized; end; if FShowMainForm then if FMainForm.FWindowState = wsMinimized then Minimize else FMainForm.Visible := True; repeat try HandleMessage; except HandleException(Self); end; until Terminated; //退出标志为true时退出 end; finally FRunning := False; end; end;
Application.Ran退出后,我们看看DoneApplication会做些什么。 procedure DoneApplication; begin with Application do begin if Handle <> 0 then ShowOwnedPopups(Handle, False); ShowHint := False; Destroying; DestroyComponents; //调用Application.DestroyComponents方法 end; end;
我们现在又回到了DestroyComponents方法,很熟悉的感觉,Application的DestroyComponents会有什么不同呢? 情况并没有不同,它没有重写DestroyComponents,还是使用的TComponent.DestroyComponents方法。好了,现在我们也该明白为什么在TPanel.Create(Application)之后,不会再手动调用FreeAndNil(FFoolPan )了。 切忌:内存的重复释放引发的危害,远远比内存泄漏来得大来得猛烈。 有一篇博文就是讲“为什么重复free()比内存泄漏危害更大”,有兴趣的同学可以过去瞧瞧。 说了这么多,我们也该休息下了 :)
------------仅以此文,献给我自己、HOMS开发的同学们,还有深受客户端退出无响应的受害者------------ PS:个人新干博客地址 http://www./ |
|