Borland Delphi 2005 Compiler, Language, and Debugger Enhancements

By: Borland Technical Publications

Abstract: This article provides an overview of several of the new features found in Delphi 2005

Table of Contents

Borland® Delphi® 2005 Compiler, Language, and Debugger Enhancements

Introduction

Delphi Compilers

Delphi for .NET Language Enhancements

For-in-do loop

Function and procedure inlining

Delphi 2005 Debugger Enhancements

Multi-unit namespaces

Dynamic multidimensional arrays

Delphi Win32 Language Enhancements

Conclusion

Borland® Delphi® 2005 Compiler, Language, and Debugger Enhancements

by Bob Swart, Bob Swart Training & Consultancy

Introduction

This tutorial will build several small console, VCL, VCL for .NET, and WinForms applications to demonstrate some of the enhanced Delphi 2005 compiler, language, and debugger capabilities.

Delphi Compilers

Delphi 2005 includes two command-line compilers (dcc32 and dccil) that can be invoked from the command line. Dcc32 is the Delphi for Win32 command-line compiler, and dccil is the Delphi for .NET command-line compiler. Both are at version 17.0. The Delphi 2005 IDE also uses two different Delphi compilers, as well as the C# compiler from the Microsoft® .NET Framework 1.1.

Delphi for .NET Language Enhancements

Compared to Delphi 8 for the Microsoft .NET Framework, a number of new language enhancements have been introduced to the Delphi for .NET compiler. These include a new for-in-do syntax, function and procedure inlining, multi-unit namespace support, dynamic multidimensional arrays, and unicode identifiers.

- Start Delphi 2005

- Do File | New, and select the "VCL Forms Application - Delphi for .NET" option. This will create a new VCL for .NET project. You may wish to save the project under a different name in a different directory other than the default generated name and directory.

- Place a TListBox, a TEdit, and a TButton component on the VCL Form and double-click on the Button to write the following code in the OnClick event handler:

procedure TForm3.Button1Click(Sender: TObject);
begin
  ListBox1.Items.Add(Edit1.Text);

end;

This will place the contents of the TEdit in the TListBoxbone of the often-used ways to demonstrate new IDE features.

For-in-do loop

We will now extend this demo to illustrate the use of the for-in-do loop, which provides a very powerful way to index items in a list or collection of elements.

- Place a TMemo component on the Form, as well as another TButton, and double-click on the Button to write the following code in the OnClick event handler:

procedure TForm3.Button2Click(Sender: TObject);

var
  S: String;
  
begin
  Memo1.Clear;
  for S in ListBox1.Items do
    Memo1.Lines.Add(S);
	
end;

The S here is a String, which is an element of the collection of Strings represented by the ListBox1.Items property. Actually, the ListBox1.Items property is of type TStrings, and this type exports a GetEnumerator function. Within the for-in-do loop, the S is a read-only value, which gets a new value for each time the loop is executed (taken from the set of values in the ListBox1.Items in this case). Without the for-in-do language construct, the same functionality could be obtained by writing the following code:

procedure TForm3.Button4Click(Sender: TObject);

var
  i: Integer;
  
begin
  Memo1.Clear;
  for i:=0 to ListBox1.Items.Count-1 do
    Memo1.Lines.Add(ListBox1.Items[i]);
	
end;

*Note that this would require an index, counting from 0 to the number of elements minus 1 (a frequent place for programming errors). And we have to explicitly specify where and how to access the indexed item itself. The new syntax is much cleaner, doesn't require the integer index anymore, and simply uses an instance to enumerate the items in a set, list or collection of items.

Apart from a string which is an item of a TStrings instance, you also can use a character which is an item of a String. For example, to count the number of spaces in a string:

