我正在将Delphi 5中的TaskSchedule API相关代码移植到Delphi XE6。我遇到了结构对齐问题和sizeof
。
实际的TASK_TRIGGER
结构声明为:
typedef struct _TASK_TRIGGER {
WORD cbTriggerSize;
WORD Reserved1;
WORD wBeginYear;
WORD wBeginMonth;
WORD wBeginDay;
WORD wEndYear;
WORD wEndMonth;
WORD wEndDay;
WORD wStartHour;
WORD wStartMinute;
DWORD MinutesDuration;
DWORD MinutesInterval;
DWORD rgFlags;
TASK_TRIGGER_TYPE TriggerType;
TRIGGER_TYPE_UNION Type;
WORD Reserved2;
WORD wRandomMinutesInterval;
} TASK_TRIGGER
我使用的MsTask.pas
的旧翻译(以及MsTask的当前JCL交易)将其翻译为:
_TASK_TRIGGER = record
cbTriggerSize: WORD;
Reserved1: WORD;
wBeginYear: WORD;
wBeginMonth: WORD;
wBeginDay: WORD;
wEndYear: WORD;
wEndMonth: WORD;
wEndDay: WORD;
wStartHour: WORD;
wStartMinute: WORD;
MinutesDuration: DWORD;
MinutesInterval: DWORD;
rgFlags: DWORD;
TriggerType: TTaskTriggerType;
Type_: TTriggerTypeUnion;
Reserved2: WORD;
wRandomMinutesInterval: WORD;
end;
{1}这条记录在Delphi 5和XE6之间有所不同:
sizeof
SizeOf(TASK_TRIGGER) = 48
SizeOf(TASK_TRIGGER) = 47
的函数调用在Dephi5中成功,但在使用ITaskTrigger.SetTrigger(TASK_TRIGGER)
的Delphi XE6时失败。
如果我天真地猜测记录的布局,我会是:
The parameters are incorrect
但是当我实际检查Delphi 5调试器中的填充结构时,实际结构是48个字节,在□□□□ □□□□ //cbTriggerSize, Reserved1 (4 bytes)
□□□□ □□□□ //wBeginYear, wBeginMonth (8 bytes)
□□□□ □□□□ //wBeginDay, wEndYear (12 bytes)
□□□□ □□□□ //wEndMonth, wEndDay (16 bytes)
□□□□ □□□□ //wStartHour, wStartMinute (20 bytes)
□□□□□□□□ //MinutesDuration (24 bytes)
□□□□□□□□ //MinutesInterval (28 bytes)
□□□□□□□□ //rgFlags (32 bytes)
□□□□□□□□ //TriggerType (36 bytes)
□□□□□□□□ //Type_ (40 bytes)
□□□□ □□□□ //Reserved2 wRandomMinutesInterval (44 bytes)
和TriggerType
之间有额外的4个字节填充:
Type_
好的,如果这就是德尔福5想要做的事情,那我就是谁来争论。它确实比我更了解Windows结构包装。
我检查布局的方法是在记录中放置已知的哨兵值:
□□□□ □□□□ //cbTriggerSize, Reserved1 (4 bytes)
□□□□ □□□□ //wBeginYear, wBeginMonth (8 bytes)
□□□□ □□□□ //wBeginDay, wEndYear (12 bytes)
□□□□ □□□□ //wEndMonth, wEndDay (16 bytes)
□□□□ □□□□ //wStartHour, wStartMinute (20 bytes)
□□□□□□□□ //MinutesDuration (24 bytes)
□□□□□□□□ //MinutesInterval (28 bytes)
□□□□□□□□ //rgFlags (32 bytes)
□□□□□□□□ //TriggerType (36 bytes)
□□□□□□□□ //4 bytes padding
□□□□□□□□ //Type_ (44 bytes)
□□□□ □□□□ //Reserved2 wRandomMinutesInterval (48 bytes)
在CPU窗口中查看生成的内存布局:
(交替成员为红色和绿色,红色为填充);
在Delphi 5中总计trigger.cbTriggerSize := $1111; // WORD;
trigger.Reserved1 := $2222; // WORD;
trigger.wBeginYear := $3333; // WORD;
trigger.wBeginMonth := $4444; // WORD;
trigger.wBeginDay := $5555; // WORD;
trigger.wEndYear := $6666; // WORD;
trigger.wEndMonth := $7777; // WORD;
trigger.wEndDay := $8888; // WORD;
trigger.wStartHour := $9999; // WORD;
trigger.wStartMinute := $aaaa; // WORD;
trigger.MinutesDuration := $bbbbbbbb; // DWORD;
trigger.MinutesInterval := $cccccccc; // DWORD;
trigger.rgFlags := $dddddddd; // DWORD;
trigger.TriggerType := TASK_TIME_TRIGGER_DAILY; // TTaskTriggerType;
trigger.Type_.Daily.DaysInterval := $ffff; // TTriggerTypeUnion;
trigger.Reserved2 := $1111; // WORD;
trigger.wRandomMinutesInterval := $2222; // WORD;
。
当我在Delphi XE6中进行相同的测试时,它的打包方式不同(并且可怕):
首先,它无法管理在32位边界上分配堆栈变量;但那没关系。
CPU窗口拒绝在结构上准确地启动视图 - 坚持它开始在DWORD边界上显示内存;但那没关系。
该记录确实未与48 bytes
对齐:
所以我们将继续这样做。
$18EB31
这是设计上的怪物还是编译器代码错误?
不确定。
□□□□ □□□□ //cbTriggerSize, Reserved1 (4 bytes)
□□□□ □□□□ //wBeginYear, wBeginMonth (8 bytes)
□□□□ □□□□ //wBeginDay, wEndYear (12 bytes)
□□□□ □□□□ //wEndMonth, wEndDay (16 bytes)
□□□□ □□□□ //wStartHour, wStartMinute (20 bytes)
□□□□□□□□ //MinutesDuration (24 bytes)
□□□□□□□□ //MinutesInterval (28 bytes)
□□□□□□□□ //rgFlags (32 bytes)
□□ □□□□ □□ //TriggerType, Type_, 1 byte padding (36 bytes)
□□□□□□□□ //4 bytes padding (40 bytes)
□□□□□□ □□ //3 bytes padding, part of Reserved2 (44 bytes)
□□ □□□□ //Remainnder of Reserved2, wRandomMinutesInterval (47 bytes)
失败。
好。
sizeof(TASK_TRIGGER) = 52
失败。
TOUCHE。
sizeof(TASK_TRIGGER) = 50
失败。
这几乎就像Delphi不相信它是Windows编译器一样。
sizeof(TASK_TRIGGER) = 52
我会耐心地添加更多内容。
答案 0 :(得分:3)
正确的布局如下:
00-01 cbTriggerSize: WORD;
02-03 Reserved1: WORD;
04-05 wBeginYear: WORD;
06-07 wBeginMonth: WORD;
08-09 wBeginDay: WORD;
10-11 wEndYear: WORD;
12-13 wEndMonth: WORD;
14-15 EndDay: WORD;
16-17 wStartHour: WORD;
18-19 wStartMinute: WORD;
20-23 MinutesDuration: DWORD;
24-27 MinutesInterval: DWORD;
28-31 rgFlags: DWORD;
32-35 TriggerType: TTaskTriggerType;
36-43 Type_: TTriggerTypeUnion;
44-45 Reserved2: WORD;
46-47 wRandomMinutesInterval: WORD;
让我们逐点理解:
int
的C enum。再次,对齐4,不需要填充。 MONTHLYDATE
,由于对齐,其大小为8。假设C头文件指定了对齐的结构,您需要使用{$MINENUMSIZE 4}
和{$ALIGN ON}
进行编译。
您省略的详细信息是JEDI单元的编译器选项以及枚举和联合的声明。看一下github仓库中的单位,我看到{$MINENUMSIZE 4}
和{$ALIGN ON}
这是好的。枚举是一个简单的Delphi枚举类型。也不错。但是工会和它所包含的记录都是包装好的。这是错误的,导致联合的大小错误。
我也从JEDI来源看到了这一点:
_TASK_TRIGGER = record
// SP: removed packed record statement as seemed to affect SetTrigger
这个单位的作者似乎对包装和对齐有点困惑。
XE6如何认为这可能是47字节超出了我。尤其是因为我无法看到所有细节,因为不幸的是,问题省略了一些。在任何情况下,您确实需要枚举大小为4,并对齐记录,因此47数据点可能不是关键数据点。我建议我们忽略它。
适当的XE6数据点是{$MINENUMSIZE 4}
和{$ALIGN ON}
大小为52的情况。这里我们看到由于一些不可理解的原因,联合消耗了12个字节。我假设您的联盟符合JEDI github repo中的声明。是吗?
从您提供的事实来看,这对我来说就像是一个Delphi XE6编译器错误。旧的德尔菲版本在对齐结构方面非常差。我认为现代版本是正确的,但也许不是。但是,可能会混淆所有这些是您正在使用的标题转换。当然,它似乎对包装感到困惑。我们还没有看到你的所有代码。我只看过最新的github。也许问题出在那里而不是编译器。 @LURD的研究表明,XE6编译器正确地列出了结构。
你应该处理这样的问题的方法是使用MS编译器转到原始头文件。包括标题并使用sizeof
和offsetof
转储布局。从马的嘴里可以这么说。
然后对您的Delphi编译器执行相同的操作并比较布局。代替C ++ offsetof
使用我在此处显示的技巧:Can we implement ANSI C's `offsetof` in Delphi?
至于如何继续,一旦你知道正确的布局,它应该很容易说服编译器以相同的方式打印记录。从github repo中的JEDI代码开始,删除packed的所有用法。尝试大小。如果这不起作用,请调查联合的布局。作为最后的手段,你可以手动包装所有东西和垫。如果问题确实在那里,或许与工会这样做就足够了。
更新: LURD的答案似乎表明,删除联合中包含的使用和包含的结构会给出正确的布局。
注意:我没有任何编译器方便所以上面的所有内容都来自我的头脑。我可能在具体细节上犯了错误。但是,我相信使用MS编译器向您展示正确布局的一般建议是解决Win32结构布局的所有疑问的一般建议。有了这个工具,您就可以解决任何这种性质的问题。
答案 1 :(得分:2)
按照David的回答,{$ ALIGN ON} {$ MINENUMSIZE 4}并删除打包声明,
program Project8;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.SysUtils,Windows;
{$ALIGN ON}
{$MINENUMSIZE 4}
Type
{$EXTERNALSYM _TASK_TRIGGER_TYPE}
_TASK_TRIGGER_TYPE = (
{$EXTERNALSYM TASK_TIME_TRIGGER_ONCE}
TASK_TIME_TRIGGER_ONCE, // 0 // Ignore the Type field.
{$EXTERNALSYM TASK_TIME_TRIGGER_DAILY}
TASK_TIME_TRIGGER_DAILY, // 1 // Use DAILY
{$EXTERNALSYM TASK_TIME_TRIGGER_WEEKLY}
TASK_TIME_TRIGGER_WEEKLY, // 2 // Use WEEKLY
{$EXTERNALSYM TASK_TIME_TRIGGER_MONTHLYDATE}
TASK_TIME_TRIGGER_MONTHLYDATE, // 3 // Use MONTHLYDATE
{$EXTERNALSYM TASK_TIME_TRIGGER_MONTHLYDOW}
TASK_TIME_TRIGGER_MONTHLYDOW, // 4 // Use MONTHLYDOW
{$EXTERNALSYM TASK_EVENT_TRIGGER_ON_IDLE}
TASK_EVENT_TRIGGER_ON_IDLE, // 5 // Ignore the Type field.
{$EXTERNALSYM TASK_EVENT_TRIGGER_AT_SYSTEMSTART}
TASK_EVENT_TRIGGER_AT_SYSTEMSTART, // 6 // Ignore the Type field.
{$EXTERNALSYM TASK_EVENT_TRIGGER_AT_LOGON}
TASK_EVENT_TRIGGER_AT_LOGON // 7 // Ignore the Type field.
);
{$EXTERNALSYM TASK_TRIGGER_TYPE}
TASK_TRIGGER_TYPE = _TASK_TRIGGER_TYPE;
TTaskTriggerType = _TASK_TRIGGER_TYPE;
{$EXTERNALSYM PTASK_TRIGGER_TYPE}
PTASK_TRIGGER_TYPE = ^_TASK_TRIGGER_TYPE;
PTaskTriggerType = ^_TASK_TRIGGER_TYPE;
type
{$EXTERNALSYM _DAILY}
_DAILY = {packed} record
DaysInterval: WORD;
end;
{$EXTERNALSYM DAILY}
DAILY = _DAILY;
TDaily = _DAILY;
type
{$EXTERNALSYM _WEEKLY}
_WEEKLY = {packed} record
WeeksInterval: WORD;
rgfDaysOfTheWeek: WORD;
end;
{$EXTERNALSYM WEEKLY}
WEEKLY = _WEEKLY;
TWeekly = _WEEKLY;
type
{$EXTERNALSYM _MONTHLYDATE}
_MONTHLYDATE = {packed} record
rgfDays: DWORD;
rgfMonths: WORD;
end;
{$EXTERNALSYM MONTHLYDATE}
MONTHLYDATE = _MONTHLYDATE;
TMonthlyDate = _MONTHLYDATE; // OS: Changed capitalization
type
{$EXTERNALSYM _MONTHLYDOW}
_MONTHLYDOW = {packed} record
wWhichWeek: WORD;
rgfDaysOfTheWeek: WORD;
rgfMonths: WORD;
end;
{$EXTERNALSYM MONTHLYDOW}
MONTHLYDOW = _MONTHLYDOW;
TMonthlyDOW = _MONTHLYDOW; // OS: Changed capitalization
{$EXTERNALSYM _TRIGGER_TYPE_UNION}
_TRIGGER_TYPE_UNION = {packed} record
case Integer of
0: (Daily: DAILY);
1: (Weekly: WEEKLY);
2: (MonthlyDate: MONTHLYDATE);
3: (MonthlyDOW: MONTHLYDOW);
end;
{$EXTERNALSYM TRIGGER_TYPE_UNION}
TRIGGER_TYPE_UNION = _TRIGGER_TYPE_UNION;
TTriggerTypeUnion = _TRIGGER_TYPE_UNION;
_TASK_TRIGGER = record
cbTriggerSize : WORD;
Reserved1 : WORD;
wBeginYear : WORD;
wBeginMonth : WORD;
wBeginDay : WORD;
wEndYear : WORD;
wEndMonth : WORD;
wEndDay : WORD;
wStartHour : WORD;
wStartMinute : WORD;
MinutesDuration : DWORD;
MinutesInterval : DWORD;
rgFlags : DWORD;
TriggerType : TTaskTriggerType;
Type_ : TTriggerTypeUnion;
Reserved2 : WORD;
wRandomMinutesInterval : WORD;
end;
PTaskTrigger =^_TASK_TRIGGER;
const
PTrigger : PTaskTrigger = Nil;
begin
WriteLn(SizeOf(_TASK_TRIGGER));
WriteLn(Integer(@PTrigger^.cbTriggerSize));
WriteLn(Integer(@PTrigger^.Reserved1));
WriteLn(Integer(@PTrigger^.wBeginYear));
WriteLn(Integer(@PTrigger^.wBeginMonth));
WriteLn(Integer(@PTrigger^.wBeginDay));
WriteLn(Integer(@PTrigger^.wEndYear));
WriteLn(Integer(@PTrigger^.wEndMonth));
WriteLn(Integer(@PTrigger^.wEndDay));
WriteLn(Integer(@PTrigger^.wStartHour));
WriteLn(Integer(@PTrigger^.wStartMinute));
WriteLn(Integer(@PTrigger^.MinutesDuration ));
WriteLn(Integer(@PTrigger^.MinutesInterval ));
WriteLn(Integer(@PTrigger^.rgFlags ));
WriteLn(Integer(@PTrigger^.TriggerType ));
WriteLn(Integer(@PTrigger^.Type_ ));
WriteLn(Integer(@PTrigger^.Reserved2 ));
WriteLn(Integer(@PTrigger^.wRandomMinutesInterval ));
ReadLn;
end.
结果:
48
0
2
4
6
8
10
12
14
16
18
20
24
28
32
36
44
46
因此XE6中的大小也是48。
<强>更新强>
恢复打包声明后,结果仍为48。 但布局不同:
48
0
2
4
6
8
10
12
14
16
18
20
24
28
32
36
42
44
自D2010扩展RTTI以来,可以制作一个通用程序来列出记录中的所有偏移量。如果有人有兴趣,请在这里展示一个例子。
procedure ListRecordFieldsOffset( ARecTp: PTypeInfo; const AList: TStrings);
// Uses Classes,RTTI,TypInfo
// Example call: ListRecordFieldsOffset(TypeInfo(TMyRec),MyList);
var
AContext : TRttiContext;
AField : TRttiField;
begin
if Assigned(ARecTp) and (ARecTp^.Kind = tkRecord) and Assigned(AList) then
begin
AList.BeginUpdate;
for AField in AContext.GetType(ARecTp).GetFields do
begin
AList.Add(AField.Name + ': ' + AField.FieldType.ToString + ' = ' +
IntToStr(AField.Offset));
end;
AList.EndUpdate;
end;
end;