Converting from Delphi Win32 to Delphi.Net
There may be several reasons for deciding to move your codebase from win32 to .Net. Delphi users have a very unique opportunity to achieve this transition with much less effort than any other languages. However, beneath the apparent ease of conversion, there are several gotchas we need to be aware of. Some of them are simple compilation issues while others could cause very hard to debug runtime errors and performance issues.
While a simple recompiling of the code will be a good start for a conversion project, I strongly suggest you consider a refactoring as well. Package (assembly) management under .Net is much cleaner than under win32. .Net also provides better ways of modularization like a better reflective language, better managed interfaces, assembly loading strategies etc. In our particular case, we decided to maintain dual builds during most of the conversion process, which is crucial for us to have a production quality version of codebase at all times. If you are not planning for dual builds, you will need a full development cycle that guarantees feature parity between the win32 and .net versions before new features can be put in.
Any changes that go beyond type declarations and well known transformations must be covered by Unit tests provided you can execute them in both win32 and .Net. Chances are that, you will be making a lot of changes during the conversion process. Being able to prove your significant changes saves a lot of hard debugging time later.
Any codebase under continuous development will gather deep inter-linkages between modules that are hard to isolate and resolve. Rebuilding codebase under .Net gives you an opportunity to isolate these and similar issues. Instead of reusing the existing package projects (assuming you have a multi-package structure), start by creating a logical package structure and pull units into these packages starting from a known root level unit. A root level unit will have dependencies only to VCL and other core never-built libraries.
Since .Net allows multi-part package names, make use of them. Instead of calling your db access library as dbaccess.dpk, name it <mycompany>.<myapp>.DBAccess.dpk. You can match the package names to the default namespace of the package itself. You can change the default namespace for a package in the options dialog. I havenâ€™t made a decision on the propriety of multiple packages defining same namespace. This could lead to confusion if you have large number of packages. However, you can use it to break dependency between two application areas which are logically in the same namespace.
Donâ€™t expect wonders and miracles from the conversion. Your ugly forms will still look ugly, your bad performance could get worse, and you will be faced with a whole new set of idiosyncrasies. I believe that the advantages from .Net far outweigh these risks. But that is for another note.
Following is a compilation of issues, tips and tricks I collected during our .Net conversion process. They are not in any particular order. I will certainly be adding more to this list.
How to prepare for Delphi Win32 to Delphi.Net conversion
1. Get a good refactoring tool. The D 2005 has some refactoring capabilities, but you will need more than what it can do. Especially for moving, copying and pasting methods, modifiers and property accessors. We using Model Maker Code Explorer. The only issue I have found so far is that it adds fully qualified unit names (eg. Borland.Vcl.Classes even when you have the unit classes in your uses list) to the uses clause when you use a known class from the classes unit. I end up removing these as they break both .net and win32 build
2. Try to split your application/source code into smaller packages. Package management in Delphi.NET is much less problematic compared to previous versions of Delphi
3. Write as many unit tests as you can under win32. It will give you an idea of where the most painful dependencies are. This is very valuable information for your package design.
4. Scrub your code for compiler warnings, especially the ones about invalid type casts and platform independence. Most of these warnings have been promoted to errors under .Net.
5. If you are using third-party components, make sure they have a vcl.net version available. If not, make sure you have their source code and it is not too hard to compile under .Net. For example if you see a lot of pointer manipulations, windows API calls or ASM keywords in your third-party source, be very scared!!!
6. Get a fast machine, ideally a multi-core and have 1GB or even better 2GB RAM. Delphi 2005 and D 2006 are memory hogs. I routinely see close to 1GB used up by BDS during a long change/rebuild cycle.
7. Use DUnit and write tests for any unit that can be tested. If the unit cannot be tested, try similar constructs in test before making the change itself.
8. If you do not have a source control, install one. There are several open source options available. If you have MSDN subscription, try using Team Foundation Server. I have grown to appreciate the nice features TFS provides especially for an agile development environment.
9. Create a project group that contains everything you are compiling. This could be a bit of a load for Delphi, so go for that 2+GB.
10. Create a command-line script for compiling your project group from command line. Compiling from IDE is much slower than command line compiles.
11. Add your unit test projects to the command line compile and run them as part of the build.
12. Be ready for some boring work. Make sure that you have whatever keeps you alive on those endless build/fix cycles. May be you can take a cruise!!
Here is another one for the elusive "Conversion Guide from Delphi win32 to Delphi.Net" that Borland/Codegear has never written.
TClass had this nice virtual NewInstance method which would call the default constructor. We used this in a lot of factories. However, understandably so, Delphi.Net does not have a NewInstance method.
I imagine it would be awkward to keep this behavior under .Net. So, we will need to use other means for achieving the result. It did take some poking around to finally figure out how this is done. Well, it was not that hard once I looked in Borland.Vcl.Classes, the TReader method to construct a new instance. So, my solution was to rewrite the factory methods similar to the following.
function MyClassFactory.CreateInstance( aName : string ): TMyClass;
var params : array of System.Type;
paramValues : array of TObject;
aConstructorInfo : System.Reflection.ConstructorInfo;
SetLength( params, 0 );
SetLength( paramValues, 0 );
aConstructorInfo := getClass( aName ).ClassInfo.getConstructorInfo(params);
Result := TMyClass( aConstructorInfo.Invoke(paramValues));
In my case, the constructors did not have any parameters. If you are expecting parameters to your constructor, fill the params array accordingly. Unfortunately, this will not compile in win32. There goes the first IFDEF CLR.
Specializations of TMessage... a good thing, but!!!
To ease the marshalling requirements of sending messages, a large number of windows messages have been converted to specialized message types in Delphi.Net. However, this has introduced an interesting problem in conversion.
We have several components that override and redeclare message handlers. One of them is a CMParentFontChanged. The original declaration was
procedure CMParentFontChanged( var Message: TMessage ); message CM_PARENTFONTCHANGED;
In the implementation, it calls inherited, which has the following declaration.
procedure CMParentFontChanged(var Message: TCMParentFontChanged); message CM_PARENTFONTCHANGED;
The compiler will cast your TMessage to a TCMParentFontChangedMessage, resulting in a null reference under .Net. So, I was getting these mysterious â€œobject reference not pointing to an instanceâ€ errors. (I still havenâ€™t figured out how to make D2005 debug into .Net VCL source. That would have helped me figuring this out easier.) My resolution was to change the type of the inherited message handlers. If you have a lot of message handlers, this could become a headache.
Some interesting tricks that are biting back!!!!
In Delphi win32, explicit casts were not type enforced - meaning, you can cast an object reference to another type even when they are not compatible. While in most cases this would be disastrous, in one of the third party components we have, there was this interesting piece of code. It was trying to access the color of the parent to set its own color. However, since the parent is a TCustomControl, which doesnâ€™t have a color property, it cast the parent to TCustomPanel in one case and TGroupBox in another both of which have a color property, and this worked fine.
When converted this code to .Net, it did compile. However, since the explicit cast is strictly typed (same as the as cast) the result of the cast was a nil, which of course created the "Object reference not blah blah"
Full reflective nature of .Net makes it easier to handle such situations. Instead of casting to an arbitrary class, use reflection to get PropInfo for the property you want to set or get. This technique can be used for even non-published properties under .net.
Following code snippet shows setting a text value to an arbitrary property of the control.
class procedure TMyControlhelper.SetString(aControl: TWinControl;
const propName: string; aValue: string);
PInfo : PPropInfo;
PInfo := GetPropInfo ( aControl.ClassInfo, propName );
if ( PInfo <> nil ) then
if ( PInfo.TypeKind = TTypeKind.tkString) then
SetStrProp( aControl , PInfo, aValue )
Please note that PPropInfo is System.Reflection.MemberInfo and thus a full citizen of.Net reflection. You can use a similar method to get the values.
A small wrinkle to the above is when you need to set a delegate (event handler) the same way. In Win32, you can use a TNotifyEvent and cast it to a TMethod for setting a event handler. For e.g
SetMethodProp(aControl, propName, TMethod(aValue)); // win32
Where aValue is of type TNotifyEvent (or some other method of object type). However, this is not true under Delphi.Net. TMethod is a whole new class. It took me sometime to figure out how to resolve this issue. Finally, I found out, almost by accident that the reference of a TNotifyEvent is actually a TMethod. So, under .Net my code looks like:
SetMethodProp(aControl, propName, @aValue); // clr
1. Variant: As usual, there are several classes and constants that have changed units. The primary one is Variant. Variant is no longer declared in System. It is declared in variants. Well, a variant is just a TObject with a very interesting ObjectHelper. By the way, if any of you want to know how to write implicit conversion routines in Delphi, Look in the variants unit, TVariantHelper object. Really interesting stuff.
1. HRESULT. I have no clue where this constant was before. Now it is in Delphi.VCL.Windows
2. If any of you were being too smart by appending unit names to functions, here is a gotcha. If any of the system units where qualified ( for e.g System.copy instead of just copy), it is not going to work. System now refers to the System unit of Framework. Borland's system unit is called Borland.Delphi.System. So, if you really want to qualify a function from Delphi's system unit, write Borland.Delphi.System.Copy.
3. HInstance: I had to do a search the whole source directory for this. It is in WinUtils.
4. Where is the Point!!! : The Point and Rect routine has moved to Borland.Vcl.Types. Well many other things have moved there. For e.g TPoint declaration. But there is a typedef in Windows unit that refers to this TPoint
Simple issues in class declarations
Unsafe Methods: In our codebase, there were some methods I wanted to use direct pointer manipulations. These methods required adding the unsafe keyword to it.
Inherited Constructor: Most of us donâ€™t follow the good programming practice of calling inherited Create in the constructors. For Win32 it was optional, and you could decide where to call it from within the constructor body. Under Delphi.Net, you must call the inherited Create before trying to access local fields and functions. It luckily shows as a compile time error.
Class methods as event handlers: It is possible to use class methods as event handlers/delegators. You cannot use a standard Delphi class method as a delegate under .Net. Delphi class methods are a very Delphiish
Visibility of Overriding Method: When the visibility of an overriding method in a descended class differs from Parent, Delphi win32 used to give a warning. Changing the visibility of an inherited method is no longer a warning, it is a compiler error. Only way to change visibility seems to be reintroducing.
Potentially polymorphic constructors should be virtual. That sure sounds mysterious!!! This is a compiler error I got. The offensive call was trying to create a class by using a variable of the type of that class. ( for e.g, FooClass = class of TFoo ). Changed the constructor to virtual and the compiler was happy.
Constructor is where the class is constructed: In Delphi win32, constructor was just a method that the compiler called during the creation of the object. The object is already allocated and initialized by the time you get to the constructor. However, in Delphi.Net, like any other .Net language, constructor is where the whole thing happens. Until you call the inherited constructor, the instance will not be initialized. There were several cases where the constructor was making use of instance variables and methods. In one case, the constructor was calling the inherited constructor passing to it an instance method as a delegate. This however was correctly flagged as an error (unlike Java!!!) by the compiler. The resolution of that issue was not very simple.
The change in constructor behavior has caused us more problems than simple compilation issues. It is common to use constructor as an initialize in Delphi Win32, which it was. In an ideal world, no one should be doing any initialization in a constructor, but in Delphi, the temptation was hard to resist..Net requirements could derail initialization dependencies and create hard to track bugs. If you confront a class hierarchy that has substantial amount of initialization code in constructors, you should try to refactor the initialization code out of the constructor to a virtual instance method called from the base constructor. This will give us a chance to verify the behavior before the actual conversion.
A related issue is dependency injection. A large initialization block is usually a symptom of arbitrary dependencies. If this is happening in your code, consider one of those dependency injection patterns.
Changed class internals
We are all humans and end up using some of the undocumented internals of VCL classes for convenience or tweaking performance. One such usage I found was of the internal pointer list that a TList holds. Accessing the values directly from internal list is slightly faster as it does not do bound checking. Under .Net, this internal list is a System.Collections.ArrayList. Furthermore, it does not do a bounds check before accessing the element of the internal list. You can either decide to sacrifice the performance gain in win32 code and access the items directly, or use compiler directive to direct access the items under .Net.
Variants are first class objects. All the variant magic is now simple operator overloads. Other than the requirement for adding the Variants unit where there are implicit casts of Variants to other types, I havenâ€™t encountered anything that will break the conversion.
TGUID: This class is now just a System.Guid and does not have the internal D1, D2 etc. fields that the win32 TGuid structure has. There was one case where we were ascertaining if a Guid was empty by comparing the D1, D2 etc. to zero. This technique obviously wonâ€™t work under .Net. Following is what I ended up doing.
NULLGUID = '00000000000000000000000000000000';
Result := aGuid.Equals( TGUID.Create( NULLGUID ) );
TStringList.GetText and TStringList.SetText are not public anymore. I am not sure why someone want to use the methods instead of the Text. I do remember one time when the strings where only 256 bytes, GetText provided a way to get a PChar of the complete text representation of a TStringList larger than 256 bytes.
TWinControl. CMFocusChanged is gone. Instead it declares a virtual method FocusChanged( newFocusControl: TWinControl ). One of our TWincontrol descendants was handling this message, which needed to be changed to the overridden method.
Delphi Exception class under .Net is a redeclaration of System.Exception. This is a welcome change as we no longer need those weird third party contraptions or home grown stack tracing mechanisms. System.Exception class contains everything you need to know about the exception. For example, to get the stack trace just access Exception.StackTrace property. One change that might affect existing code is that Message property is now read only. You cannot append to the message property of an instance of an exception to provide additional information. However, System.Exception has an InnerException property which contains additional exceptions that were thrown before this one.
TList and ValueType Pointers
TList is a very popular container class for Delphi programmers. One common use for TList is to hold valuetype references, typically to native types. These constructs will take the address of the passed in value (integer, float, TDateTime etc.) and store it as a TList item. When the value is read, it will be dereferenced to the corresponding type.
There are two major differences in .Net. TList is now an array list that can hold any System.Object. Secondly, you cannot use the address operator on a ValueType.
If you want create a pointer to a ValueType, you will need allocate outside managed memory and marshal it back and forth. In a similar case, I considered three different approaches. First one was using marshalling. I soon found the horror in that approach. Second approach was to use Variants to hold value instead of a pointer. Third approach is to use Boxing; a technique .Net uses to make ValueTypes to behave as an object reference. Though Delphi.Net does not support auto-boxing, you can use a simple cast to box the value type, for example, you can make an integer into a boxed integer using TObject(value). Just to be sure, I wrote a small test that compares the three options. As expected, marshalling found to be the slowest approach followed by Variants. Boxing approach seems to introduce the least amount of overhead in the above scenario. Using Variants provide additional support for type checking and safe conversions. If you require this, use variants.