- Change the source code for the Button2Click event handler to count the number of spaces in the strings, as follows (you can add your own way to report the actual number of spaces; it's the technique that counts at this time, not the answer):

procedure TForm3.Button2Click(Sender: TObject);

var
  S: String;
  C: Char;
  Spaces: Integer;

begin
  Spaces := 0;
  Memo1.Clear;
  for S in ListBox1.Items do
  begin
    Memo1.Lines.Add(S);
    for C in S do
      if C = ' ' then Inc(Spaces);
  end;

end;

*Note that we cannot assign any new value to the S or C loop index variables; they are provided as read-only references only. Apart from strings and characters, you can also use literal sets, or TList or TCollection classes as enumeration containers that can be used to enumerate.

As a last example, consider the fact that a VCL Form contains a list of components, which can be used as a collection as well.

- Place a third TButton on the Form and write the following code, which will list the name and type of all components on the form, and place them in the TListBox.

procedure TForm1.Button1Click(Sender: TObject);

var
  Comp: TComponent;

begin
  ListBox1.Clear;
  for Comp in Self do
    ListBox1.Items.Add(Comp.Name + ': ' + Comp.ClassName);

end;

*Note that the new for-in-do loop uses a variable Comp of type TComponent, which is going through the set of components contained in Self.

*Note that the Comp variable is read-only; you cannot assign to it. However, it's actually an implicit pointer to a component, so we can change properties such as the Name.

- Modify the event handler of the third Button as follows, in order to modify the Caption of the three buttons on the VCL Form:

procedure TForm3.Button3Click(Sender: TObject);

var
  Comp: TComponent;

begin
  ListBox1.Clear;
  for Comp in Self do
  begin
    Comp.Tag := 42;
    if (Comp is TButton) then
      (Comp as TButton).Caption :=
        '<'+(Comp as TButton).Name+'>'; 
    ListBox1.Items.Add(Comp.Name + ': ' + Comp.ClassName);
  end;

end;

So, although you cannot actually assign a new value to Comp, you still can change the contents of the component itself. The same thing is true for components or objects that are contained in TList or TCollection classes.

Warning: changing the name property of a component usually is not a good idea at runtime.

Function and procedure inlining

Both the Win32 and .NET Delphi languages are extended with function inlining, which can result in faster performance. Instead of calling a routine, the code from the routine itself is expanded in place of the call (saving a call and return, as well as parameter management). This is especially beneficial for small routines, routines outside your unit scope, or routines with many parameters. For bigger routines, the trade-off between efficiency at the cost of bigger code size should be considered carefully before applying inline.

- Start Delphi 2005.

- Do File | New - Other, and select a Console application from the Delphi projects group (to create a Win32 console application).

Delphi 2005 Object Repository (Delphi Win32 Projects)

- Do File | Save Project As to save the project as InlineDemo.dpr

- Write the following code, which demonstrates a function Max that determines the maximum of three passed integer values:

program InlineDemo;
{$APPTYPE CONSOLE}

uses
  MMSystem;

  function Max(const X,Y,Z: Integer): Integer;
  begin
    if X > Y then
      if X > Z then Result := X
               else Result := Z
    else
      if Y > Z then Result := Y
               else Result := Z
  end;

const
  Times = 10000000; // 10 million
var
  A,B,C,D: Integer;
  Start: LongInt;
  i: Integer;

begin
  Random; // 0
  A := Random(4242);
  B := Random(4242);
  C := Random(4242);
  Start := TimeGetTime;
  for i:=1 to Times do
    D := Max(A,B,C);
  Start := TimeGetTime-Start;
  writeln(A,', ',B,', ',C,': ',D);
  writeln('Calling Max ',Times,' times took ',Start,' milliseconds.');
  readln

end.

- Compile and run the InlineDemo example application. It will show the output, as well as the rough amount of seconds it took to make the 10 million calls to the Max function.

The calls to the function Max can be enhanced by turning the call into an inlined method, thereby expanding the actual code from the function Max at the place where the call was made. This will eliminate the need for the call (and as a consequence, noticeable performance gains can be obtained if the call and return are relatively expensive compared to the time spend in the function body itselfb in other words: you'll get better results for smaller functions than for bigger functions).

- Change the function Max by adding an inline directive at the end of the function header. The new code should look as follows:


  function Max(const X,Y,Z: Integer): Integer; inline;
  begin
    if X > Y then
      if X > Z then Result := X
               else Result := Z
    else
      if Y > Z then Result := Y
               else Result := Z
  end;

This will instruct the compiler to inline the function. Each call will be replaced by the actual code inside the function (and not by a call to Max).

- Recompile and rerun the InlineDemo project. This time, the results will be the same, although the timings will show an increased performance gain.

- Repeat the exercise using a Delphi for .NET console application. This will show an even bigger performance increase (when using inline) compared to the performance increase with Win32.

*Note that it's important to measure and verify the results of inlining routines, because in some situations it can also lead to slower code instead of faster code. Especially when using bigger routines (this can lead to bigger applications, with little speed improvements) or in cases in which you pass a lot of arguments.

As an example, the following Win32 console application does not result in faster code when using inline:

program InlineDemo;
{$APPTYPE CONSOLE}

uses
  MMSystem;

  procedure Swap(var X,Y: Char); inline; // faster without inline
  var
    Z: Char;
  begin
    Z := X;
    X := Y;
    Y := Z
  end;

  procedure ReverseString(var S: String);
  var
    i: Integer;
    Len: Integer;
  begin
    Len := Length(S);
    for i:=1 to Len div 2 do
      Swap(S[i],S[Len+1-i]);
  end;

const
  Times = 10000000; // 10 million

var
  S: String;
  Start: Cardinal;
  i: Integer;

begin
  S := 'ABCDEFGHIJKLMNOPQRSTUVWXTZ1234567890abcdefghijklmnopqrstuvwxyz!@#$%&*';
  Start := TimeGetTime;
  for i:=1 to Times do
    ReverseString(S);
  Start := TimeGetTime-Start;
  writeln(S);
  writeln(Start);
  readln

end.

The ReverseString procedure is too big to result in any performance gain by eliminating the call, and inlining it can lead to bigger code if you call it more than once. Moreover, the performance actuallywill decrease when adding the inline directive to the Swap procedure.

Finally, apart from using the explicit inline directive, we can also use {$INLINE AUTO} as compiler directive. This will leave it up to the compiler to select routines for inlining that are likely to improve your performance. Using {$INLINE ON} you specify that a set of routines will all be inlined from that point on, and with {$INLINE OFF} you specify that the routines from that point on will explicitly not be inlined. By default, routines will not be inlined, unless you have explicitly specified inlining (or INLINE is set to AUTO).

There are a number of exceptions regarding routines that cannot be inlined by the compiler. Although you can inline routines from different units in a package (assembly), you cannot inline routines across package boundaries, for example. It's also not possible to inline virtual, dynamic, or message methods, as well as methods of interfaces and dispinterfaces.

Delphi 2005 Debugger Enhancements

The first inline code example in the .NET version of the InlineDemo project is a perfect candidate to also demonstrate some debugger enhancements.

- Set a breakpoint at line 28, which contains the for-loop, as well as line 29, which performs the "call" to the Max function.

- Do View | Debug Windows - Breakpoints and watch the list of breakpoints.

Although you can still right-click on a breakpoint in the Breakpoint list to edit the breakpoint properties, we also can directly edit and modify the most important properties directly from the Breakpoint list itself.

- Click on the condition field of the second breakpoint and enter "i = 42" as condition for this breakpoint.

- You can enable or disable the breakpoint by checking or unchecking the checkbox on the left. Uncheck the checkbox for the second breakpoint.

Delphi 2005 Breakpoint List

Optionally, you can specify the Group to which the breakpoints belong (all directly from the Breakpoint list dialog).

Now that the breakpoint on line 29 has been disabled, we have only one breakpoint left. If we break there, we can inspect the CPU view to see the mixed source code around the breakpoint.

- Compile and Run the application (press F9). The application will break at the start of the for-loop.

- Do View | Debug Windows - CPU. This will show the CPU window as shown below.

Delphi 2005 Debug Window CPU View (mixed code)

Note that apart from mixed Delphi and ASM code, you can also view IL code if you wish (this option is selected by default, but then the display of the inlined Max would not fit in the screenshot; feel free to experiment with the mixed source and IL view for yourself).

Multi-unit namespaces

The previous version of the Delphi for .NET compiler used a mapping of one unit per namespace (where the name of the unit would be the name of the namespace). This has been expanded in Delphi 2005, where a namespace can now be made up of several units.

- Start Delphi 2005.

- Do File | New - Other, select Package from the Delphi for .NET Projects category, resulting in a .NET assembly project.

Delphi 2005 Object Repository - Delphi for .NET Projects

The package project is empty, contains no units, and requires only the Borland.Delphi.dll assembly.

- Do File | Save Project As, and specify MyUtilities.dpr as filename for the new .NET assembly project.

- Do File | New and select "Unit - Delphi Win32" (this produces a simple unit that can also be used in Delphi for .NET applications and packages of course).

- Do File | SaveAs and specify MyUtils.One.pas as filename for the first unit.

- Do File | New and select "Unit - Delphi Win32".

- Do File | Save As and specify MyUtils.Two.pas as filename for the second unit.

The Project Manager now displays the MyUtilities .NET assembly project which contains two units: MyUtils.One.pas and MyUtils.Two.pas, as shown below:

Delphi 2005 Project Manager for .NET package

Compiling the project results in an assembly MyUtilities with a namespace MyUtils that consists of two units: MyUtils.One and MyUtils.Two. We can now add content to the two units, for example, class definitions and implementations.

- Edit MyUtils.One.pas and enter the following code:

unit MyUtils.One;

interface

type
  TMyClass = class
    function Max(const X,Y,Z: Integer): Integer; inline;
  end;

implementation

{ TMyClass }

function TMyClass.Max(const X, Y, Z: Integer): Integer;

begin
  if X > Y then
    if X > Z then Result := X
             else Result := Z
  else
    if Y > Z then Result := Y
             else Result := Z

end;

end.

This will define a class called TMyClass in namespace MyUtils (from a Delphi point of view just in unit MyUtils.pas).

- Add some different code to the MyUtils.Two.pas unit, for example as follows:

unit MyUtils.Two;

interface

type
  TMyClass2 = class
    function Min(const X,Y,Z: Integer): Integer; inline;
  end;

implementation

{ TMyClass }

function TMyClass2.Min(const X, Y, Z: Integer): Integer;

begin
  if X < Y then
    if X < Z then Result := X
             else Result := Z
  else
    if Y < Z then Result := Y
             else Result := Z

end;

end.

*Note that the MyUtils.Two.pas unit contains a class called TMyClass2, with a method Min (instead of a class TMyClass with a method Max which is defined in unit MyUtils.One.pas).

- Save and compile the project to the MyUtilities.dll assembly.

- Do Tools | Reflection to start the Borland Reflection tool.

- Click on the Open button of the Borland Reflection tool and open the MyUtilities.dll assembly.

- Open the MyUtils namespace, and then the TMyClass as well as TMyClass2.

Click to see full-sized image

Reflection showing TMyClass and Max method

*Note that both contain their method (Max and Min) as well as additional information (introduced by the Delphi class helpers), and both TMyClass and TMyClass2 are part of the MyUtils namespace.

The benefit of having the ability to add multiple units to a single namespace can be very significant. Especially for ASP.NET custom control developers, who might want to place each custom control in its own unit but still specify just one ASP.NET prefix in the single namespace that contains all these custom controls.

There is one thing to be aware of: you can accidentally add duplicate types or symbols to the same namespace. For example, if we had given TMyClass2 the same name as TMyClass. This will compile and work just fine inside Delphi (because TMyClass can be used from either the MyUtils.One unit or the MyUtils.Two unit, and the last one in the scope is the one that defines which TMyClass to use).

However, if we have defined a TMyClass in both MyUtils.One.pas and MyUtils.Two.pas, then using the resulting MyUtilities.dll assembly from another .NET development environment will fail. In fact, PEVerify also will fail and point out the problems, which can be demonstrated as follows:

- Change the TMyClass2 to TMyClass in MyUtils.Two.pas and recompile MyUtilities.dll.

- Open a command prompt.

   Go to the directory where the MyUtilities.dll assembly is located.

- Run PEVerify on MyUtilities.dll.

This will produce the following output (with TMyClass in MyUtils.One.pas and MyUtils.Two.pas):


Microsoft (R) .NET Framework PE Verifier  Version 1.1.4322.573
Copyright (C) Microsoft Corporation 1998-2002. All rights reserved.

[MD]: Error: TypeDef has a duplicate based on name+namespace, token=0x02000003.  [token:0x02000002]
[MD]: Error: TypeDef has a duplicate based on name+namespace, token=0x02000002.  [token:0x02000003]
2 Errors Verifying c:MyUtilities.dll

Although this will tell you that there are two typedefs that are duplicates of each other, PEVerify does not tell you their names. For the current example, it should be easy to see that TMyClass is duplicated, but for more real-world-sized projects, it might not be as obvious (hint: we need to look for tokens 0x02000002 and 0x02000003).

- In order to find the duplicate typedefs, start ILDASM.

- In IL DASM, do File | Open and select the MyUtilities.dll assembly.

- Do View | Show Token Values.

- Do File | Dump and specify that you want to dump IL Code complete with the Token Values.

- Now, inside the dump file, look for the token values that PEVerified mentioned as being the source of duplication (in our case tokens 0x02000002 and 0x02000003).

Once you have found the duplicate typedefs, you can change them to solve the problem.

Dynamic multidimensional arrays

The Delphi for .NET compiler now supports dynamically allocated multi-dimensional arrays using the new keyword. We will demonstrate this using a new Delphi for .NET console application.

- Do File | New - Other, and from the Object Repository start a Console Application from the Start a Delphi for .NET projects.

Delphi 2005 Object Inspector - Delphi for .NET console application

- Save the project as MultiArray and write the following code:

program MultiArray;
{$APPTYPE CONSOLE}

var
  X: array[,] of Integer; // 2 dimensional array

- This defines a two-dimensional array without specifying the size. For that, you must write some additional code:

begin
  X := New(array[2,3] of Integer); // 2x3 matrix

- You can now use X[0] and X[1] as single-dimensional integer arrays with three values (from 0 to 2), for example as follows:


  X[0,0] := 1;
  X[0,1] := 2;
  X[0,2] := 3;
  X[1,0] := 4;
  X[1,1] := 5;
  X[1,2] := 6;

end.

Apart from passing the size for the dynamic array dimensions in the call to New, you can also specify just the type and a set of initializers.

- To demonstrate the other way of calling New, extend the console application with the following code.

program MultiArray;
{$APPTYPE CONSOLE}

var
  X,Y: array[,] of Integer; // 2 dimensional array
  i,j: Integer;
begin
  i := 2;
  j := 3;
  X := New(array[i,j] of Integer); // only size of 2x3 matrix
  X[0,0] := 1;
  X[0,1] := 2;
  X[0,2] := 3;
  X[1,0] := 4;
  X[1,1] := 5;
  X[1,2] := 6;
  // type and initializer list
  Y := New(array[,] of Integer, ((1,2,3), (4,5,6)));
  for i:=0 to 1 do
    for j:=0 to 2 do
      if X[i,j] <> Y[i,j] then writeln(i,j);
  writeln('done');
  readln;

end.

This application demonstrates the two different ways of dynamically allocating multidimensional arrays with the Delphi for .NET compiler.

Delphi Win32 Language Enhancements

The Delphi for Win32 compiler supports the new for-in-do loop, function and procedure inlining, and multi-unit namespaces. It does not support the dynamic multidimensional array syntax using the new keyword. Using the Delphi for Win32 compiler, you must specify the following code construct (which actually works in both .NET and Win32):

var
  X: array of array of Integer;

begin
  SetLength(X, 2);
  SetLength(X[0], 3);
  SetLength(X[1], 3);
  X[0,0] := 1;
  X[0,1] := 2;
  X[0,2] := 3;
  X[1,0] := 4;
  X[1,1] := 5;
  X[1,2] := 6;

end.

Conclusion

This tutorial introduces the compiler language and debugger enhancements in Borland Delphi 2005 and demonstrates the new language features and debugging capabilities. We've seen the new for-in-do loop, function and procedure inlining, multi-unit namespace support, dynamic multidimensional arrays as enhancements in the Delphi for .NET compiler, as well as the new debugging features working with breakpoint list and new debug windows.


Server Response from: ETNASC03