本人在OOP方面刚刚入门,只是看过一些国外这方面的好材料,才萌生了写本文的念头。希望能够起到抛砖引玉的作用,引出高手们的批评和建议。
OOP和面向过程都是编程中的思想,用学术一些的话是paradigm。曾经有人说过,既然cfront生成的是C代码,那么用C本身乃至汇编都可以实现OOP,只是太多东西需要自己手工来完成。确实是这样的,面向过程早就用在汇编设计中了,OOP也早就和汇编有了交汇点(95年之前,TASM就引入了OOP的概念)。只是汇编实现OOP是没有形式上的,无法提供C++这样的Strong-typed和其他安全保证(比如存取权限)。封装只是一种概念上的,自觉遵守的。
OOP有几个关键,据我的粗浅理解即为:封装性,继承与多态。具体表现就是把数据和操作数据的函数放在一起,数据放在对象中,提供接口实现存取。继承性实现了语义或者实现的继承,同时体现在概念层次与代码重用两个方面。多态则是利用指针实现使用pointer或reference来实现同一函数在不同继承类中的多态表现。
OOP的对象模型有好几种实现方式,在《inside C++ object model》中有极其详尽的叙述:
- 1.只把数据放在对象中,而通过name mangling技术把member-function与class关联起来。
- 2.单表模型,把member function的pointer放入到单独的一个表格,把表格的入口地址放入对象中(一个类对应一个表格)。这在C++中表现为Vtbl与Vptr,这种模型实现了运行时的动态灵活性,虽然多了两次dereference。
- 3.双表模型,把数据与函数分列在两个表格中,然后把两个表格的入口地址存放在对象中,使得单个对象有了固定的大小。
- 4.简单模型,这个是汇编实做的时候用的模型。就是对象中即保存了数据也保存了函数地址。无论是TASM还是MASM,都是这么做的。
从效能上来说,C++的做法是最优的。汇编使用第四种是迫不得已,是为了实现的简单性。一定程度上与汇编的高效的精神违背。
TASM已经不常用了,其OOP的做法和MASM的做法也是类似的。这里主要讨论MASM的OOP做法。作者是NaN 和Thomas Bleeker。其实现的办法是用宏定义来达到本来应该是编译器做的幕后工作。其中的宏的技巧很多。但是最终的使用是挺简单的。宏的定义放在一个OBJECTS.INC的文件中,asm文件包含这个inc就能使用这个object model。
虽然宏做得很精巧,但是毕竟MASM缺少支持OOP的语法特性,在使用的很多方面都有麻烦或者在空间时间上有代价。比如覆盖基类的虚函数必须每次手工的完成。也就是继承的层次中所有父类以上的被覆盖的虚函数都需要在子类中手工完成。虽然是有这样那样的缺点,但是OOP还是给汇编带来了不少好处。比如:
- 1.汇编更好的和COM,C++这样的面向对象领域的东西互动。已经有用汇编+OOP调用com的例子。如果用汇编+OOP来写com将可以产生适合高速度和小尺寸的组件。
- 2.扩大了汇编能够解决的问题范围,使得汇编程序更加容易管理和合作编写。这个object model的作者就用汇编+OOP写了一个基于神经网络的手写字母识别的程序,不到200k(其中大部分是图象文件占用的空间)。
使用
定义一个基类的办法。
;准备好函数原型 Shape_Init PROTO :DWORD Shap_destructorPto TYPEDEF PROTO :DWORD Shap_getAreaPto TYPEDEF PROTO :DWORD Shap_setColorPto TYPEDEF PROTO :DWORD, :DWORD
;实际上就是STRUC的定义 CLASS Shape, Shap CMETHOD destructor CMETHOD getArea CMETHOD setColor Color dd ? Shape ENDS
.data ;初始化 BEGIN_INIT dd offset Shap_destructor_Funct dd offset Shap_getArea_Funct dd offset Shap_setColor_Funct dd NULL END_INIT
.code Shape_Init PROC uses edi esi lpTHIS:DWORD ;实际调用初始化 SET_CLASS Shape ;把edi assmue 为Shape类型 SetObject edi, Shape ;额外定义的DPrint宏,不用细究 DPrint "Shape Created (Code in Shape.asm)" ;取消assmue ReleaseObject edi ret Shape_Init ENDP Shap_destructor_Funct PROC uses edi lpTHIS:DWORD SetObject edi, Shape DPrint "Shape Destroyed (Code in Shape.asm)" ReleaseObject edi ret Shap_destructor_Funct ENDP Shap_setColor_Funct PROC uses edi lpTHIS:DWORD, DATA:DWORD SetObject edi, Shape mov eax, DATA mov [edi].Color, eax DPrint "Shape Color Set!! (Code in Shape.asm)" ReleaseObject edi ret Shap_setColor_Funct ENDP Shap_getArea_Funct PROC uses edi lpTHIS:DWORD SetObject edi, Shape DPrint " " DPrint " SuperClassing!!!!! This allows code re-use if you use this method!!" DPrint " Shape's getArea Method! (Code in Shape.asm)" mov eax, [edi].Color DPrint " Called from Shape.getArea, (Code in Shape.asm)" DPrintValH eax, " This objects color val is" DPrint " " ReleaseObject edi ret Shap_getArea_Funct ENDP
继承这个类
include Shape.asm ; Inherited class info file Circle_Init PROTO :DWORD Circ_destructorPto TYPEDEF PROTO :DWORD Circ_setRadiusPto TYPEDEF PROTO :DWORD, :DWORD Circ_getAreaPto TYPEDEF PROTO :DWORD
CLASS Circle, Circ ;继承原有的数据和函数 Shape <> ; Inherited Class CMETHOD setRadius Radius dd ? Circle ENDS
.data BEGIN_INIT dd offset Circ_destructor_Funct dd offset Circ_setRadius_Funct dd NULL END_INIT
.code Circle_Init PROC uses edi esi lpTHIS:DWORD ;初始化并实现继承 SET_CLASS Circle INHERITS Shape SetObject edi, Circle ;相当于构造函数重置vptr OVERRIDE getArea, CirleAreaProc DPrint "Circle Created (Code in Circle.asm)" ReleaseObject edi ret Circle_Init ENDP Circ_destructor_Funct PROC uses edi lpTHIS:DWORD SetObject edi, Circle DPrint "Circle Destroyed (Code in Circle.asm)" ;实现了基类函数的调用 SUPER destructor ReleaseObject edi ret Circ_destructor_Funct ENDP Circ_setRadius_Funct PROC uses edi lpTHIS:DWORD, DATA:DWORD SetObject edi, Circle mov eax, DATA mov [edi].Radius, eax DPrint "Circle Radius Set (Code in Circle.asm)" ReleaseObject edi ret Circ_setRadius_Funct ENDP CirleAreaProc PROC uses edi lpTHIS:DWORD LOCAL TEMP SetObject edi, Circle SUPER getArea mov eax, [edi].Radius mov TEMP, eax finit fild TEMP fimul TEMP fldpi fmul fistp TEMP mov eax, TEMP DPrint "Circle Area (integer Rounded) (Code in Circle.asm)" ReleaseObject edi ret CirleAreaProc ENDP
根据类来生成对象,并使用
DEBUGC equ 1 .586 .model flat,stdcall option casemap:none include \masm32\include\windows.inc include \masm32\include\masm32.inc include \masm32\include\kernel32.inc include \masm32\include\user32.inc includelib \masm32\lib\kernel32.lib includelib \masm32\lib\user32.lib includelib \masm32\lib\masm32.lib include Dmacros.inc include Objects.inc include Circle.asm .data .data? hCircle dd ? .code start: ; Recuse all inherited constructors.. and do all inits DPrint " " DPrint " >>> main.asm <<< [ mov hCircle, $NEW( Circle ) ]" mov hCircle, $NEW( Circle ) DPrint " " DPrint " >>> main.asm <<< [ METHOD hCircle, Circle, setColor, 7 ]" METHOD hCircle, Circle, setColor, 7 DPrint " " DPrint " >>> main.asm <<< [ METHOD hCircle, Circle, setRadius, 2 ]" METHOD hCircle, Circle, setRadius, 2 DPrint " " DPrint " ------------ TEST POLYMORPHIC METHOD hCircle.getArea ------------- " DPrint " " DPrint " >>> main.asm <<< [ DPrintValD $EAX( hCircle, Circle, getArea ) , 'Area of hCircle' ]" DPrintValD $EAX( hCircle, Circle, getArea ) , "Area of hCircle" DPrint " " DPrint " ------------ TEST POLYMORPHIC METHOD hCircle.getArea ------------- " DPrint " " DPrint " >>> main.asm <<< [ DPrintValD $EAX( hCircle, Shape, getArea ) , 'Area of hCircle' ]" DPrint " Typing calling this Ojbect Instance as a SHAPE type only! This is the true value" DPrint " of Polymorphism. We dont need to know its a Circle object in order to get the" DPrint " proper area of this instance object, that is inherited from Shape." DPrint " " DPrintValD $EAX( hCircle, Shape, getArea ) , "Area of hCircle" DPrint " " DPrint " " DPrint " >>> main.asm <<< [ DESTROY hCircle ]" DESTROY hCircle DPrint " " DPrint " " DPrint " NOTE: superclassing here, as each destructor call's the SUPER destructor" DPrint " To properly clean up after each class. To see SUPER classing in" DPrint " in the Polymorphic getArea Function. Uncomment the SUPER code in" DPrint " CircleAreaProc, and re-compile" call ExitProcess end start
看起来挺杂乱的,其实还是挺整齐的。由四部分组成。
- 第一部分是各个成员函数的声明。特别的一定要有一个“类名_Init”的函数,这个函数是类的构造函数,名字就是这个,不能改动。
- 第二部分是由class引导的函数声明,其实就是定一个STRUC,也就是结构体。其中通过内含基类的定义来达到结构上的继承。(数据上的继承在构造函数中调用SET_CLASS完成)。
- 第三部分是放在.data中的初始化序列(BEGIN_INIT,END_INIT)。相当于C++的vtbl,但又包括了对象的数据成员的初始值。
- 第四部分是各个成员函数的实现。特别的是构造函数中要调用的SET_CLASS和有可能调用的OVERRIDE,完成了数据的继承和虚函数的改写。
实际使用中参考一些已经有的例子就可以蛮舒畅的使用了。确实可以带来很大的方便。
原理
所有的奥秘都在Object.inc中,其中定义了如下的宏
; --=====================================================================================-- ; MACRO LIST INDEX: ; --=====================================================================================-- ; NEWOBJECT 创建新的对象实体 ; METHOD 调用实体中的函数 ; DESTROY destroy 对象 (MUST) ; SetObject 把寄存器中的指针“认为”某种结构的指针 ; ReleaseObject 取消这种“认为” ; OVERRIDE 改写表格中函数的地址,实现多态 ; SET_CLASS 实现初始化,如有必要实现继承 (MUST) ; SUPER 调用基类函数的支持 ; ; $EAX() Accelerated METHOD, returns in eax ; $EBX() Accelerated METHOD, returns in ebx ; $ESI() Accelerated METHOD, returns in esi ; $EDI() Accelerated METHOD, returns in edi ; $NEW() Accelerated NEWOBJECT, returns in eax ; $SUPER() Accelerated SUPER, returns in eax ; $DESTROY() Accelerated DESTROY, returns in eax ; $invoke() Accelerated invoke, returns in eax ; ; BEGIN_INIT 在数据段中标记初始化的信息 (MUST) ; END_INIT 表明标记结束 (MUST) ; ; CLASS 即为STRUCT (MUST) ; SET_INTERFACE To Declair Abbreviated Interface and Abv Name (MUST) ; CMETHOD 声明类或者接口中的函数 ; ; --=====================================================================================--
宏的数量也不是很多,但是的确完成了编译器为我们完成的幕后工作。我只列出宏展开前后的代码,并加以解释。对于宏的具体实现由于牵涉到诸多语法和技巧,不方便详细讲解(其实我也是刚刚通过查手册,一点点读懂的)。
先来看CLASS吧,这个是想当然的入口部分。
其实很简单就是把Class换成STRUC。
CLASS Shape, Shap CMETHOD destructor CMETHOD getArea CMETHOD setColor Color dd ? Shape ENDS
替换之后就是
Shape STRUC CMETHOD destructor CMETHOD getArea CMETHOD setColor Color dd ? Shape ENDS
很自然,看看CMETHOD是怎么做的
CMETHOD destructor
就变成了
destructor PTR Circ_destructorPto ?
整个就展开为:
Shape STRUC destructor PTR Circ_destructorPto ? getArea PTR Circ_getAreaPto ? setColor PTR Circ_setColor ? Color dd ? Shape ENDS
^_^,结构体中就是函数指针和数据嘛。然后,线索就断了。光这样定义一个结构是肯定不行的。那么就从对象的产生开始吧,new是怎么做的。
NEWOBJECT Circle
-->
invoke GetProcessHeap invoke HeapAlloc, eax, NULL, SIZEOF Circle push eax invoke Circle_Init, eax pop eax
这里显示了一个很明显的缺陷,就是一定要在win32下使用,因为win32api的使用。可以把api替换成一个外部的函数。然后放在不同的平台上使用只要改变这个动态分配内存的函数就可以了。
产生的代码很朴实,就是分配内存,然后调用对象的构造函数。这里,强制的要求类的构造函数要以“类名_Init”的形式。虽然不是什么大的限制,但是也不是很爽。这样做也是有道理的,通过在编写上对接上名字可以避免用指针这样的东西来实现灵活性所带来的overhead,下面可以看到析构函数用了指针的形式,这是因为这里默认了virtual desturctor。
好,下面前进到构造函数,我们看构造函数是怎么写的:
Shape_Init PROC uses edi esi lpTHIS:DWORD SET_CLASS Shape SetObject edi, Shape DPrint "Shape Created (Code in Shape.asm)" ReleaseObject edi ret Shape_Init ENDP
lpTHIS应该不会陌生,就是指向对象的指针。这里一个对象也就是一个sturct啦。第一行就是关键所在,SET_CLASS是最麻烦和富有技巧的一个宏。我们来看看是怎么做的
SET_CLASS Shape
-->
push esi push edi cld mov esi, offset @InitValLabel mov edi, lpTHIS mov ecx, @InitValSizeLabel shr ecx, 2 rep movsd mov ecx, @InitValSizeLabel and ecx, 3 rep movsb pop edi pop esi
push和pop是很普通的保存现场的做法。而mov esi, offset @InitValLabel则是和后面的BEGIN_INIT有关。offset @InitValLabel也就是BEGIN_INIT所标记的地址。这一段程序其实没有做什么特别的事情。也就是把BEGIN_INIT和END_INIT之间的初始化的数据赋给刚刚new出来的对象。lpTHIS就是这个对象的地址。由于SET_CLASS总是假定你在构造函数中调用它,所以lpTHIS当然是存在的(作为构造函数的参数)。cld, rep movvsd 等都是汇编的快速搬移数据的技巧。查一下手册就知道是干什么的了。也就是一开始尽量一个dword一个dword的搬移,然后就一个byte一个byte的移,直到全部都搬过去了。
如果带上了继承,则要麻烦许多
SET_CLASS Circle INHERITS Shape
-->
push esi push edi cld mov edi, lpTHIS mov esi, offset @InitValLabel mov eax, [esi] mov [edi], eax add esi, 4 add edi, Inher mov ecx, (@InitValSizeLabel - 4) shr ecx, 2 rep movsd mov ecx, (@InitValSizeLabel - 4) and ecx, 3 rep movsb pop edi pop esi
由于继承了,所以要重置析构函数。mov eax, [esi]和mov [edi], eax做了这样的工作。而由于析构函数的地址已经改变了,所以只需要也只能继承后面的数据成员包括虚函数的指针
接下来是对象的destroy
DESTROY hCircle
-->
mov eax, hCircle push eax call dword ptr [hCircle] push eax invoke GetProcessHeap invoke HeapFree, eax, NULL, hCircle pop eax
由于析构函数的地址是对象(结构体)的第一个成员,所以call就是调用析构函数。调用了之后就用win32api把申请的内存释放掉
接下来是析构函数
Circ_destructor_Funct PROC uses edi lpTHIS:DWORD SetObject edi, Circle DPrint "Circle Destroyed (Code in Circle.asm)" SUPER destructor ReleaseObject edi ret Circ_destructor_Funct ENDP
这个是shape的继承类circle的析构函数。里面有一个SUPER,实现了调用基类中的函数。我们来继续看它的实现。
SUPER destructor
-->
invoke Circ_destructorPto PTR [ (INHER_initdata+INHER.MethodName) ], lpTHIS
Circ_destructorPto指定这个地址的类型是一个什么样的函数。INHER是宏内部的一个全局的东西,表示该类的基类名称。INHER_initdata+INHER.MethodName的结构就是这个类在基类中的实际地址。
剩下的就是实际的使用对象中的函数了(你是“无权”操作对象中的数据的,虽然是概念上的。实际上你可以肆意的破坏这里体现的OOP思想。因为汇编不提供这样的保护)。
METHOD hCircle, Shape, getArea
-->
mov edx, hCircle invoke (Shape PTR [edx]).getArea, edx
收获的季节了。这一句体现了多态的思想。hCircle指向的是一个Circle类的对象,但是调用的时候解释为Shape类。自己去理解吧。哪里体现了多态。
我的看法
从全局来看这个对象模型,我们可以发现是这样的。
- 对象数据和虚函数指针放在同一个表格中
- 所有的函数都是虚的
- 继承类改写基类的虚函数需要在初始化数据之后手工完成(构造函数中)
- 仅提供对上一层基类中被改写的虚函数的访问
- 内存的分配和释放使用win32api
OOP的三个特性的支持,如下
- 封装性:并没有提供对于其中数据的特别保护(没有Private)。数据和函数指针置于同一个结构体中成为一个对象。访问数据通过提供的接口完全靠自觉。
- 继承:通过结构体定义的嵌套(定义中包含已经定义的结构体),完成结构上的继承。通过SET_CLASS完成数据意义上的继承。所有的继承都是Public的。
- 多态:多态的狭义理解是对于同一个函数的调用将有不同的行为。我们通过以下比较观察,为什么这个对象模型支持了多态(因为它支持了派生类对于基类函数的改写)。
class Shape { virtual float getArea(); …… }; class Cicle: public Shape { float getArea(); …… };
当你通过对象指针调用一个对象中的虚函数的时候。其实你在编译的时候已经指定了该指针的类型。比如:
float getArea(Shape* shp) { return shp->getArea(); }
所以,编译器可以通过查询编译时的信息来确定你所调用的函数在vtbl中的索引位置。然后这个调用就会被一个查询vtbl,然后call所代替。而运行的时候,传来的指针shp并不一定就是Shape类型,而是他的继承类型。这样两个类的vtbl内容可能不一样(派生类改写了其中的某些slot的地址)。所以,这样就可以实现在不知道派生类是什么的情况下调用派生类的函数。奥秘就在于派生类和基类都把各自的实现版本放在了vtbl的相同位置。编译期确定了位置,运行时确定了该位置的内容。
而这个汇编版的object model呢?其实差不多。mov edx, hCircle和 invoke (Shape PTR [edx]).getArea, edx就是一个多态的调用。hCircle实际指向一个Circle类型的对象(在这里对象即有数据又承担了vtbl的任务)。而调用的时候设计是把这个指针作为Shape类性解释的。也就是按照shape类型中getArea所在的index来调用。相同的index索引到不同的函数,多态就产生了。
可能的改进
关于虚函数
其实说实话,这个对象模式做得实在是很不错,将宏的功能发挥到了极致。不过它强制的要求所有的类都有一个virtual destructor和所有的函数都是virtual。在宏的能力所及的范围内,已经将定义和调用都做得尽可能的简单,使用起来也的确有赏心悦目的感觉。不过我觉得不要把所有的函数都放在对象中(即强制性的作为虚函数),那样会增加一定的成本。
C++把非虚的成员函数看作是普通的函数,放在对象之外。其实我觉得这个对象模型也可以采用。而现有的强制性的将一些成员函数原型名称限定为“类型名_函数名Pto”,不如提供一个宏来这么做好了。
我的建议,将部分函数放入对象中(就是用CMETHOD来声明于类中)。而其他则不放入,只是写在同一个文件中。然后用METHOD调用对象的member-function时,它会在汇编时决定这个member-function是否存在于虚函数表(也就是对象本身存放的一系列函数指针)中。如果不是则按照普通函数的一样调用。如果是则按照现在这个模式调用。
METHOD这个宏也是编写得出来的。在SUPER这个宏中就有检验该method是否在基类中第一次出现(而且还实现了检验是否是上一层而不是多层基类的检验)。这么METHOD也可以检验调用的方法是否在class中出现过,然后分别使用不同的函数调用办法。
关于SUPER
这个model中只能调用上一层(也就是父类的)的被覆盖的函数,对于上上一层的被覆盖的函数则无法SUPER了。其中的障碍在于无法知道那个函数是在哪个类型中第一次出现。我想如果手工提供这个类名,则可以SUPER任意层次的被覆盖了的函数。像这样:
SUPER getArea
SUPER getArea, Shape
没有提供具体的类名,则认为是SUPER上一层的,否则使用具体的类名来SUPER。SUPER的奥秘也就是查询放在.data段的“类型名_initdata”中数据来达到“恢复”被改写的函数的功能。
随便讲一句,TASM中的TABLE却可以弥补OVERRIDE的缺陷,是一种更好的STRUC。不过那是Borland加强语法的结果。无论如何Thomas做到这一步也算是相当厉害。