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

Sunday, October 07, 2007

DN4DP#24: .NET vs Win32: Untyped parameters

This post continues the series of The Delphi Language Chapter teasers from Jon Shemitz’ .NET 2.0 for Delphi Programmers book.

The previous post listed the Win32 specific language and RTL features. The next few posts will focus on minor differences in implementation between Win32 and .NET - starting with differences in the detailed semantics of untyped var and out parameters.

Note that I do not get any royalties from the book and I highly recommend that you get your own copy – for instance at Amazon.

"Win32 and .NET differences

In addition to the new language features and the obsolete features we have already mentioned, there are some implementation details that are different between the Win32 and .NET versions of the Delphi language. These differences are minor, but it is useful to know about them.

Untyped var and out parameters

It is interesting to note that Delphi supports type-less var and out parameters in a strictly typed and managed environment like .NET.

var
GlobalInt: integer;

procedure FooVar(var Bar);
var
BarValue: integer;
begin
BarValue := Integer(Bar);
Inc(BarValue);
Bar := BarValue;
end;

procedure TestFooVar;
begin
GlobalInt := 1;
FooVar(GlobalInt);
end;

The implementation relies on boxing the actual argument to and from System.Object before and after the method call. The compiler compiles the code above like this

procedure FooVarImpl(var Bar: TObject);
var
BarValue: integer;
begin
BarValue := Integer(Bar); // Unbox
Inc(BarValue);
Bar := TObject(BarValue); // Autobox
end;

procedure TestFooVarImpl;
var
Temp: TObject;
begin
GlobalInt := 1;
Temp := TObject(GlobalInt); // Autobox
FooVarImpl(Temp);
GlobalInt := Integer(Temp); // Unbox - after the routine returns
end;

Because of this implementation, you will not see intermediate modifications of the actual argument until the call returns. The compiler allows direct assignments to the untyped parameter in .NET (as if $AUTOBOX is turned ON just for that TObject parameter) – this is not allowed in Win32. In addition, left-hand-side casts of an untyped parameter is not allowed in .NET – only in Win32. This can make it hard to write single-source routines using var and out parameters without resorting to IFDEFs. The UntypedParameters project demonstrates these differences.

unit UntypedParametersU;

interface

procedure Test;

implementation

{$DEFINE CHECK_SIDEFFECTS}

type
TMyObject = class
end;

var
GlobalInt: integer;
GlobalRef: TMyObject;

procedure FooVar(var Bar);
var
BarValue: integer;
begin
BarValue := Integer(Bar);
{$IFDEF CHECK_SIDEFFECTS}
writeln('1. FooVar Bar = ', BarValue);
writeln('1. FooVar GlobalInt = ', GlobalInt);
{$ENDIF}
Inc(BarValue);
{$IFDEF CIL}
Bar := BarValue;
{$ELSE}
Integer(Bar) := BarValue;
{$ENDIF}
{$IFDEF CHECK_SIDEFFECTS}
writeln('2. FooVar Bar = ', Integer(Bar));
writeln('2. FooVar GlobalInt = ', GlobalInt);
if GlobalInt = Integer(Bar)
then writeln('Win32 untyped var semantics')
else writeln('.NET untyped var semantics');
{$ENDIF}
end;

procedure FooVarImpl(var Bar: TObject);
var
BarValue: integer;
begin
BarValue := Integer(Bar);
Inc(BarValue);
Bar := TObject(BarValue);
end;

procedure FooOut(out Bar);
begin
{$IFDEF CIL}
Bar := Integer(42);
{$ELSE}
Integer(Bar) := 42;
{$ENDIF}
{$IFDEF CHECK_SIDEFFECTS}
writeln('1. FooOut Bar = ', Integer(Bar));
writeln('1. FooOut GlobalInt = ', GlobalInt);
if GlobalInt = Integer(Bar)
then writeln('Win32 untyped out semantics')
else writeln('.NET untyped out semantics');
{$ENDIF}
end;

procedure FooConst(const Bar);
var
Temp: integer;
begin
writeln('1. FooConst Bar = ', Integer(Bar));
writeln('1. FooConst GlobalInt = ', GlobalInt);
Temp := Integer(Bar);
writeln('1. FooConst Temp = ', Temp);
end;

