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