首页  编辑  

如何在Delphi中构造Win32 API的特殊记录类型

Tags: /超级猛料/Language.Object Pascal/数组、集合和记录、枚举类型/   Date Created:

1.1   如何在Delphi中构造Win32 API的特殊记录类型?

问题

       有些Win32 API描述的结构,简直就是为C语言而做的,以至于我们根本无法用正常的方法在Delphi中定义一个记录来表示它。这使得一些特殊的API只能通过分配指针和直接操作内存块来构造传入参数。

       总结一下这些特殊的结构,主要包括两类:

异常的变体结构。例如一个包含输入设备(MOUSE)物理状态信息的结构:

typedef struct tagRAWMOUSE {

 USHORT    usFlags;

 union {

        ULONG    ulButtons;

            struct {

                      USHORT usButtonFlags;

                      USHORT usButtonData;

                      };

 };

 ULONG ulRawButtons;

 LONG  lLastX;

 LONG  lLastY;

 ULONG ulExtraInformation;

} RAWMOUSE, *PRAWMOUSE, *LPRAWMOUSE;

在结构中存在不定长的数组

typedef struct _MINIDUMP_USER_STREAM_INFORMATION {

 ULONG UserStreamCount;

 PMINIDUMP_USER_STREAM UserStreamArray;

} MINIDUMP_USER_STREAM_INFORMATION, *PMINIDUMP_USER_STREAM_INFORMATION;

typedef struct _IMAGE_OPTIONAL_HEADER {

WORD Magic

// ...

DWORD NumberOfRvaAndSizes

IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];

} IMAGE_OPTIONAL_HEADER, *PIMAGE_OPTIONAL_HEADER;

       

       那么,在Delphi中我们应该用什么方法来定义这些结构呢?

       

解决思路

       首先要应付的是变体结构。其实在Delphi中有变体记录用于定义可变类型的记录。例如:

type

 TSharedModule = Record

   Next           : PSharedModule;

   MemMgrModule   : HModule;

   SetThirdMemMgr : procedure(hMod: HModule);

   case byte of

     0 : (StaticModules : DWORD);

     1 : (Module : HModule);

 end;

       但Delphi的语法规则里,要求"变动部分必须放在其它字段之后"。这使得在记录中间存在变动部分时很难于描述。例如上面提到的tagRAWMOUSE结构。

       我们来看一下约定"变动部分必须在其它字段之后"这条规则的真实理由。要在内存中分配一个变体结构的变量,必须保证即使是极限情况下,类型所声明的全部字段都要能被包含。也就是说,变体记录的长度(SizeOf)将是能包含全部可变部分的最大长度。如果变动部分在一些字段前面,就无法确定其它部分的真实长度,因此才会有这样一条奇怪的约定。

       接下来我们应该知道,对于Delphi来说,一个类型一旦定义完,则表明它的长度是既定的。在代码一层的表现,就是随时都可以用SizeOf()来取长度值。例如:

type

 TSharedModule = Record

   // ...

 end;

 TSharedModel_ByteBuff = array [0..SizeOf(TSharedModule)] of byte;

       这里我们已经窥见到定义一个异常的变体结构的方法:既然已经定义的类型是定长的,那么我们就可以在变体记录中定义一个既定类型的域。

       除了提前定义一个类型之外,也可以直接定义某一个域的类型,这与声明变量类型的方法是一致的。

       接下来我们来讨论 "不定长的数组"。

       不定长数组可能存在两种情况:一种是一个指向数组的指针,例如_MINIDUMP_USER_STREAM_INFORMATION结构;另一种是一个未知长度的数组(不是数组指针),并在记录中另有一个域在运行期填写数组的实际大小,例如_IMAGE_OPTIONAL_HEADER结构。

       对于含有指向数组的指针的记录,由于指针的类型大小是既定的,因此这样的记录定义与普通的记录定义没有什么不同。不过应该知道的是:完全可以用一个指针动态数组的指针来替代这个指针,这不会给代码造带来负面的影响。

       对于未知长度的数组并附带一个长度域的情况,就比较复杂了,只能使用动态分配内存的方法来构建记录。此外,在存取记录和数组的过程中,也需要时时注意越界访问的问题。

       由于数组是不定长的,因此通常会在记录中保留一个域用以存放运行期的数组元素数。但是并没有一种通用的方法可以在调整数组长度同时去设定这个域。例如在_IMAGE_OPTIONAL_HEADER结构中,域NumberOfRvaAndSizes和DataDirectory就至少需要用两行代码分开填写。

       这样的结构描述大多数出现在格式化文件存储上,你不必奇怪为什么会出现这种格式。因为这样的结构,可以使得在读取文件到一个变量的内存时,能先读Size值,再读取数组的全部元素。

       伴随不定长数组而来的问题,主要是来自于Delphi在运行期时对数组边界的检测。通常会使用类型如下的定义来描述一个不定长的数组:

type

 TNoSizeArr = array [0..0] of byte;

       而Delphi缺省打开边界检测选项(range checking),因此对该数组的访问通常会导致异常。这种情况下,应该在单元前加入编译条件:{$RANGECHECKS OFF} or {$R-}。

       另一种使Delphi忽略边界检测的方法是定义一个足够大的数组类型(类型定义并不实际占用内存):

         TFullArr = array [0..MaxInt div SizeOf(DWORD)-1] of DWORD;

       然后通过对记录域进行类型强制转换,该问时边界检测选项就可以打开。