procedure NilRef(var Obj);
begin
{$IFDEF CIL}
Obj := nil;
{$ELSE}
TObject(Obj) := nil;
{$ENDIF}
{$IFDEF CHECK_SIDEFFECTS}
if GlobalRef = nil
then writeln('Win32 untyped var semantics')
else writeln('.NET untyped var semantics');
{$ENDIF}
end;

procedure NilRefImpl(var Obj: TObject);
begin
Obj := nil;
{$IFDEF CHECK_SIDEFFECTS}
if GlobalRef = nil
then writeln('Win32 untyped var semantics')
else writeln('.NET untyped var semantics');
{$ENDIF}
end;

procedure TestNilRef;
begin
GlobalRef := TMyObject.Create;
NilRef(GlobalRef);
end;

procedure TestNilRefImpl;
var
Temp: TObject;
begin
GlobalRef := TMyObject.Create;
Temp := GlobalRef;
NilRefImpl(Temp);
GlobalRef := TMyObject(Temp);
end;

procedure TestFooVar;
begin
GlobalInt := 1;
FooVar(GlobalInt);
end;

procedure TestFooVarImpl;
var
Temp: TObject;
begin
GlobalInt := 1;
Temp := TObject(GlobalInt);
FooVarImpl(Temp);
GlobalInt := Integer(Temp);
end;

procedure Test;
begin
TestNilRef;
{$IFDEF CIL}
TestNilRefImpl;
{$ENDIF}
TestFooVar;
{$IFDEF CIL}
TestFooVarImpl;
{$ENDIF}
Writeln('2. GlobalInt = ', GlobalInt);
FooOut(GlobalInt);
Writeln('3. GlobalInt = ', GlobalInt);
FooConst(GlobalInt);
end;

end.

"

Friday, October 05, 2007

DN4DP#23: .NET only: Obsolete features

This post continues the series of The Delphi Language Chapter teasers from Jon Shemitz’ .NET 2.0 for Delphi Programmers book.

The previous post showed how to use the mysterious P/Invoke features. This time we'll list the Win32 specific features of the language and RTL that didn't make it to the .NET side.

Note that I do not get any royalties from the book and I highly recommend that you get your own copy – for instance at Amazon.

"Obsolete features

Due to the managed and garbage collected nature of the .NET environment, a number of native Win32 specific features had to be left out in the Delphi for .NET language. Most of these are already warned against when you compile with the Win32 compiler, easing the porting process.

Feature    Comment
Pointers  Including PChar, @ operator, GetMem, etc. See Unsafe code
absolute    Variable overlaying not supported
Real48   This is a relic from the Borland Pascal (BP) days
File of <type> Size of records are not fixed in .NET
BlockRead, BlockWrite Size of records are not fixed in .NET
Old-style objects  Really a relic from BP – deprecated since Delphi 1
BASM   Built-in Assembler – is specific to x86 and native code
IUknown   No longer has AddRef, Release and QueryInterface
implements   Interface delegation, not implemented yet
automated, dispid  OLE Automation not supported

"

Tuesday, October 02, 2007

My article on CodeGear RAD Studio 2007

As some of you may know, I've recently written an article/review on the new CodeGear RAD Studio 2007. It was originally written in Norwegian (my first technical article in Norwegian - that was hard:)) and published on the Hardware Business site hwb.no:

http://www.hwb.no/test/utvikling/codegear_rad_studio_2007_/43487

Due to popular demand, I've translated the article into English and sent it to Nick Hodges so it might appear on CDN in the near future... ;)

Update: The translated article is available on CDN now:
http://dn.codegear.com/article/37131

Monday, October 01, 2007

DN4DP#22: .NET only: P/Invoke magic

This post continues the series of The Delphi Language Chapter teasers from Jon Shemitz’ .NET 2.0 for Delphi Programmers book.

Last time we're explored an undocumented corner of the language. This time we'll explore the exotic (and cryptic?) world of P/Invoke.

Note that I do not get any royalties from the book and I highly recommend that you get your own copy – for instance at Amazon.

"P/Invoke magic

Moving into the slightly more esoteric topics, Delphi for .NET supports two Platform Invoke (or P/Invoke) technologies called Reverse P/Invoke (or Unmanaged Exports) and Dynamic P/Invoke (or Virtual Library Interfaces).

Reverse P/Invoke lets you write a .NET DLL that can be used like any other DLL from Win32 code. It is a quick way of introducing .NET functionality into a Win32 application without performing a complete porting process or hosting the CLR explicitly.

