I am porting TaskSchedule API related code from Delphi 5 to Delphi XE6. I'm having an issue with structure alignment and sizeof
.
The actual TASK_TRIGGER
structure is declared as:
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
The old translation of MsTask.pas
i'm using (and current JCL transactions of MsTask) translate it as:
_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;
The sizeof
this record differs between Delphi 5 and XE6:
SizeOf(TASK_TRIGGER) = 48
SizeOf(TASK_TRIGGER) = 47
The function call of ITaskTrigger.SetTrigger(TASK_TRIGGER)
succeeds in Dephi5, but fails with Delphi XE6 with The parameters are incorrect
.
If i were to naively guess the layout of the record, i would it is:
□□□□ □□□□ //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)
But when i actually examine the populated structure inside the Delphi 5 debugger, the actual structure is 48 bytes, with extra 4 bytes padding between TriggerType
and Type_
:
□□□□ □□□□ //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)
Ok, if that's how Delphi 5 wants to do it who am i to argue. It certainly knows more about Windows structure packing than i do.
The way i examined the layout was to place known sentinel values in the record:
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;
And look at the resulting memory layout in the CPU window:
(alternating members red and green, red is the padding);
For a total of 48 bytes
in Delphi 5.
When i do the same test in Delphi XE6, it is packed differently (and terrifyingly):
First, it couldn't manage to allocate a stack variable the on 32-bit boundary; but that's fine.
The CPU window refused to start the view exactly on the structure - insisting it start showing memory on a DWORD boundary; but that's fine.
The record really is not aligned at $18EB31
:
so we'll go with that.
□□□□ □□□□ //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)
Is this monstrosity by design, or a compiler code-gen bug?
Sure.
sizeof(TASK_TRIGGER) = 52
Fails.
Okay.
sizeof(TASK_TRIGGER) = 50
Fails.
Touche.
sizeof(TASK_TRIGGER) = 52
Fails.
It's almost like Delphi refuses to believe that it is Windows compiler.
$ALIGN $MINENUMSIZE $OLDTYPELAYOUT "packed" | sizeof
====== ============ ================ ======== ======
ON 4 ON yes 57
ON 4 OFF yes 50
ON 4 ON no 52
ON 4 OFF no 52
ON 2 ON yes 50
ON 2 OFF yes 50
ON 2 ON no 52
ON 2 OFF no 52
ON ON yes 49
ON OFF yes 49
ON ON no 52
ON OFF no 52
i'll add more as i get the patience.
The correct layout is as follows:
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;
Let's work through this point by point:
int
. Again, alignment of 4, no padding needed. MONTHLYDATE
which, due to alignment, has size 8. Assuming that the C header file specifies aligned structs you need to compile this with {$MINENUMSIZE 4}
and {$ALIGN ON}
.
Details that you omitted are the compiler options for the JEDI unit and the declaration of the enum and the union. Looking at the unit in the github repo, I see {$MINENUMSIZE 4}
and {$ALIGN ON}
which is good. And the enum is a plain Delphi enumerated type. Also good. But the union and the records it contains are all packed. That's wrong and results in the union being the wrong size.
And I also see this from the JEDI source:
_TASK_TRIGGER = record
// SP: removed packed record statement as seemed to affect SetTrigger
It seems that the authors of this unit have been a little confused over packing and alignment.
How XE6 thinks this can be 47 bytes is beyond me. Not least because I can't see all the details because unfortunately the question omitted some. In any case, you really do need to have an enum size of 4, and align the records, so the 47 data point is perhaps not the critical one. I propose we ignore it.
The appropriate XE6 data point is the {$MINENUMSIZE 4}
and {$ALIGN ON}
case with size of 52. Here we see that the union consumes 12 bytes for some incomprehensible reason. I assume your union is as per the declaration in the JEDI github repo. Is it?
On the face of the facts you present, this smells like a Delphi XE6 compiler bug to me. Old Delphi versions were notoriously poor at aligning structures. I thought modern versions got it right, but perhaps not. However, possibly confounding all of this is the header translation you are using. Certainly it seems confused over packing. And we can't yet see all of your code. I've only seen the latest in github. Perhaps the problem is there rather than with the compiler. And @LURD's investigations suggest that the XE6 compiler lays out the struct correctly.
The way you should deal with issues like this is to go to the original header file, with the MS compiler. Include the header and dump the layout using sizeof
and offsetof
. From the horse's mouth so to speak.
Then do the same with your Delphi compiler and compare layouts. In place of C++ offsetof
use the trick that I show here: Can we implement ANSI C's `offsetof` in Delphi?
As for how you proceed, once you know the correct layout, it should be easy enough to persuade the compiler to lay the record out the same way. Start from the JEDI code in the github repo and remove all uses of packed. Try that for size. If that doesn't work, investigate the layout of the union. As a last resort you can pack everything and pad manually. Perhaps doing so with the union would suffice, if indeed the problem lies there.
Update: LURD's answer seems to show that removing the use of packed in the union and contained structs gives the correct layout.
Note: I don't have any compilers handy so all the above is generated from my head. I may have erred in the specifics. However the general advice of using the MS compiler to show you the correct layout is, I believe, the general piece of advice that resolves all doubt over Win32 struct layout. With that tool at hand you can solve any problem of this nature.