具体步骤

       一、提前定义类型来声明变体记录的可变部分。例如声明tagRAWMOUSE记录:

type

 tagRAWMOUSE_union = record

   case Integer of

     0: (

       ulButtons: ULONG);

     1: (

       usButtonFlags: USHORT;

       usButtonData: USHORT);

 end;

 tagRAWMOUSE = record

   usFlags: USHORT;

   union: tagRAWMOUSE _union;

   ulRawButtons: ULONG;

   lLastX: LONG;

   lLastY: LONG;

   ulExtraInformation: ULONG;

 end;

       二、直接声明变体记录的可变部分。例如声明tagRAWMOUSE记录:

type

 tagRAWMOUSE = record

   usFlags: USHORT;

   union: record

     // ...

     // 参见上例中tagRAWMOUSE_union的声明

     end;

   ulRawButtons: ULONG;

   lLastX: LONG;

   lLastY: LONG;

   ulExtraInformation: ULONG;

 end;

三、使用动态数组来替代记录中的数组指针定义。例如声明_MINIDUMP_USER_STREAM_INFORMATION记录:

type

 PMINIDUMP_USER_STREAM =  ^_MINIDUMP_USER_STREAM_ARRAY;

 _MINIDUMP_USER_STREAM = record

   Type_: ULONG;

   BufferSize: ULONG;

   Buffer: Pointer;

 end;

 _MINIDUMP_USER_STREAM_ARRAY = array of _MINIDUMP_USER_STREAM;

 _MINIDUMP_USER_STREAM_INFORMATION = record

   UserStreamCount: ULONG;

   UserStreamArray: PMINIDUMP_USER_STREAM;

 end;

四、定义和操作记录中的不定长数组域。以_IMAGE_OPTIONAL_HEADER记录为例:

在Windows.pas单元中,_IMAGE_OPTIONAL_HEADER记录被声明为:

type

 _IMAGE_OPTIONAL_HEADER = packed record

   Magic: Word;

   //...

   NumberOfRvaAndSizes: DWORD;

   DataDirectory: packed array[0..IMAGE_NUMBEROF_DIRECTORY_ENTRIES-1] of TImageDataDirectory;

 end;

       这个类型声明并不是非常合理。因为PE文件格式中没有约定DataDirectory的数组长度一定为IMAGE_NUMBEROF_DIRECTORY_ENTRIES (16)个,这个值只是Windows系统中的一个惯例。所以用上面这个记录类型的变量读取"不规范的"PE文件时,就会丢失信息。这种情况下,我们可以采用如下的声明和存取代码:

type

 _IMAGE_OPTIONAL_HEADER = packed record

   Magic: Word;

   //...

   NumberOfRvaAndSizes: DWORD;

   DataDirectory: packed array[0..0] of TImageDataDirectory;

 end;

(*

 var

   Header : _IMAGE_OPTIONAL_HEADER;

   i : Integer;

   Data : TImageDataDirectory;

 // ... 假定数据已经从文件中读到变量Rec中

 // 读取代码示例1:使用编译条件来忽略边界检测

 {$R-}

 for i := 0 to Header.NumberOfRvaAndSizes - 1 do

   Data := Header.DataDirectory[i];

 {$R+}

 // 读取代码示例2:使用类型强制转换来忽略边界检测

         // Type

         //   TDataArr = array [0..MaxInt div SizeOf(TImageDataDirectory)-1] of TImageDataDirectory;

 //   PDataArr = ^TDataArr;

 for i := 0 to Header.NumberOfRvaAndSizes - 1 do

   Data := PDataArr(@Header.DataDirectory)[i];

*)

专家说明

       如果记录中的域是"不定长数组",而不是"数组指针",那么使用动态数组类型去替代的话,将会导致非常严重的问题。因此类似这样的定义是不正确的:

= packed record~Word;~Word;~array of TPaletteEntry; // 这样 end;

       在Win32 API中,palPalEntry域是一个数组,而Delphi中的态数组实际上是一个指向数组起始地址的指针。此外,该起始地址的负偏移上还有一个头结构,用于存放长度和引用计数。因此不能直接在一个记录中包含这个动态数组(这与Win32 API所要求的记录格式不相同)。

       但如果仅在记录中引用它的指针就不受影响了。上面的示例三使用了这一技巧,使得操作_MINIDUMP_USER_STREAM_INFORMATION记录时更加符合Delphi的语言习惯。如果你想查看更加标准的定义,可以下载开源项目JEDI API的代码包。

       选用"设定编译条件"还是"强制类型转换"这两种方案的哪一种,是程序员自行决定的。两种方案都不会带来编译文件大小或代码执行效率的变化。

       由于该数组被描述成只有一个元素,因此记录的大小(SizeOf)就不再是一个有效值。这使得为这种类型的变量初始分配内存变得相对复杂。

       如果要访问不定长数组之后的其它域,则需要重新做地址运算。--这是没有其它折衷方案的。

       

相关问题

       参考:

       http://www.delphibbs.com/delphibbs/dispq.asp?lid=1885566