Unmanaged exports must reside within a library project and generate thunks of unmanaged code, so you must turn {$UNSAFECODE ON}. The syntax is the same exports declaration as is used in Win32 Delphi. Only global-level routines can be exported, not class methods.

library ReversePInvoke;
procedure Foo(const S: string);
function Bar: integer;
function Greeting(Name: string): string;
//...
{$UNSAFECODE ON}
exports
Foo,
Bar,
Greeting;

On the Win32 side you import these routines just like you would import any other DLL, using external declarations.

const
LibName = 'ReversePInvoke.DLL';

procedure Foo(const S: string); stdcall; external LibName;
function Bar: integer; stdcall; external LibName;
function Greeting(Name: string): PChar; stdcall; external LibName;


Caution: Not all managed types can be used as parameters in exported routines. Generally you can use simple types and strings. String input parameters map to Win32 AnsiString, string results and output parameters map to PChar.


Virtual Library Interfaces uses Dynamic P/Invoke to import a Win32 DLL by using an interface to specify what routines to import. The DLL can be seen as a singleton object that implements the interface. The advantage is that you can use the Supports function from the Borland.Delphi.Win32 unit to check if the DLL and all the methods are available.

uses
Win32;
type
IMyInterface = interface
procedure Foo(const S: string);
function Bar: integer;
function Greeting(const Name: string): string;
end;

procedure Test;
var
MyInterface: IMyInterface;
begin
if Supports('Win32NativeDLL.DLL', TypeOf(IMyInterface), MyInterface) then
begin
Writeln('.NET App dynamically calling into Win32 DLL');
Writeln('The Answer is ', MyInterface.Bar);
MyInterface.Foo('.NET client');
Writeln(MyInterface.Greeting('Ida'));
end
else
Writeln('Cannot find Win32NativeDLL.DLL!');
end;

In effect you are dynamically loading the DLL if and only if it is available. If not, the application can continue running, but with reduced functionality. It also allows the application to control the folder the DLL is loaded from.


Tip: Use the LibraryInterface attribute to control calling convention and the wideness of string parameters. The defaults are CharSet.Auto (PChar on Win9x and PWideChar on WinNT) and CallingConvention.Winapi (or stdcall)."

Sunday, September 30, 2007

DN4DP#21: .NET only: Undocumented corner

This post continues the series of The Delphi Language Chapter teasers from Jon Shemitz’ .NET 2.0 for Delphi Programmers book.

Last time we're covered the differenced between classic single-cast events vs .NET-style multi-cast events. This time we'll dive into an undocumented corner of the language.

Note that I do not get any royalties from the book and I highly recommend that you get your own copy – for instance at Amazon.

"Undocumented corner

We have already mentioned the new semi-undocumented record helper feature in Delphi for .NET. Another undocumented and more subtle extension[1] is the ability to initialize global variables and typed constants with simple casting and constructor calls using constant parameters.

type
TFoo = class
constructor Create(A, B, C: integer);
end;
TBar = record
public
class operator Explicit(Value: Integer): TBar;
class operator Implicit(Value: Double): TBar;
end;

var
Foo: TFoo = TFoo.Create(1, 2, 3);
Bar1: TBar = TBar(42);
const
Bar2: TBar = 3.14;

In native Delphi you can initialize global variables with constant expressions such as integers, floating point values and strings. In .NET this has been extended to allow initialization of object references and records using a constructor call or an implicit or explicit cast operator. This feature can’t be used for instance fields or class vars, only for global variables and typed constants, so its usefulness is a little limited. The InitializeGlobals project demonstrates this new syntax.


Note: While this is currently an undocumented feature, it is fairly safe to assume it will continue to be available in the future. For instance, the Currency type in the Borland.Delphi.System unit is implemented as a record with operator overloading and it has implicit conversion operators from Double and Integer. Without this feature, there would be no way to initialize a global Currency variable (breaking existing Win32 code).





[1] This existence of this new syntax was first published by Chee Whee Chua (Borland Singapore) at http://blogs.borland.com/chewy/archive/2005/11/23/22210.aspx "



Update:  The new URL is: http://blogs.codegear.com/chewy/2005/11/23/22210

Friday, September 28, 2007

DN4DP#20: .NET only: Multi-cast events

