Search code examples
delphidelphi-xe6

How to align the TASK_TRIGGER record correctly under XE6?


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:

  • Delphi 5: SizeOf(TASK_TRIGGER) = 48
  • Delphi XE6 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.

The layouts

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:

enter image description here

(alternating members red and green, red is the padding);

For a total of 48 bytes in Delphi 5.

Enter XE6

When i do the same test in Delphi XE6, it is packed differently (and terrifyingly):

enter image description here

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:

enter image description here

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?

Did you try {$ALIGN ON}?

Sure.

sizeof(TASK_TRIGGER) = 52

enter image description here

Fails.

What about {$MINENUMSIZE 4}?

Okay.

sizeof(TASK_TRIGGER) = 50

enter image description here

Fails.

Yeah, but did you try both together?

Touche.

sizeof(TASK_TRIGGER) = 52

enter image description here

Fails.

It's almost like Delphi refuses to believe that it is Windows compiler.

Summary

$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.


Solution

  • 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:

    • The first 10 words sit together with no padding, all naturally aligned.
    • Then the 3 double words, again no padding needed to align on 4 byte boundaries.
    • Next the C enum which is really an int. Again, alignment of 4, no padding needed.
    • Now the union. A union of structs, the largest of which is MONTHLYDATE which, due to alignment, has size 8.
    • Two more words which fit at their natural offsets with no padding.

    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.