小技巧: Delphi创建/调用C++对象

应用场景

图库编辑器使用Delphi编写,当其中图片转为PVRTC后,无法直接解码/预览,不够直观。

Imagination提供的PVRTexLib.dll,可用于处理PVR图片,包括编码/解码,但它导出的是C++的类(实际上导出的还是函数,类的函数)。由于C++和Delphi,对象的内存结构存在差异,除非改造C++部分的代码(将成员函数全部声明为虚函数,并且导出一个创建C++对象的函数),否则无法直接被Delphi使用。

正常做法是,用C++编写DLL,封装PVRTexLib.dll,导出C风格的函数供Delphi调用。但是,有没有可能跳过这一步,直接在Delphi中创建C++对象,并且使用呢?答案是肯定的!


什么是类?

“类”只不过是语法层面的概念,当程序编译后,到汇编这一级,类成员函数和普通函数本质上是一样的,但多了一个隐含的参数:this指针(在Delphi中叫Self),即当前要操作的对象的地址。而对象本身,是一块内存,主要存放了成员变量,以及其他必要的信息,比如虚函数表地址。

创建一个对象,就是分配一块内存,并调用构造函数;删除一个对象,就是调用析构函数,并释放那块内存;而调用对象的成员函数,就默默带上对象的地址,以便函数内部对其进行操作。

所以,只要模仿编译器来使用“类",就可以啦!


x86函数调用约定

thiscall:C++特有,this指针放入ECX,其余参数从右到左压入堆栈,由被调用方清理堆栈。非可变参数非静态C++类成员函数的默认调用约定。

stdcall:参数从右到左压入堆栈,由被调用方清理堆栈。32 位 Windows API 采用此调用约定。

cdecl:参数从右到左压入堆栈,由调用方清理堆栈。C++中非成员函数的默认调用约定,可变参数函数的强制调用约定

register:Delphi特有,参数1、2、3分别放入EAX、EDX、ECX,其余参数从左到右压入堆栈,由被调用方清理堆栈。Delphi的默认调用约定。

补充说明,为什么可变参数函数只能使用cdecl?因为由被调用方清理堆栈,是通过指令 ret x 实现的,在编译期这个 x 是不确定的,无法生成合适的指令。


C++函数命名规则

为了支持重载等语言特性,C++有着复杂的name mangling,且VC++和GCC采用的规则不同,VC++中,函数的命名规则如下:

 <全局函数的name mangling> ::= ?<function name>@ [<namespace>@]∞0 @ <func modifier> 
         <calling conv> [<storage ret>] <return type> <parameter type>∞1 <throw type> 
 <成员函数的name mangling> ::= ?<function name>@ [<namespace>@]∞0 @ <func modifier> 
         [<const modifier>]<calling conv> [<storage ret>] <return type> 
         <parameter type>∞1 <throw type> 

更具体的规则可以看这里 Visual C++名字修饰,如果用工具查看PVRTexLib.dll的导出函数,会发现函数名形如:

??0PixelType@pvrtexture@@QAE@EEEEEEEE@Z, 28, 00042620
??0CPVRTString@@QAE@PBDI@Z, 14, 0005F160
??0CPVRTexture@pvrtexture@@QAE@PBX@Z, 19, 0003EDC0
??1CPVRTexture@pvrtexture@@QAE@XZ, 37, 0003F180
?getDataPtr@CPVRTexture@pvrtexture@@QBEPAXIII@Z, 196, 0003F380
?saveFile@CPVRTexture@pvrtexture@@QBE_NABVCPVRTString@@@Z, 241, 00042200
?getDataSize@CPVRTextureHeader@pvrtexture@@QBEIH_N0@Z, 197, 00043800
?Transcode@pvrtexture@@YA_NAAVCPVRTexture@1@TPixelType@1@W4EPVRTVariableType@@W4EPVRTColourSpace@@W4ECompressorQuality@1@_N@Z, 128, 0005E6F0

以CPVRTexture(const void* pTexture)为例,我们试着分析一下它的命名:??0CPVRTexture@pvrtexture@@QAE@PBX@Z

?0构造函数(函数名省略)
CPVRTexture类名
pvrtexture命名空间
QAE

public / 非只读成员函数 / thiscall

PBX参数:指针 / const / void;(返回值描述省略)
Z缺省的异常规范

引入需要的DLL函数

根据上述规则,找出需要的函数,在Delphi中引入。注意:类成员函数的调用约定写为stdcall,普通函数的调用约定写为cdecl,原因见下文分析。

// PixelType::PixelType(uint8 C1Name, uint8 C2Name, uint8 C3Name, uint8 C4Name, uint8 C1Bits, uint8 C2Bits, uint8 C3Bits, uint8 C4Bits); ??0PixelType@pvrtexture@@QAE@EEEEEEEE@Z, 28, 00042620
procedure Dll_PixelType_0PixelType(C1Name, C2Name, C3Name, C4Name, C1Bits, C2Bits, C3Bits, C4Bits: Integer) stdcall; external 'PVRTexLib.dll' index 28;

// CPVRTString::CPVRTString(const char* _Ptr, size_t _Count = npos); ??0CPVRTString@@QAE@PBDI@Z, 14, 0005F160
procedure Dll_CPVRTString_0CPVRTString(StrPtr: PAnsiChar; StrLen: Integer) stdcall; external 'PVRTexLib.dll' index 14;

