C++与Delphi中对象的互访2015年9月18日QQ:393660149QQ群:484247712实现目标:1)测试在C/C++中调用Delphi对象的方法。2)测试在delphi中调用C++对象的方法。我们知道两种语言的编译器产生的代码有很多区别,这就造成跨语言的代码利用变得困难。但是对于一个程序员来说,只要所用的语言工具容许内存指针操作,那么总是有办法实现代码的互相调用。C/C++/Delphi都支持指针操作,因此可以遍历所有用户空间内存。而且Delphi编译器和C/C++编译器有很多选项可以让函数调用按照一定的规则产生代码。因此实现互访是有可能的。前提:1)至少明白对象、类、继承等基本概念。2)至少明白DLL库是什么。3)至少明白栈、堆、函数调用是什么。4)对编译、链接、加载有基本概念。基本概念:1)对象这里简单介绍下“对象”的概念,对象其实就是一个内存数据块,与C语言的结构体(struct)没有本质区别。当我们create(或new)一个对象时,其实就是最终会向内存管理单元申请(malloc)一个能存放在类中声明的变量的大小的内存块。然后调用初始化函数(构造函数)来初始化申请到这个内存块。在delphi的System单元中classfunctionTObject.NewInstance:TObject;beginResult:=InitInstance(_GetMem(InstanceSize));end;可以看到对象的create,就是先获取一块内存,然后初始化其中的变量。classfunctionTObject.InitInstance(Instance:Pointer):TObject;{$IFDEFPUREPASCAL}varIntfTable:PInterfaceTable;ClassPtr:TClass;I:Integer;beginFillChar(Instance^,InstanceSize,0);//全部清0PInteger(Instance)^:=Integer(Self);//将类对象赋值给对象的第一个指针。对象通常在堆内存分配,而不在栈上分配,这是由编译器的策略决定的,因为对象通常在函数内部创建之后还有可能被函数外部使用,如果保留在栈上,那么栈就不能释放,这是不符合栈的使用要求的。偶尔也有在栈上分配对象的,例如C++语言的声明:CSomeClasscobject;这样的声明,会让编译器直接在栈上创建一个对象,这时cobject对象与struct类型没有本质区别,因此cobject.someMethod类似结构体的“.”语法结构是正确的。此时需要注意的是,对象仅仅是在函数体内使用才这样声明。下面测试下直接将record(C/C++为struct)变量转换为对象。TMyClassRecord=recordpclass:Pointer;v1:integer;v2:integer;end;声明一个与类一样的记录结构,这类似C/C++的结构体,然后将第一个指针赋值为类对象地址,这样这个记录就是该类的一个对象。这也是可以在delphi中实现栈对象的方式。Varrobj:TMyclassRecord;在栈中创建了一个对象robj,robj.pclass:=TMyClass;则TMyClass(@robj).someMethod方法调用就是(TMyClass.create).someMethod;robj.pclass:=TMyOtherClass;则TMyOtherClass(@robj).someMethod方法调用就是(TMyOtherClass.create).someMethod;这里有意思的就是将一个record类型变量直接变成了一个类的对象来使用,而且你可以转换成任意的类,只要该record的结构与该类的对象结构相同。如果要在堆中创建对象,CSomeClasscobject=newCSomeClass();此时,cobject是一个对象指针,其地址是在栈上分配的,这个4字节(32位)的指针的值就是在堆中分配的地址。new会调用CSomeClass的构造函数,而在此之前还有很多其他代码,这些代码负责申请对象内存。此时必须采用cobject-someMethod这样的指针引用操作方式,而不是点语法。Delphi编译器不支持在栈中直接创建对象,因此Delphi中对象变量都是指针处理。类的方法函数与普通函数没有本质区别,只是这里有一点编译器为你做了的事情,就是this/self这个指针会默认传递给类方法函数。cobject.someMethod(param1);编译后实际的函数会变成someMethod(cobject,param1);这样,我们就将对象与类的方法关联起来了,对于不同的对象调用同样的方法函数,实际上该函数操作的就是不同的内存数据(对象)。而在方法内直接访问类的成员属性或者加上this/self也是一样,在编译时会变成cobject+偏移量(偏移量是由编译器根据变量在对象中的排列来确定的)。这里有一个字节对齐问题,每个编译器对于对象的字节对齐处理都不同的,这没有严格规定,因此在不同语言之间共享对象时,就必须检查字节对齐。2)Delphi对象的内存布局:Delphi编译器只支持ObjectPascal语言,这里我们就暂且用Delphi名词代替ObjectPascal。Delphi比C++具有了更多的动态特性,但是相对于C++,省去了很多繁琐的技术,如多重继承、模板等。Delphi相对来说更加简洁些,功能当然就少些,学起来也更清爽些。个人认为ObjectPascal简洁的代码是所有语言里最为漂亮的。Delphi的对象第一个指针指向类对象,然后紧接着是对象的私有变量和公有变量(属性)。这里说的“类对象”是什么?就是一块内存,这块内存是由编译器维护的,程序员通常不知道这块内存的存在,这些内存中存放的就是对于类的一些说明信息,这些信息是编译器在编译期收集的,实现语言的RTTI就要靠这些信息。如Delphi的类对象中就保存了对象的大小,属性的类型、方法表,虚拟函数表,父类指针。在delphi的System单元中定义了类对象的数据结构vmtSelfPtr=-76;//保存了指向自己的指针,就是类self的地址。vmtIntfTable=-72;vmtAutoTable=-68;vmtInitTable=-64;vmtTypeInfo=-60;//属性类型信息,对象释放时,根据类型自动释放属性指向的对象。vmtFieldTable=-56;//属性类型信息,对象释放时,根据类型自动释放属性指向的对象。vmtMethodTable=-52;//方法表,只有published的方法才被记录vmtDynamicTable=-48;//动态方法表,运行期才查询此表地址来获得函数调用vmtClassName=-44;vmtInstanceSize=-40;//实例尺寸=所有变量大小+4字节(第一个指针指向类)vmtParent=-36;//用来查询父类信息vmtSafeCallException=-32deprecated;//don'tusetheseconstants.vmtAfterConstruction=-28deprecated;//useVMTOFFSETinasmcodeinsteadvmtBeforeDestruction=-24deprecated;vmtDispatch=-20deprecated;vmtDefaultHandler=-16deprecated;vmtNewInstance=-12deprecated;vmtFreeInstance=-8deprecated;vmtDestroy=-4deprecated;vmtQueryInterface=0deprecated;//这些是基于COM的声明,vmtAddRef=4deprecated;vmtRelease=8deprecated;vmtCreateObject=12deprecated;如果不是创建COM对象,0偏移地址处就是第一个虚拟方法。当Delphi要引用COM对象时,那么就可以将COM对象指针的0偏移处转换为QueryInterface方法。当我们创建一个对象时,对象的第一个指针就指向类对象0偏移处,例如我们可以获得对象的地址然后通过这个地址中的第一个指针获得类对象地址,然后将这个地址-40,就可以获得类的名称。等下我们讲解在C++中怎样通过在DLL中输出的Delphi对象来获得其类名。注意Delphi的对象self与类的self是不同的,类的self指向了类对象的vmtSelfPtr。3)Delphi中类函数的类型(1)普通函数:在编译时就会变成实际的地址,因为这些函数地址是确定的,因此除非你自己编写函数将这些函数的地址保存到一个表,否则在运行时,你就没法再通过代码来查询到这些函数地址。这样的函数,在C++中是没法访问的(除非你在调试时记录这个函数加载到内存的位置)。(2)published函数这些函数会被记录到vmtMethodTable指向的方法表中,那么我们只要获得这个对象指针,根据对象指针找到类对象,然后找到方法表,通过遍历方法表就可以获得指定函数名称的方法。方法表中记录了方法的地址和名称以及参数类型。对于一个已经编译好的VCL库,delphi设计器一样采用了方法表来获得类支持的方法函数,因为delphi只需要动态管理published的方法,对于public方法是在编译期确定的,不公布给其他动态调用者。方法描述符:前2个字节为描述符长度,然后是方法地址4字节,紧接着是方法名字符串。classfunctionTObject.MethodAddress(constName:ShortString):Pointer;MethodAddress是用汇编写的,为了提高速度。(3)virtual函数虚拟函数,在C++中也有这个概念,是实现多态的机制。虚函数的地址并不是在运行期才确定。虚拟方法表的地址是在编译期就确定的,只是对于虚方法表的调用编译器处理时会转个弯,并不直接调用函数地址,而是调用虚拟方法表的地址。而多态其实就是因为对象指针的变化导致引用到的虚拟方法表也不同。SuperClassObj=newSomeClass1();Obj.virtualMethod();//调用的是SomeClass1虚拟方法表中的方法。Obj=newSomeClass2();Obj.virtualMethod();//调用的是SomeClass2虚拟方法表中的方法。同样的语句,但是调用的是不同的方法,这里其实有一个隐含参数this/self。通过Obj本身找到了类对象,然后通过类对象找到了虚拟方法。因为Obj本身指向的对象变了,其找到的方法自然也就不同。虚拟方法在编译时,编译器会将其注册到以上类对象0偏移地址开始处。因此通过对象指针找到类对象时,第一个指针就是第一个虚拟方法。Obj.virtualMethod();编译后类似MOVEAX,Obj//EAX为对象地址MOVEAX,[EAX]//EAX为类对象地址Call[EAX+4]//类对象地址开始的第二指针也就是第二个虚拟函数地址。(4)dynamic动态函数生命为dynamic的函数,在编译时,会进入vmtDynamicTable指向的动态函数表,当子类继承时,并不会复制表中的内容到子类的类对象。因此子类在调用声明为dynamic的函数时,编译器并不检查是否有该函数的实现,编译器会产生一个查询方法然后再调用的代码,因此对于Obj.dynamicMethod方法的调用,编译后会是一段比较长的代码,而不是简单的Call指令。当然这段代码是一个可共享的函数,是由编译器支持库提供的。动态方法可以在0地址上操作:SomeClass1(0).dynamicMethod,运行时并不出错,因为运行时,查询方法函数在0地址对象上是找不到该方法的。正因为动态方法表并不复制到子类,因