2

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.

Ian Boyd
  • 246,734
  • 253
  • 869
  • 1,219
  • [Old and new packed alignment to fields](http://docwiki.embarcadero.com/RADStudio/XE3/en/Internal_Data_Formats#Implicit_Packing_of_Fields_with_a_Common_Type_Specification) – Abelisto Aug 29 '14 at 22:48
  • Get a C compiler and look at the layout. Use and `offsetof()` macro. – David Heffernan Aug 30 '14 at 02:17
  • @DavidHeffernan I have a functional compiler here (Delphi 5). I just need Delphi XE6 to create structures compatible with the Windows ABI. – Ian Boyd Aug 30 '14 at 02:24
  • @Ian D5 sucks at struct layout. Seriously get a real compiler! One that understands the windows header files. Get your info from the horses mouth. – David Heffernan Aug 30 '14 at 02:50
  • The first screenshot doesn't match the sentinels. If you want to force alignment use heap alloc. Since the size is 47 (!) the alignment must be 1 and so stack location reasonable, modulo bogus size. sizeof and offetof are really the way to inspect this as per my answer. – David Heffernan Aug 30 '14 at 02:56
  • @DavidHeffernan Yes, but Delphi 5 **gets it right**. It actually **works** in Delphi 5. And yes, i changed the sentinels halfway through. I'd rather not have to regenerate all the screenshots just to avoid a nitpick. – Ian Boyd Aug 30 '14 at 15:32
  • In this case it does. But it has a track record of failure. Whereas the ms compiler is the definitive source. Seems that the problems with your xe6 code are not compiler problems. – David Heffernan Aug 30 '14 at 15:33
  • I don't understand the table you added. You need to remove all the packed and use `{$MINENUMSIZE 4}` and `{$ALIGN ON}` like I said. Documenting all the wrong ways to do it seems to add nothing. Surely the mystery has been solved? – David Heffernan Aug 30 '14 at 15:51
  • @DavidHeffernan It is mostly *"research effort"*. Research effort adds nothing to the question, but at least it shows i'm trying. – Ian Boyd Aug 30 '14 at 16:00
  • 1
    @Ian We can already see that ;-) – David Heffernan Aug 30 '14 at 16:07

2 Answers2

3

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.

Community
  • 1
  • 1
David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • I provided a small example of your conclusions, size is 48 in XE6 as well. – LU RD Aug 30 '14 at 07:34
  • Turns out it was a combination of things conspiring against me. The first was the strange packing. I tried to fix that with manual padding. Then i tried adding `oldtypelayout`. Then i tried combinations of `align` and `minenumsize`. Not knowing if Windows ABI defines enums as 4 or 8 bytes, or alignment spacing, i was about to delve into `{$Axx}`. But copying @LURD code to spit out structure offsets pointed me to the problem: my manual padding was still in there! I now see all `WinApi.xxx` files are `{$ALIGN ON} {$MINENUMSIZE 4}`. It simply has to be added to all Windows structures. – Ian Boyd Aug 30 '14 at 16:07
  • i suppose i was initially distracted, and terrified, by the default non-cache-aligned, non-DWORD aligned, struture layout used by XE6. – Ian Boyd Aug 30 '14 at 16:09
  • Yes, align structs, enums are ints. There are exceptions though. Lots of native api structs are packed. And there are other oddities. Hence using ms compiler with header file to be sure. – David Heffernan Aug 30 '14 at 16:23
  • *default non-cache-aligned, non-DWORD aligned, struture layout* It's important to know about size and alignment. An aligned struct always places members on multiples of their alignment. And structures have alignment determined by the alignment of their largest member. But when you pack a structure in delphi that affects layout **and** alignment. A packed structure has alignment of 1. Hence the compiler feels no compulsion to place it at a special byte boundary. – David Heffernan Aug 30 '14 at 16:26
2

Following David's answer, {$ALIGN ON} {$MINENUMSIZE 4} and removing the packed declarations,

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.

Results in:

48
0
2
4
6
8
10
12
14
16
18
20
24
28
32
36
44
46

So the size is 48 in XE6 as well.

Update

With the packed declarations restored, the result is still 48. But the layout is different:

48
0
2
4
6
8
10
12
14
16
18
20
24
28
32
36
42
44

Since D2010 with extended RTTI its is possible to make a generic procedure to list all offsets in a record. Just showing an example here if anyone is interested.

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;
LU RD
  • 34,438
  • 5
  • 88
  • 296
  • +1 thanks. And it's 52 with packed on the union and contained structs? – David Heffernan Aug 30 '14 at 07:33
  • @DavidHeffernan, with all packed structs and union restored, it's still 48. Hmm, could have sworn I had something else before I changed. – LU RD Aug 30 '14 at 07:42
  • I'd expect 46. There would be two bytes padding at the end of MONTHLYDATE missing. – David Heffernan Aug 30 '14 at 07:47
  • Did you restore all the packed declarations? One one the union and four on the contained types? – David Heffernan Aug 30 '14 at 07:59
  • @DavidHeffernan, yes. I even recopied from the JCL source and restarted the compiler. – LU RD Aug 30 '14 at 08:01
  • I really don't understand it because the alignment of a packed structure is 1, AFAIK. Anyway, it's moot. The real point is that we've demonstrated how to troubleshoot these issues. If you don't beat me to it (currently travelling), then I'd add a C variant to display layout of the structures. – David Heffernan Aug 30 '14 at 08:33
  • @DavidHeffernan, see my last update, I missed that the layout is different with the packed keyword. – LU RD Aug 30 '14 at 09:36
  • Ok. I get it. Two bytes padding at the end to achieve 4 byte alignment for the dwords. – David Heffernan Aug 30 '14 at 11:30