// CPVRTexture::CPVRTexture(const void* pTexture); ??0CPVRTexture@pvrtexture@@QAE@PBX@Z, 19, 0003EDC0
procedure Dll_CPVRTexture_0CPVRTexture(DataPtr: PByte) stdcall; external 'PVRTexLib.dll' index 19;

// CPVRTexture::~CPVRTexture(); ??1CPVRTexture@pvrtexture@@QAE@XZ, 37, 0003F180
procedure Dll_CPVRTexture_1CPVRTexture() stdcall; external 'PVRTexLib.dll' index 37;

// void* CPVRTexture::getDataPtr(uint32 uiMipLevel = 0, uint32 uiArrayMember = 0, uint32 uiFaceNumber = 0) const; ?getDataPtr@CPVRTexture@pvrtexture@@QBEPAXIII@Z, 196, 0003F380
function Dll_CPVRTexture_GetDataPtr(MipLevel, ArrayMember, FaceNumber: LongWord): PByte; stdcall; external 'PVRTexLib.dll' index 196;

// bool CPVRTexture::saveFile(const CPVRTString& filepath) const; ?saveFile@CPVRTexture@pvrtexture@@QBE_NABVCPVRTString@@@Z, 241, 00042200
function Dll_CPVRTexture_SaveFile(FileName: PPVRTString): Boolean; stdcall; external 'PVRTexLib.dll' index 241;

// uint32 CPVRTextureHeader::getDataSize(int32 iMipLevel=PVRTEX_ALLMipLevelS, bool bAllSurfaces = true, bool bAllFaces = true) const; ?getDataSize@CPVRTextureHeader@pvrtexture@@QBEIH_N0@Z, 197, 00043800
function Dll_CPVRTextureHeader_getDataSize(MipLevel: Integer; AllSurfaces, AllFaces: LongBool): LongWord; stdcall; external 'PVRTexLib.dll' index 197;

// bool PVR_DLL Transcode(CPVRTexture& sTexture, const PixelType ptFormat, const EPVRTVariableType eChannelType, const EPVRTColourSpace eColourspace, const ECompressorQuality eQuality=ePVRTCNormal, const bool bDoDither=false);
function Dll_Transcode(ObjPtr: PPVRTexture; PixelType: TPixelType; ChannelType: EPVRTVariableType; Colourspace: EPVRTColourSpace; Quality: ECompressorQuality; DoDither: LongBool): Boolean; cdecl; external 'PVRTexLib.dll' index 128;

调用C++成员函数

由于这些成员函数都是固定参数的,并且没有额外修饰调用约定,所以调用约定为thiscall,需要把this指针放入ECX,其余参数压栈,Delphi并不直接支持这样的调用约定,怎么办?

起初我想到的是用汇编编写调用代码,后来我的同事大黄提醒:Delphi的默认调用约定register,第三个参数是放在ECX中的,利用这一点,不需要写汇编也可以达到同样的效果。

代码如下,加了一个占位参数Unused,使得ObjPtr成为第三个参数(注意:第一个参数是Self)。另外,引入C++成员函数时,调用约定写为stdcall,并且特意少写了一个参数(this指针),所以刚好可以和thiscall对上。

procedure TPVRTexLib.CPVRTexture_0CPVRTexture({$IFDEF PASCAL_CALL_CPP}Unused: Integer; {$ENDIF}ObjPtr: PPVRTexture; DataPtr: PByte);
{$IFDEF PASCAL_CALL_CPP}
begin
  Dll_CPVRTexture_0CPVRTexture(DataPtr);
end;
{$ELSE}
asm
  push DataPtr
  mov ecx, ObjPtr;
  call Dll_CPVRTexture_0CPVRTexture
end;
{$ENDIF}

调用C++普通函数

C++中非成员函数的默认调用约定为cdecl,Delphi支持此调用约定,直接调用即可。代码如下:

function TPVRTexLib.Transcode(ObjPtr: PPVRTexture; PixelType: PPixelType; ChannelType: EPVRTVariableType; Colourspace: EPVRTColourSpace; Quality: ECompressorQuality; DoDither: LongBool): Boolean;
{$IFDEF PASCAL_CALL_CPP}
begin
  Result := Dll_Transcode(ObjPtr, PixelType^, ChannelType, Colourspace, Quality, DoDither);
end;
{$ELSE}
asm
  push DoDither
  push Quality
  push Colourspace
  push ChannelType
  push [PixelType + 4]
  push [PixelType]
  push ObjPtr
  call Dll_Transcode
  add esp, $1C
end;
{$ENDIF}

BTW,从反汇编代码来看,当参数为结构体时,VC++是将结构体内容push入栈后调用函数;而Delphi是将结构体地址push入栈后调用函数,同时,如果没有用const修饰结构体参数,则函数会在栈上申请空间,然后拷贝结构体内容。


创建C++对象

分配一块内存,调用构造函数进行初始化,代码如下:

function TPVRTexLib.CPVRTexture_New(DataPtr: PByte): PPVRTexture;
begin
  GetMem(Result, 96);
  CPVRTexture_0CPVRTexture({$IFDEF PASCAL_CALL_CPP}66, {$ENDIF}Result, DataPtr);
end;

删除C++对象

调用析构函数,然后释放内存,代码如下:

procedure TPVRTexLib.CPVRTexture_Delete(ObjPtr: PPVRTexture);
begin
  CPVRTexture_1CPVRTexture({$IFDEF PASCAL_CALL_CPP}66, {$ENDIF}ObjPtr);  
  FreeMem(ObjPtr);
end;

是不是很有趣呢 ? 

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值