Friday, November 04, 2011
Wednesday, October 17, 2007
More fun with Enumerators
As part of the new language syntax inherited from Delphi.NET, native Delphi now (since Delphi 2005) supports for-in loops (known as foreach in C#). The new syntax is easy to read, and it reduces the clutter of maintaining a loop index variable, checking boundary conditions (typically 0 and Count-1) and indexing into the array or list.
While Delphi has special built-in support for for-in for (got it? ;) arrays, strings, and set types, the RTL and VCL also implements support for for-in by implementing an enumerator pattern. You can do the same with your own collection classes. There are a number of ways to implement such enumerators. We will look at a couple of variants, studying the code that the compiler generates for them. We will mainly focus on getting as efficient code as possible.
The Enumerator Pattern
Primoz Gabrijelcic has already posted several excellent blog posts about how to write enumerators in Delphi - please read them first if you haven't already.
Basically, when writing support for the for-in loop in one of your own classes or records (you can have records with methods these days, you know) you need to provide a single function called GetEnumerator. This function needs to return an instance of a class or a record that needs to have a public MoveNext function and a public Current property.
Here is a complete example that closely mimics the way TList implements its enumerator.
type
TMyObject = class(TObject)
procedure Foo;
end;
TMyList = class;
TMyListEnumerator = class
private
FIndex: Integer;
FMyList: TMyList;
public
constructor Create(AMyList: TMyList);
function MoveNext: Boolean;
function GetCurrent: TMyObject;
property Current: TMyObject read GetCurrent;
end;
TMyList = class(TList)
public
procedure Add(AMyObject: TMyObject);
function GetEnumerator: TMyListEnumerator;
end;
implementation
{ TMyObject }
procedure TMyObject.Foo;
begin
end;
{ TMyList }
procedure TMyList.Add(AMyObject: TMyObject);
begin
inherited Add(AMyObject);
end;
function TMyList.GetEnumerator: TMyListEnumerator;
begin
Result := TMyListEnumerator.Create(Self);
end;
{ TMyListEnumerator }
constructor TMyListEnumerator.Create(AMyList: TMyList);
begin
inherited Create;
FIndex := -1;
FMyList := AMyList;
end;
function TMyListEnumerator.GetCurrent: TMyObject;
begin
Result := FMyList.List[FIndex];
end;
function TMyListEnumerator.MoveNext: Boolean;
begin
Result := FIndex < FMyList.Count - 1;
if Result then
Inc(FIndex);
end;
With this code in place we can now create instances of TMyList and run the shiny new for-in loops on them.
procedure Test;
var
MyList: TMyList;
MyObject: TMyObject;
begin
MyList := TMyList.Create;
for MyObject in MyList do
MyObject.Foo;
end;
The Generated Code
This is all pretty straight-forward code, but let's look one level deeper and look at the assembly code that the compiler generates from this code. This is as easy as setting a break-point on the for-in loop, hitting run (F5) and then press Ctrl+Alt+C to open the CPU window. The code we see looks like this (from Delphi 2007).
for MyObject in MyList do
call TMyList.GetEnumerator
mov [ebp-$04],eax
xor eax,eax
push ebp
push $0040ff8c
push dword ptr fs:[eax]
mov fs:[eax],esp
jmp $0040ff62
mov eax,[ebp-$04]
call TMyListEnumerator.GetCurrent
mov ebx,eax
MyObject.Foo;
mov eax,ebx
call TMyObject.Foo
for MyObject in MyList do
mov eax,[ebp-$04]
call TMyListEnumerator.MoveNext
test al,al
jnz $0040ff51
xor eax,eax
pop edx
pop ecx
pop ecx
mov fs:[eax],edx
push $0040ff93
MyObject.Foo;
cmp dword ptr [ebp-$04],$00
jz $0040ff8b
mov dl,$01
mov eax,[ebp-$04]
call TObject.Destroy
ret
jmp @HandleFinally
jmp $0040ff7b
Wow! That sure was a lot of machine code from two innocent looking lines of Pascal!
Without digging into the semantics of each assembly instruction, we can quickly see that there are calls to four methods (GetEnumerator, GetCurrent, Foo and MoveNext) and one destructor (TObject.Destroy). There are also some magic gyrations involving FS: [xx] segment overrrides and a jump instruction to HandleFinally - this is the hallmark of a try-finally implementation.
If we try and expand the for-in loop into Pascal code, it would look something like this.
procedure TestImpl;
var
MyList: TMyList;
MyObject: TMyObject;
MyListEnumerator: TMyListEnumerator;
begin
MyList := TMyList.Create;
MyListEnumerator := MyList.GetEnumerator;
try
while MyListEnumerator.MoveNext do
begin
MyObject := MyListEnumerator.Current;
MyObject.Foo;
end;
finally
if Assigned(MyListEnumerator) then
MyListEnumerator.Destroy;
end;
end;
Is all of this overhead really necessary? Not really. While all of the enumerators in the Delphi RTL and VCL (and even in Primoz' sample code) are implemented as classes (forcing the the compiler to implicitly free the enumerator instance after the loop), there is no rule that says that all enumerators must be implemented by classes.
Enumerator Records
No, light-weight records with methods can implement enumerators, too - and they are allocated directly on the stack and has no need for calling a destructor. So changing the enumerator from a class to a record will make the code smaller and faster. Most enumerators can be favorably be implemented as records. In fact, all the existing enumerators in the Delphi RTL and VCL could be easily re-implemented as records - producing smaller and faster code for all for-in loops that use them. I can see only two reasons to use classes for enumerators; if the class needs to free something in an overridden destructor, or if the class needs to inherit functionality from another class. In all other cases, enumerators should be implemented as records.
Another observation is that the implementations for GetCurrent and MoveNext (and even GetEnumerator) are typically very short and thus perfect candidates for inlining. The two first methods will be called once for every item iterated over in the collection, so it makes sense to inline them at the call site.
So, lets change the sample code above with these two optimizations. To better see the effect on the generated code, we'll do one change at the time. First we replace the class with a record - yielding the following simple changes in the code.
type
TMyListEnumerator = record
private
FIndex: Integer;
FMyList: TMyList;
public
constructor Create(AMyList: TMyList);
function MoveNext: Boolean;
function GetCurrent: TMyObject;
property Current: TMyObject read GetCurrent;
end;
constructor TMyListEnumerator.Create(AMyList: TMyList);
begin
// inherited Create;
FIndex := -1;
FMyList := AMyList;
end;
The only changes we did was to change "class" to "record" and comment out the call to the inherited constructor (as records do not inherit anything - and cannot have parameterless constructors). Without the need to free the enumerator instance, the code the compiler generates for the for-in loop is now much simpler.
for MyObject in MyList do
mov edx,esp
mov eax,ebx
call TMyList.GetEnumerator
jmp $0041010d
mov eax,esp
call TMyListEnumerator.GetCurrent
mov ebx,eax
MyObject.Foo;
mov eax,ebx
call TMyObject.Foo
for MyObject in MyList do
mov eax,esp
call TMyListEnumerator.MoveNext
test al,al
jnz $004100fd
We can still see the three method calls to GetEnumerator, GetCurrent, Foo and MoveNext, but the heavy try-finally code and the Destroy call are gone. If we try to write this in Pascal again, it would be something like this.
procedure TestImpl;
var
MyList: TMyList;
MyObject: TMyObject;
MyListEnumerator: TMyListEnumerator;
begin
MyList := TMyList.Create;
MyListEnumerator := MyList.GetEnumerator;
while MyListEnumerator.MoveNext do
begin
MyObject := MyListEnumerator.Current;
MyObject.Foo;
end;
end;
That is much better, don't you think!? ;)
Inlining the Loop Calls
While the class-to-record optimization gave some nice code size savings, for long running loops it doesn't really affect the overall running time, because it only affects what happens before and after the loop.
The next step is to turn on inlining the the small and simple enumerator functions. This is as easy as adding the inline directive to the declaration of the methods, like this.
TMyListEnumerator = record
private
FIndex: Integer;
FMyList: TMyList;
public
constructor Create(AMyList: TMyList);
function MoveNext: Boolean; inline;
function GetCurrent: TMyObject; inline;
property Current: TMyObject read GetCurrent;
end;
Here we have just added "inline;" to the MoveNext and GetCurrent methods. Recompiling, running and hitting the break-point again, we can inspect the assembly code once again.
for MyObject in MyList do
mov edx,esi
mov eax,ebx
call TMyList.GetEnumerator
jmp $00410222
mov eax,[esi+$04]
mov eax,[eax+$04]
mov edx,[esi]
mov ebx,[eax+edx*4]
MyObject.Foo;
mov eax,ebx
call TMyObject.Foo
for MyObject in MyList do
mov eax,esi
call TMyListEnumerator.MoveNext
test al,al
jnz $00410210
Notice that the GetCurrent call is now gone - instead the assembly instructions for its implementation have been merged into our loop. This is good as excessive branching inside a loop brings down performance. But notice that the MoveNext call is still there. For some reason the compiler is not heeding our inline directive for the MoveNext function.
While Expressions Not Inlined
As discussed in my DN4DP piece on Delphi inlining, the inline directive is just a hint that the compiler should try to inline the call if possible - there are a number of documented and undocumented cases where it will not be inlined. It looks like we have stumbled onto one of the undocumented cases here - the MoveNext function of an enumerator will not currently (D2007) be inlined in the code that the compiler generates for a for-in loop.
I've done some more testing and digging, and it seems like no functions are inlined if they appear inside a while loop control expression. Given some mock-up test code:
procedure Foo;
begin
end;
function Inlined(var I: integer): boolean; inline;
begin
Dec(I);
Result := I <> 0;
end;
We can now used this inlined function in a while loop.
procedure Test1;
var
I: integer;
begin
I := 100;
while Inlined(I) do
Foo;
end;
If the inlining worked we should see no call to the Inlined function in the generated assembly code.
I := 100;
mov [esp],$00000064
jmp $0040fdc7
Foo;
call Foo
while Inlined(I) do
mov eax,esp
call Inlined
test al,al
jnz $0040fdc2
Alas, there is clearly a "call Inlined" in there :-(.
Transforming a While Loop
Let's experiment a little. Testing shows that the Inlined routine is properly inlined for a simple if-test. It is possible to rewrite a while loop with an expression into a while-true loop with an if-statement on the negated expression and a Break. We can rewrite the non-inlining while loop in Test1 with a semantically equivalent loop.
procedure Test2;
var
I: integer;
begin
I := 100;
while True do
begin
if not Inlined(I) then
Break;
Foo;
end;
end;
Looking at the generated code again (we're really getting the hang of this, right?;)), we can see that the inlining does work just fine in this loop.
I := 100;
mov [esp],$00000064
if not Inlined(I) then
dec dword ptr [esp]
cmp dword ptr [esp],$00
setnz bl
test bl,bl
jz $0040fdf2
TestInlining.dpr.57: Foo;
call Foo
while True do
jmp $0040fddd
This shows that the compiler is able to inline loop control like this - it just needs a little assistance ;).
Quality Central Reports
CodeGear could fix this in two ways;
- Fix the compiler so that while-loop expressions can be inlined (this is the best solution). This should then automatically also inline the MoveNext call that the compiler generates for for-in loops.
- If this is hard or impossible for some reason, at least the for-in loop could be changed to generate a while-true loop with if not MoveNext then Break; logic. Thus would ensure that for-in loops can become more efficient than today.
While they are at it, they could also:
- Refactor all existing RTL and VCL enumerators from class to record
- Inline all GetCurrent and MoveNext functions in all enumerators
Yes, I know should probably log these issues in Quality Central. And I will - eventually ;).
Update: I have now taken the time to report these issues and suggestions in Quality Central. Note: I think the first QC report ended up as a Beta report and may thus not be publically viewable - but it seems CodeGear have noticed anyway ;).
QC#53623 - Function calls inside while expressions are not inlined
QC#53737 - For-in codegen: Enumerator MoveNext calls are not inlined
QC#53738 - Change all RTL and VCL enumerators from class to record
QC#53739 - Inline GetCurrent and MoveNext in all RTL and VCL enumerators
Posted by
Hallvards New Blog
at
Wednesday, October 17, 2007
17
comments
Friday, April 27, 2007
Psst, a special price for you, my friend...
An item that may interest you is available at eBay now, click here.
PS: I still have another Delphi 2007 licence.
Posted by
Hallvards New Blog
at
Friday, April 27, 2007
1 comments
Labels: D2007
Wednesday, March 21, 2007
Delphi 2007 ESD arrived - one gotcha
We ordered Delphi 2007 ESD from the local Norwegian CodeGear distributor alfacode.no a few days ago and earlier today the email with download link and licence keys arrived (the DVD will arrive in a couple of weeks, they say).
I downloaded the installer stub that then downloads and installs the required binaries - it all worked very smoothly. After the install was done Delphi 2007 ran fine. But I was a little worried for a while - because when I tried to start BDS 2006, I got an error dialog with the following unnerving message:
---------------------------
bds.exe - Entry Point Not Found
---------------------------
The procedure entry point @Uxtheme@BufferedPaintSetAlpha$qqruip11Types@TRectuc could not be located in the dynamic link library rtl100.bpl.
---------------------------
OK
---------------------------
After scratching my head for a while, I searched by harddisk for all rtl*.bpl files. Both Delphi 2007 and BDS 2006 use a binary called rtl100.bpl - and normally it resides in the Windows\System32 directory. The Delphi 2007 installer updates this (and most other VCL bpls) when it installs. The problem is that I had a second copy of this file in the BDS 2006 bin directory (called C:\Delphi2006\bin on my system). This was entirely my own fault because I had copied it there and patched it using Peter Vones tool to hack away startup time as I've blogged about before.
The reason this happens is that Delphi 2007 installer upgrades all the *100.bpls in the System32 directory, but BDS 2006 tries to load the old rtl100.bpl from the bds.exe startup directory that is not compatible with the new vcl100.bpls now residing in System32. The solution was simply to delete or rename the rtl100.bpl in the Delphi2006\bin directory.
Lesson learned: it is dangerous to live on the edge - and you better know (and remember) what you are doing.... ;).
Posted by
Hallvards New Blog
at
Wednesday, March 21, 2007
5
comments
Monday, March 19, 2007
The Delphi Geek: Delphi 2007 is here!
Primoz Gabrijelcic has created a summary with links to all blog posts about the Delphi 2007 beta: The Delphi Geek: Delphi 2007 is here!
Posted by
Hallvards New Blog
at
Monday, March 19, 2007
2
comments
Labels: D2007
Sunday, March 11, 2007
Review: Delphi 2007 for Win32 (Beta) - part three
Read the part one and two first.
What's new in the VCL
There are quite a few bugfixes in the VCL (as well as the RTL), but we'll not go into them in detail here. While remaining binary .dcu compatible, CodeGear has managed the feat of adding new functionality and even new properties on the existing TCustomForm class.
The GlassFrame property hack
In particular, all forms now have a GlassFrame property with sub-properties to control extended glass functionality when running on Windows Vista. Allen Bauer has blogged about this in his post "How to add a "published" property without breaking DCU compatibility". Please read his blog entry first - that will make it easier to understand the rest of this section.
First of all, what does the GlassFrame property contain and what does it do? Let's pretend that it had been implemented the "proper" way by adding a normal property to TCustomForm that is promoted to published in TForm. Then the code changes would look like this:
type
TCustomForm = class;
TGlassFrame = class(TPersistent)
public
constructor Create(Client: TCustomForm);
procedure Assign(Source: TPersistent); override;
function FrameExtended: Boolean;
function IntersectsControl(Control: TControl): Boolean;
property OnChange: TNotifyEvent {...};
published
property Enabled: Boolean {...} default False;
property Left: Integer {...} default 0;
property Top: Integer {...} default 0;
property Right: Integer {...} default 0;
property Bottom: Integer {...} default 0;
property SheetOfGlass: Boolean {...} default False;
end;
TCustomForm = class(TScrollingWinControl)
public
procedure UpdateGlassFrame(Sender: TObject);
property GlassFrame: TGlassFrame {...} ;
end;
TForm = class(TCustomForm)
published
property GlassFrame;
end;
For simplicity I've left out the private and protected implementation details. Note that the new GlassFrame property has the type TGlassFrame which is a TPersistent descendent. This means that its properties will appear in the Object Inspector as sub-properties of GlassFrame. The Left, Top, Right and Bottom properties defines how large the border of glass around the non-transparent part of the form should be. If you set SheetOfGlass to True, the entire form will be glass. Finally the Enabled property can be used to quickly turn the extra glass effect on or off. Note that the glass effect controlled by the GlassFrame property is in addition to the normal glassed non-client frame (caption and window border) provided by Vista. I don't have Vista running on my low-powered Acer laptop here, so you can take a peek at the screenshots over in Jeremy North's blogpost about the glass effect here and here to see how it looks at design-time and run-time.
The challenge that faced CodeGear in implementing the GlassFrame property and functionality in the non-breaking Delphi 2007 version was four-fold.
- The GlassFrame property needs to be available at runtime and appear to compiling code to reside on all form instances. This is solved using a class helper called TCustomFormHelper.
- Class helpers cannot add new fields or per-instance storage. Somehow the storage needs of the GlassFrame property needs to be satisfied. This is done by using a hack and reusing one of the existing TCustomForm private fields (FPixelsPerInch)
- While class helpers are great for creating a runtime mirage effect, "fooling" your code to see an injected property on an existing class, they do not help with RTTI and thus getting the property into the Object Inspector. As Allen explained in his article, in BDS 2006 they introduced a new selection editor interface called ISelectionPropertyFilter that makes it possible to dynamically add and remove design-time properties.
- Finally, the GlassFrame property needs to be streamed to and from the .dfm storage. This is not solved by the class helper alone, nor the property filter interface. The solution is to use the existing DefineProperties mechanism, but an extra twist is needed to support Property.SubProperty names for defined properties.
Lets dive into each of these issues in more detail. After all, this blog is mostly about hacks and the Delphi 2007 GlassFrame feature is arguably the most high-profile and best (?) hack in the VCL ever :-). Note: The source code I show here is for illustration only and is based on a beta build of Delphi 2007 - check your own copy of Forms.pas in the shipping version.
The CustomFormHelper class helper
Here is what the class helper that provides the runtime GlassFrame property looks like:
TCustomFormHelper = class helper for TCustomForm
private
function GetGlassFrame: TGlassFrame;
procedure ReadGlassFrameBottom(Reader: TReader);
procedure ReadGlassFrameEnabled(Reader: TReader);
procedure ReadGlassFrameLeft(Reader: TReader);
procedure ReadGlassFrameRight(Reader: TReader);
procedure ReadGlassFrameSheetOfGlass(Reader: TReader);
procedure ReadGlassFrameTop(Reader: TReader);
procedure SetGlassFrame(const Value: TGlassFrame);
procedure WriteGlassFrameBottom(Writer: TWriter);
procedure WriteGlassFrameEnabled(Writer: TWriter);
procedure WriteGlassFrameLeft(Writer: TWriter);
procedure WriteGlassFrameRight(Writer: TWriter);
procedure WriteGlassFrameSheetOfGlass(Writer: TWriter);
procedure WriteGlassFrameTop(Writer: TWriter);
public
procedure UpdateGlassFrame(Sender: TObject);
property GlassFrame: TGlassFrame read GetGlassFrame
write SetGlassFrame;
end;
This provides the public GlassFrame property that is transposed onto TCustomForm and all descendants. It also makes the UpdateGlassFrame method available, but this is mostly used internally in the Forms unit. It is the target of the OnChange event defined in the TGlassFrame class, forcing the form to repaint itself whenever one of the GlassFrame properties changes. Finally there are the ReadXXX and WriteXXX methods used in the TCustomForm.DefineProperties method to stream the GlassFrame properties to and from .dfm files. We'll discuss this in more detail below.
This is a virtual method defined on TPersistent. Luckily, TCustomForm already overrode this method in BDS 2006 (to store the pseudo properties PixelsPerInch, TextHeight and IgnoreFontProperty) so it was simple to add the new GlassFrame properties there.
The FPixelsPerInch storage hack
As you can see above the class helper does not have (and cannot have) and instance fields. Still the GlassFrame instance pointer has to be stored somewhere - and it needs to be stored per form instance. There are several different potential solutions to this. One possibility is to use some kind of hash-table to map form instances into corresponding GlassFrame instances, but this is complex, could be relatively slow and require extra coordination to make sure the GlassFrame instance is freed when the form is freed etc. The other solution is to stash the information into one of the existing fields of TCustomForm.
This is what CodeGear decided to do. They picked a relatively seldom used private field that does not have its address exposed via property accessors (you can read about why this would have been dangerous here).
type
TCustomForm = class(TScrollingWinControl)
private
FPixelsPerInch: Integer;
end;
The FPixelsPerInch field is declared as Integer, but in the implementation section it is actually treated as a pointer to a record structure.
{ Hack to overlay GlassFrame on PixelsPerInch in TCustomForm }
type
PPixelsPerInchOverload = ^TPixelsPerInchOverload;
TPixelsPerInchOverload = record
PixelsPerInch: Integer;
GlassFrame: TGlassFrame;
RefreshGlassFrame: Boolean;
end;
The record gives storage for both the PixelsPerInch property (declared on TCustomForm) the new GlassFrame property (injected by TCustomFormHelper) and another private implementation field called RefreshGlassFrame.
The TCustomForm constructor and destructor allocate and deallocate the FPixelsPerInch field as a TPixelsPerInchOverload pointer.
constructor TCustomForm.CreateNew(AOwner: TComponent; Dummy: Integer);
begin
Pointer(FPixelsPerInch) := AllocMem(SizeOf(TPixelsPerInchOverload));
inherited Create(AOwner);
//...
end;
destructor TCustomForm.Destroy;
begin
//...
FreeMem(Pointer(FPixelsPerInch));
inherited Destroy;
//...
end;
All access to the PixelsPerInch and RefreshGlassFrame fields in the TPixelsPerInchOverload record are delegated to a set of inlined getter and setter routines, such as this one:
function GetFPixelsPerInch(FPixelsPerInch: Integer): Integer; inline;
begin
Result := PPixelsPerInchOverload(FPixelsPerInch).PixelsPerInch;
end;
Finally the class helper methods can now use the same trick to get and store the GlassFrame property:
function TCustomFormHelper.GetGlassFrame: TGlassFrame;
begin
Result := PPixelsPerInchOverload(FPixelsPerInch).GlassFrame;
end;
procedure TCustomFormHelper.SetGlassFrame(const Value: TGlassFrame);
begin
PPixelsPerInchOverload(FPixelsPerInch).GlassFrame.Assign(Value);
end;
Neat hack, no? ;)
Injecting design-time properties
The next step is to convince the Object Inspector to make the GlassFrame compound property available for inspection and editing at design-time. This is achieved using a little known component selection interface called ISelectionPropertyFilter. This interface was first introduced in Delphi 2006 and used to help implement the ControlIndex property injected into all components dropped on a TFlowPanel or TGridPanel. These components are documented in a BDN article by Ed Vander Hoek here and the ISelectionPropertyFilter interface and how it is used is discussed by Tjipke A. van der Plaats here.
The interface is declared in the DesignIntf unit and looks like this:
{ ISelectionPropertyFilter
This optional interface is implemented on the same class that implements
ISelectionEditor. If this interface is implemented, when the property list
is constructed for a given selection, it is also passed through all the various
implementations of this interface on the selected selection editors. From here
the list of properties can be modified to add or remove properties from the list.
If properties are added, then it is the responsibility of the implementor to
properly construct an appropriate implementation of the IProperty interface.
Since an added "property" will typically *not* be available via the normal RTTI
mechanisms, it is the implementor's responsibility to make sure that the property
editor overrides those methods that would normally access the RTTI for the
selected objects.
FilterProperties
Once the list of properties has been gathered and before they are sent to the
Object Inspector, this method is called with the list of properties. You may
manupulate this list in any way you see fit, however, remember that another
selection editor *may* have already modified the list. You are not guaranteed
to have the original list. }
ISelectionPropertyFilter = interface
['{0B424EF6-2F2F-41AB-A082-831292FA91A5}']
procedure FilterProperties(const ASelection: IDesignerSelections;
const ASelectionProperties: IInterfaceList);
end;
There is no shipping source for it, but one of the IDE packages registers a selection editor for TCustomForm (and thus all descendants) using the DesignIntf.RegisterSelectionEditor routine.
type
{ TBaseSelectionEditor
All selection editors are assumed to derive from this class. A default
implemenation for the ISelectionEditor interface is provided in
TSelectionEditor class. }
TBaseSelectionEditor = class(TInterfacedObject)
public
constructor Create(const ADesigner: IDesigner); virtual;
end;
TSelectionEditorClass = class of TBaseSelectionEditor;
procedure RegisterSelectionEditor(AClass: TClass; AEditor: TSelectionEditorClass);
You can see some of the details of how the ISelectionPropertyFilter is used in the DesignEditors unit and the GetComponentProperties routine. This routine is called by the IDE when you select a form and it gets the list of registered selection editors for forms and each of the selection editor classes that implement the ISelectionPropertyFilter interface have their FilterProperties method called. This allows it to remove or add properties to the list that is eventually presented to the user in the Object Inspector. The new TCustomForm selection editor adds an implementation of IProperty for the new ghost property GlassFrame. The net result is that as far as the Object Inspector is concerned, the GlassFrame looks like a published property on TCustomForm, even though it isn't actually there and there is no RTTI for it. Feels like magic! ;).
Defining the streaming properties
The final piece of the puzzle to make the GlassFrame property illusion complete is to gel with the .dfm streaming system. It would help much having the GlassFrame property in the Object Inspector if the values you set didn't persist between runs of the IDE or when loading the form at runtime. The virtual DefineProperties method on TPersistent has always been part of the Delphi streaming system. You override it to store additional information than the published properties. Luckily TCustomForm already overrides DefineProperties (to store PixelsPerInch, TextHeight and IgnoreFontProperty). This means that additional code can be added to store the GlassFrame property without changing the interfaced part of TCustomForm. Here is the new DefineProperties method.
procedure TCustomForm.DefineProperties(Filer: TFiler);
//...
begin
inherited DefineProperties(Filer);
Filer.DefineProperty('PixelsPerInch', {...});
Filer.DefineProperty('TextHeight', {...});
Filer.DefineProperty('IgnoreFontProperty', {...});
Filer.DefineProperty('GlassFrame.Bottom', {...});
Filer.DefineProperty('GlassFrame.Enabled', {...});
Filer.DefineProperty('GlassFrame.Left', {...});
Filer.DefineProperty('GlassFrame.Right', {...});
Filer.DefineProperty('GlassFrame.SheetOfGlass', {...});
Filer.DefineProperty('GlassFrame.Top', {...});
end;
I have commented out the details about the ReadData, WriteData and HasData parameters of each DefineProperty call. The actual reading and writing has been delegated to the private Read and Write methods of the TCustomFormHelper that we listed at the top of this article.
The special thing to note here is that the GlassFrame properties are stored using a nested 'GlassFrame.SubProperty' name. In earlier versions of Delphi and the streaming subsystem this would not have worked, but there is now extra code in the Classes unit that makes this possible.
procedure TReader.ReadProperty(AInstance: TPersistent);
//...
PropInfo := GetPropInfo(Instance.ClassInfo, FPropName);
if PropInfo = nil then
begin
// Call DefineProperties with the entire PropPath
// to allow defining properties such as "Prop.SubProp"
FPropName := PropPath;
{ Cannot reliably recover from an error in a defined property }
FCanHandleExcepts := False;
Instance.DefineProperties(Self);
FCanHandleExcepts := True;
if FPropName <> '' then
PropertyError(FPropName);
Exit;
end;
The reason for doing it this way is that this is an important aspect in the future plans for the GlassFrame implementation.
A GlassFrame into the future
In future binary-breaking releases of Delphi (aka Highlander or BDS (CDS?) 2007) the GlassFrame implementation will be folded into the proper classes. Allen talks about this too. So most probably, GlassFrame will become a proper property on TCustomForm (and promoted to published in TForm), the TCustomFormHelper and the tricks discussed above will disappear. The neat thing, though, is that all code and .dfms using it will just continue to work. The 'GlassFrame.Subproperty' names of the defined properties will map directly to the nested properties of the GlassFrame instance. So while the inner workings will change, the externally observable behavior will stay the same. Impressive, don't you think?!
Other VCL changes
We running out of time and space (at least metaphorically), so we'll just quickly mention the other main changes to the VCL in Delphi 2007. Applications built with Delphi has normally been very easy to spot, because the taskbar icon belongs to a special hidden TApplicaton.Handle instead of the actual main form, so the system menu has been a little short ;). This behavior has now been made optional - old applications should have the old behavior while new projects now set a new Application. MainFormOnTaskBar property to True. This ensures that the icon in the taskbar actually belongs to the main form, instead of the application handle. This will fix a few issues in general and on Vista in particular. The new property is again temporarily provided by a class helper called TApplicationHelper, but this time without the extra design-time and streaming shenanigans. This class helper also injects a somewhat cryptically named EnumAllWindowsOnActivateHint property that as far as I can see provides a fix for showing hints for windows that belongs to the process, but was created in another thread.
type
TApplicationHelper = class helper for TApplication
public
property EnumAllWindowsOnActivateHint: Boolean {...};
property MainFormOnTaskBar: Boolean {...};
end;
A lot of the other changes in VCL are to properly support themeing and painting correctly with a glass form background, but at design-time and run-time. There are new Vista specific components in the Dialogs unit named TFileOpenDialog, TFileSaveDialog and TTaskDialog. These are all marked with the platform directive, so if you use them directly, they will only work in Windows Vista.
In addition there is a global UseLatestCommonDialogs variable that when set to True (the default) will automatically upgrade the good old TOpenDialog, TSaveDialog and MessageDlg into using the new Vista GUI look when available. Primoz Gabrijelcic has a nice post and good screenshots of these dialogs here.
The other new features
I'm not a database guy, but the database express architecture has been updated to generation 4 (DBX 4). This involves potential performance improvements, single sourcing database code for both native and managed code, (some) drivers with source code, all Delphi code, backward compatibility with DBX 3 drivers, connection pooling and more. Looks very impressive. The returned CodeGear database guru Steve Shaughnessy knows more about this stuff than anyone else - get the details here. An overview of the database architecture classes (generated using Together) can be seen here in CodeGear's Andreano Lanusse's blog.
Delphi 2007 ships with a new version of IntraWeb that CodeGear dubs VCL for the Web and it includes Ajax functionality. The native Win32 SOAP and WebServices support has had many bugfixes and performance improvements and is more capable than ever. As we noted in the screenshots of the first article in this series, MS Build is now used as the build engine allowing you to customize the build process with pre-compile and post-compile events. And it supports multiple build configurations for debug and release builds, for instance.
The debugger is an invaluable tool for experienced developers and now it is better than ever. I'll probably blog about it in more detail later. Often people use only a fraction of the useful feature in the debugger, mostly because they don't know about them or how to use them. The Call Stack has a number of improvements. There are glyphs indicating if a stack frame has debug info available or not. You can now set break points directly in the Call Stack - the debugger will break when control returns to that point. When you double click an entry in the Call Stack it will now show the locals in that frame in addition to navigating to the correct spot in the source code. In the default keyboard mapping, you can now press Shift+F5 to enable or disable a breakpoint on the current line (ah - nice to reduce those mouse operations!).
Phew! Hope you have enjoyed this little series on the Delphi 2007 beta.
Conclusion
It feels like Delphi 2007 is going to be a rock-solid release. Here is a summary of my conclusions of who Delphi 2007 is for:
- If you're developing on or for Windows Vista, you'll want to get Delphi 2007.
- If you're already on BDS 2006 and plan to upgrade to BDS 2007, you should seriously consider buying Delphi 2007 with SA (Software Assurance) or subscription now - that will (by all likelihood) give you BDS 2007 later this year. The transition from Win32 development on BDS 2006 should be very smooth - just keep using the same components without recompiling them. No need to wait for new versions from 3rd parties.
- If you are a Delphi 5-7 developer that is still sitting on the fence, this is the time to jump and take this offer. The IDE is much more capable and productive and the performance and flicker-free operation is on par with or better than Delphi 7 now. You can see the list of improvements between Delphi 7 and BDS 20006 in Nick Hodges blog here - all of these goodies (on the native Win32 side) are also part of Delphi 2007, of course.
I highly recommend Delphi 2007 Win32 for all native Delphi developers!
Posted by
Hallvards New Blog
at
Sunday, March 11, 2007
10
comments
Labels: D2007, Hack, Highlander, Vista, Win32
Tuesday, March 06, 2007
Review: Delphi 2007 for Win32 (Beta) - part two
Read part one here first.
What more is new in the compiler?
New published vs $M+ behavior
The new "W1055 PUBLISHED caused RTTI ($M+) to be added to type '%s' " warning is interesting. It solves one of the issues we discussed earlier. In earlier versions of the compiler, if you compiled code like this:
type
TMyClass = class
private
FName: string;
published
property Name: string read FName write FName;
end;
The published property would not have RTTI generated, but would be silently treated as a public property. This is because TMyClass does not derive from a class compiled with $M+ enabled (such as TPersistent) and $M+ is not specified for the TMyClass itself. When you compile the same code in Delphi 2007 you will now get a warning:
[DCC Warning] ThSort.pas(13): W1055 PUBLISHED caused RTTI ($M+) to be added to type 'TMyClass'
This indicates that despite the missing $M+, the compiler has promoted the published properties to published and generated RTTI fro them. To remove the warning, add a $M+ directive in front of the class - or change published to public - if that is what you really meant. Nice touch!
$DYNAMICBASE and ASLR
The compiler accepts a new $DYNAMICBASE ON/OFF compiler directive, and a corresponding command line parameter called "--dynamicbase". This is a shortcut for the existing $SetPEOptFlags directive. $DYNAMICBASE ON corresponds to $SetPEOptFlags $40 where $40 is defined by Windows as:
const IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE = $0040;
// The DLL can be relocated at load time.
The SetPEOptFlags directive sets a field in the PE header that is also called DllCharacteristics. It is used by the Windows loader to determine capabilities of a loaded module (DLL or BPL, for instance). The bit $40 seem to have a specific meaning in Windows Vista and enables a feature called Address Space Layout Randomization (ASLR). It means that the OS loads the module at a more or less random address (actually a random offset between 0 and 255) instead of trying to load it at exactly at the specified imagebase address. This is a security and anti-remote-hack measure making it harder to construct buffer overflow attacks that call known system DLL routines at fixed addresses. You can read more about it here.
--description command line option
The compiler also has a new command line option --description:<string> that corresponds to the $DESCRIPTION compiler directive. It lets you set the module description entry in the PE header.
What's new in the RTL?
The run time library contains the lowest level building blocks in the Delphi eco-system above the compiler. The special SysInit and System units are tightly bound to the internals of the compiler and linker. When using packages, SysInit is compiled into every module (EXE and package BPLs), while the System unit is only compiled into the rtl100.bpl. That is the reason they are split into to physically different units.
That's enough nitty-gritty background. What is new, then? Well, SysInit hasn't changed. I guess this is an indication that it has stabilized and don't need any changes for fixes. So, good news. Let's dive into the inner secrets of the System unit changes.
The System unit changes
There is a new interfaced boolean global variable with the catchy name NeverSleepOnMMThreadContention. It is used in the new FastMM based memory manager (courtesy of Pierre le Riche) that resides in the getmem.inc file that is included in the implementation section of System.pas. Here is the comment that accompanies the variable declaration:
{Set this variable to true to employ a "busy waiting" loop instead of putting
the thread to sleep if a thread contention occurs inside the memory manager.
This may improve performance on multi-CPU systems with a relatively low thread
count, but will hurt performance otherwise.}
NeverSleepOnMMThreadContention: Boolean;
When the FastMM memory manager needs to protect shared resources, it uses light-weight atomic operations (such as the lock cmpxchg assembly instruction) instead of the more heavy-duty OS-level critical section APIs. If there is contention (the resource has already been locked by another thread), it normally sleeps to release the CPU and allow the other thread to finish its work and release the lock.
As the comment above alludes to, in some situations on multiprocessor machines (that is becoming mainstream) it may be more efficient to just keep looping and checking the lock availability (also called a spin-lock) while waiting for the other thread to execute on one of the other processors to release the lock. Here is an example of how the Pascal version of this logic looks like:
while LockCmpxchg(0, 1, @MediumBlocksLocked) <> 0 do
begin
if not NeverSleepOnMMThreadContention then
begin
Sleep(InitialSleepTime);
if LockCmpxchg(0, 1, @MediumBlocksLocked) = 0 then
break;
Sleep(AdditionalSleepTime);
end;
end;
Another improvement to the assembly versions (which gets used by default) of these spin-locks is usage of the pause x86 instruction to be more processor and OS-friendly:
asm
//...
{The pause instruction improves spinlock performance}
pause
//...
end;
In addition the FastMM code size has been slightly reduced (I haven't measured) by using the equivalent of a method-extract refactoring on some of the assembly code (the logic for resizing a large memory block has been refactored into an internal ReallocateLargeBlock routine).
Finally, it looks like there is some improved logic to support segmented large blocks. Pierre's comment in the code about this is:
{Is this large block segmented? I.e. is it actually built up from more than
one chunk allocated through VirtualAlloc? (Only used by large blocks.)}
Optimized RTL routines
As Steve Threfethen has already blogged about there are now updated and new FastCode replacement functions included in the stock RTL. The compiler magic System routine _LStrCmp is used whenever you compare strings, so it is a performance critical piece of code. Pierre le Richie wrote a new, faster version of this routine (based on the FastCode winner of the CompareStr challenge) and submitted it to Quality Central #31328. This routine has now been incorporated into the RTL.
Note that _LStrCmp is a general routine that is called for all string compares, including the string compare operators >, >=, =, <>, < and <=. In Pierre's QC entry he suggests changing the compiler so that it calls a different, more specific function (_LStrEqual) - in most cases, it would be able to determine non-equalness simply by comparing the strings' lengths. As this change would probably break binary .dcu compatibility, it is not included in Spacely, but it is a possibility for Highlander (that will probably become BDS 2007).
The other FastCode replacement functions reside in SysUtils. The UpperCase and LowerCase were already FastCode functions (by Aleksandr Sharahov) in BDS 2006 - there are now even faster versions by John O'Harrow in Delphi 2007. In addition CompareStr and StrLen are new FastCode functions from Pierre le Riche.
In addition the FileAge and FileSearch functions have been optimized. FileAge now calls the more efficient GetFileAttributesExA API (when available) instead of the FindFirst/FindClose pair. FileSearch now leaves early if the Name parameter is an empty string. These two changes also helps improve the speed of the compiler and linker.
Finally the hash-table logic used in the CheckForDuplicateUnits routine during loading of packages has been further optimized. This code was greatly improved in BDS2006 and it is now even faster. This helps reduce the startup time of applications that loads lots of packages dynamically (such as the Delphi IDE itself).
Desktop Window Manager API
There is a new DwmApi unit (in the source\Win32\rtl\win directory) that dynamically binds to the Windows Vista specific APIs exported by DWMAPI.DLL. On other platforms the routines are stubbed out to return the E_NOTIMPL error code. The constants and routines in this unit is the basis of all the new Windows Vista and Glass specific functionality added to the VCL and Delphi IDE. It has exiting sounding routine names like DwmExtendFrameIntoClientArea and DwmUpdateThumbnailProperties. Most of the time the VCL shields you from using these nitty-gritty APIs directly, but its handy to know where to find them.
Math unit and complete boolean evaluation
The math unit has had a couple of interesting changes to the trio (Integer, Int64 and Double versions) of the InRange functions. Basically, the code has been rearranged from (BDS 2006 version):
function InRange(const AValue, AMin, AMax: Integer): Boolean;
begin
Result := (AValue >= AMin) and (AValue <= AMax);
end;
to (Delphi 2007 version):
function InRange(const AValue, AMin, AMax: Integer): Boolean;
var
A,B: Boolean;
begin
A := (AValue >= AMin);
B := (AValue <= AMax);
Result := B and A;
end;
The code changes forces the compiler to evaluate both boolean expressions before setting the Result variable. This reduces the number of conditional jumps and thus typically improves performance (particularly in the cases where InRange returns true - then it has to evaluate both expressions anyway). The generated code differences are as follows - here is the assembly code generated for the old BDS 2006 version of InRange:
Result := (AValue >= AMin) and (AValue <= AMax);
//0040841C 3BD0 cmp edx,eax
//0040841E 7F04 jnle $00408424
//00408420 3BC8 cmp ecx,eax
//00408422 7D03 jnl $00408426
//00408424 33C0 xor eax,eax
//00408426 C3 ret
and here is the assembly code for the new version:
A := (AValue >= AMin);
//0040842C 3BD0 cmp edx,eax
//0040842E 0F9EC2 setle dl
B := (AValue <= AMax);
//00408431 3BC8 cmp ecx,eax
//00408433 0F9DC0 setnl al
Result := B and A;
//00408436 22C2 and al,dl
end;
//00408438 C3 ret
As you can see the second version does not have any branches, while the first one has two conditional branch instructions. Modern processors work better with branchless code, so the net effect is better performance. We might revisit this topic in the future - and the alternative way of accomplishing the same thing - using the $B+ compiler directive to enable complete boolean evaluation.
That concludes part two of this review - part three will follow shortly. Stay tuned! ;)
Posted by
Hallvards New Blog
at
Tuesday, March 06, 2007
12
comments
Labels: D2007, Hack, Highlander, Vista, Win32
Thursday, March 01, 2007
Review: Delphi 2007 for Win32 (Beta) - part one
Nick Hodges of CodeGear contacted me and gave me permission to talk about the upcoming Delphi 2007 for Win32 product - codenamed Spacely. Note that this review is based on a pre-release Beta build 2063 from mid February. Anything you see here is subject to change in the release version.
The installer
Here are a few screenshots of how the new installer looks like - click them to see them in full size. The installer has been created with InstallAware (which is also bundled with Delphi 2007) and it should allow CodeGear to release patches and potentially new versions in an easier and more integrated way.
Delphi 2007 is a non-breaking version
What does it mean that Delphi 2007 is a non-breaking version? Well, according to CodeGear roadmaps there will be a new BDS (or CDS?) version later this year code named Highlander. To reduce the pain (for developers, 3rd party component providers etc) of having two new full Delphi versions in less than a year, CodeGear decided to release Delphi 2007 as a non-breaking version. The result is that you should be able to use code and components designed for and compiled in BDS 2006 or Turbo Delphi 2006 without even recompiling any .dcu (delphi compiled unit) files. So even .dcu only shareware components should continue to just work.
Delphi 2007 is special in that it is (mostly) a non-breaking version in relation to BDS 2006 and Turbo Delphi 2006. This means that it is binary compatible at the .dcu level and no breaking changes has been made to the interface of any units. This means that there are no changes to existing classes or routines, for instance. Note that the Delphi unit interface checks are granular enough to allow new classes and identifiers to be added, as long as no existing identifiers are modified in any way.
A side-effect is that while it is relatively easy to fix RTL and VCL bugs (that only need changes in the implementation section), and to introduce new classes and components, it is very hard to introduce new functionality, methods and properties on existing classes. Hard, but not impossible, of course. CodeGear has not been shy to apply the odd hack or two to add native support for extended Vista Glass functionality in the TForm class, for instance. Allen Bauer has already spilled the beans on this and how it was achieved. We will take a closer look at the RTL and VCL changes in Delphi 2007 (relative to BDS 2006) further down in the review.
What's new in the compiler?
It also means that the compiler has not been updated with new features that would break binary compatibility, so there are no generics support yet, for instance. However, a little investigation shows that there have been improvements to the compiler. For instance, there are a slew of new compiler hints, warnings and errors. It also looks like the compiler has spiffed up its XML documentation generation capabilities. Some of the new error messages are probably due to bug fixes. Code that would earlier generate internal errors now generate clean compile-time errors with explanations of what is wrong with the code. Here is an example list of the new hints, warnings and errors that I've identified:
New Hints:
- H2445 Inline function '%s' has not been expanded because its unit '%s' is specified in USES statement of IMPLEMENTATION section and current function is inline function or being inline function
- H2451 Narrowing given WideChar constant (#$%04X) to AnsiChar lost information
- H2456 Inline function '%s' has not been expanded because contained unit '%s' uses compiling unit '%s'
New Warnings:
- W1055 PUBLISHED caused RTTI ($M+) to be added to type '%s'
- W1201 XML comment on '%s' has badly formed XML -- 'Whitespace is not allowed at this location.'
- W1202 XML comment on '%s' has badly formed XML -- 'Reference to undefined entity '%s'.'
- W1203 XML comment on '%s' has badly formed XML -- 'A name was started with an invalid character.'
- W1204 XML comment on '%s' has badly formed XML -- 'A name contained an invalid character.'
- W1205 XML comment on '%s' has badly formed XML -- 'The character '%c' was expected.'
- W1206 XML comment on '%s' has cref attribute '%s' that could not be resolved
- W1207 XML comment on '%s' has a param tag for '%s', but there is no parameter by that name
- W1208 Parameter '%s' has no matching param tag in the XML comment for '%s' (but other parameters do)
New Errors:
- E2447 Duplicate symbol '%s' defined in namespace '%s' by '%s' and '%s'
- E2448 An attribute argument must be a constant expression, typeof expression or array constructor
- E2449 Inlined nested routine '%s' cannot access outer scope variable '%s'
- E2450 There is no overloaded version of array property '%s' that can be used with these arguments
Linker errors:
- F2446 Unit '%s' is compiled with unit '%s' in '%s' but different version '%s' found
What's new in the IDE
In general the IDE feels very fast and nice to work with - it has noticeably less flickering when switching between debug and design layouts, for instance.
Here are a couple of screen shots showing some of the news in the IDE.
I've marked the changes with numbers:
- I've added a new toolbar button to toggle a debugger setting called Notify on Language Exceptions
- The new TForm property GlassFrame to control the Vista Glass effect on the form's area. This is achieved cunningly with class helpers, property injectors and some extra hacks.
- The Tool Palette has improved partial search matching. Any component that contains the string you're typing will be included in the list. So starting to type "but" will show all button components. This is great!
Here is a another screen shot.
- The new File Browser view is very handy. It works like a mini Explorer, reducing the need to use an external Explorer or File | Open all the time. And of course, this view can be docked anywhere - or you can keep it floating, or unpin it so that it scrolls away when you're not using it.
- The project's options for Compiler, Compiler Warnings, Linker and Directories/Conditionals can now be set separately for a Release and Debug configurations. In addition you can create more named configurations. Finally!
- Here you can again see the new compiler warnings that can be toggled on or off
- I added a Windows Vista specific component called TFileOpenDialog - the compiler issues a platform warning for this. Nice.
This review will be updated with more information and details in the coming few days. Stay tuned ;)
Posted by
Hallvards New Blog
at
Thursday, March 01, 2007
17
comments