应用场景
图库编辑器使用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> ::= ?
<成员函数的name mangling> ::= ?
[
更具体的规则可以看这里 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;
是不是很有趣呢 ?