This post continues the series of The Delphi Language Chapter teasers from Jon Shemitz’ .NET 2.0 for Delphi Programmers book.

Last time we're scared by unsafe code. Here we discuss the syntax for normal Delphi events vs .NET-style multicast events.

Note that I do not get any royalties from the book and I highly recommend that you get your own copy – for instance at Amazon.

"Multi-cast events

Delphi for .NET has full support for both old-style single cast events and .NET style multi cast events.

To write a traditional Delphi event, you first declare a delegate type using the procedure of object syntax, then declare an event property that reference a delegate field in the read and write specifiers.

type
TLevelChangedEvent = procedure (Sender: TObject; NewLevel: integer) of object;
TMyComponent = class
strict private
FOnLevelChanged: TLevelChangedEvent;
public
property OnLevelChanged: TLevelChangedEvent read FOnLevelChanged write FOnLevelChanged;
end;

This is a single-cast event that will compile both in .NET and Win32. It supports direct assignment of a method reference or nil to the event property and it supports directly invoking the event property. For instance, most VCL events are single cast and support assignments like this:

  MyComponent.OnLevelChanged := MyTest.FirstTarget;
MyComponent.OnLevelChanged(nil, 1);

However, many .NET consumers will expect multicast events in your classes. To enable this in a Delphi for .NET class you simply use add and remove specifiers instead of read and write, like this:

  TMyComponent = class
strict private
FOnMultiChanged: TLevelChangedEvent;
public
property OnMultiChanged: TLevelChangedEvent add FOnMultiChanged remove FOnMultiChanged;
end;

The simplest solution is simply to reference a delegate field as before - the compiler will then implement proper add_Event and remove_Event methods for you. In some special cases you may want to implement your own logic in these routines - to do that you simply write and reference your own add_Event and remove_Event methods, like this

  TMyComponent = class
strict private
FOnCustomChanged: TLevelChangedEvent;
public
procedure add_OnCustomChanged(Value: TLevelChangedEvent);
procedure remove_OnCustomChanged(Value: TLevelChangedEvent);
property OnCustomChanged: TLevelChangedEvent add add_OnCustomChanged remove remove_OnCustomChanged;
end;

procedure TMyComponent.add_OnCustomChanged(Value: TLevelChangedEvent);
var
Inlist: Delegate;
begin
if Assigned(FOnCustomChanged) then
for Inlist in Delegate(@FOnCustomChanged).GetInvocationList do
if InList.Equals(Delegate(@Value)) then
Exit;
FOnCustomChanged := TLevelChangedEvent(Delegate.Combine(Delegate(@FOnCustomChanged), Delegate(@Value)));
end;

procedure TMyComponent.remove_OnCustomChanged(Value: TLevelChangedEvent);
begin
FOnCustomChanged := TLevelChangedEvent(Delegate.Remove(Delegate(@FOnCustomChanged), Delegate(@Value)));
end;

This example add-handler only allows unique delegate targets, ignoring any attempt to add the same object’s method more than once. Note the tricky-looking code with casts to Delegate and use of the @-operator. The Delegate casts are required to force the compiler to treat the procedure of object as a System.Delegate instance (which is an implementation detail from the compiler’s point of view). The @-operator is required to prevent the compiler from trying to call the event instead of evaluating its value.

Most WinForms events are multicast events - they support multiple methods as targets. To add or remove a method from a multi-cast event, you use the Include and Exclude intrinsic procedures:

  Include(MyComponent.OnMultiChanged, MyTest.FirstTarget);
Include(MyComponent.OnMultiChanged, MyTest.SecondTarget);
MyComponent.TriggerMulti(6);
Exclude(MyComponent.OnMultiChanged, MyTest.FirstTarget);

This corresponds directly to the += and -= operators that C# supports on events.


Tip: Use multi-cast add/remove events for WinForms code and components. Use single-cast read/write events for VCL for .NET code and components, unless you really need multi-cast behavior. "


Update/Note: If you need to implement an interface that includes a multi-cast event property - i.e. the interface has add_Event and remove_Event methods - you can get by by declaring an add/remove event property on the class, with the add and remove specifiers referencing a procedure of object field. This will force the compiler to generate the add_Event and remove_Event methods for you - just like it did in the third code block above.



Copyright © 2004-2007 by Hallvard Vassbotn