2004. augusztus 3., kedd

Tips for Converting VCL Components to VCL.NET

Problem/Question/Abstract:

Tips for Converting VCL Components to VCL.NET

Answer:

Delphi 8http://www.borland.com/delphi_net/ (actually, Borland® Delphi™ 8 for the Microsoft® .NET Framework) introduces Delphi developers to the .NET world (and .NET developers to Delphi). Delphi 8 allows developers to create native .NET applications using any of the .NET framework's classes, including the standard Windows Forms user interface controls and designers. It also provides a migration path for existing applications and components using the new VCL.NET framework. Using VCL.NET, many existing Delphi applications will port to .NET with little or no modifications.

The story is a little different for components. Components tend to be "closer to the metal", invoking system calls, managing memory and object lifetime, in generally doing things we want to hide from normal application code. Component need to be aware of the platform's architecture, and may require substantial work to function properly or even compile in the new version.

In this article I'll show some of the conversion issues I've encountered while working on a VCL.NET version of some of my components. Some may be obvious, while other are more subtle and can be the source of hard-to-find bugs.

Objects and Pointers

Let's start with the big one: managed codehttp://msdn.microsoft.com/library/en-us/netstart/html/cpglom.asp doesn't support pointers. Pointers allow unsupervised memory access, and are therefore considered unsafe. A lot of Delphi components rely heavily on pointers, and make implicit or explicit assumptions about their content and structure. This has quite a few implications for the conversion process.

Use TObject instead of Pointer

In Delphi for Win32 (and Kylix, for that matter), object references are simply pointers. When declaring an object-type variable, you're actually declaring a pointer variable. You can typecast your objects to pointers and vice-versa. Because of this, pointer variables and properties are widely used to store object references. Since unmanaged code no longer supports pointers, the first step in converting a component to VCL.NET is to search your code for pointers and change them to something else.

In most cases, you can safely replace pointers with object references. Remember that in .NET, everything is an object. However, there may be cases where you may need to refactor your code.

No more Integer typecasts

In 32-bit unmanaged code, a pointer is simply a 32-bit value. This means the Integer, Pointer, and TObject types are all interchangeable. Components often use this type compatibility, especially when using external code that expects a specific type. For example, consider the Win32 EnumWindows function:

function EnumWindows(lpEnumFunc: TFNWndEnumProc; lParam: LPARAM): BOOL; stdcall;
The function expects two parameters: a callback function and a second parameter, of type LPARAM, that will be passed back as a parameters to the callback function. LPARAM is defined as:

type
LPARAM = Longint;

A common usage of the lParam parameter is to hold an object reference. The callback function is written as a stub invoking a method of that object. Such code will have to be rewritten or marked as unsafe in Delphi 8.

The case of TList

A special case for integer typecasting is the TList class, one of the most widely used classes in the VCL. TList is a generic container. In Win32, TList holds pointers, since they can be easily typecast to both integers and objects. In Delphi 8, TList holds TObject references.

PChars are Gone

Like all pointers, PChars are no longer supported. Code that used character buffers, called Win32 API functions requiring strings, or passed character strings to external DLLs will no longer work.

API functions the required PChars in Win32 now accept standard Delphi strings. Functions that returned data into character buffers now accept StringBuilder objects.

Memory Allocation

Since pointers are gone, code that allocates and deallocates memory for records and buffers is no longer valid. Search for calls to GetMem, FreeMem, New, and Dispose - they should all go away.

Message Handlers

One type of code that is heavily used in components but rarely in applications is message handling. Visual controls handle Windows messages and internal VCL messages using message functions. VCL.NET allows you to use your existing message handling code, but introduces some quirks.

Message Types

Message handlers are invoked by TObject's Dispatch method. Visual controls call Dispatch in their WndProc method, passing a TMessage record as Dispatch's only parameters. The Dispatch method can accept any parameter type, but assumes the first two bytes of the referenced parameter contain the message ID.

In unmanaged Delphi code, the type of the message parameter doesn't really matter. Since WndProc calls Dispatch with a TMessage parameter, as long as you're using a compatible record you'll be fine. Not so in Delphi 8. Consider the following code:

type
TForm1 = class(TForm)
private
procedure WMNCPaint(var Message: TMessage); message WM_NCPAINT;
end;

{...}

procedure TForm1.WMNCPaint(var Message: TMessage);
begin
inherited;
end;

This code compiles just fine on any version of Delphi. When compiled to a Win32 application, the code runs just fine. It doesn't really do anything with the WM_NCPAINT message, so nothing should go wrong. In Delphi 8, however, running the application produces a System.NullReferenceException exception. Since the exception is raised deep in Delphi's RTL, it is almost impossible to debug. The solution is deceptively simple, though:

type
TForm1 = class(TForm)
private
procedure WMNCPaint(var Message: TWMNCPaint); message WM_NCPAINT;
end;

{...}

procedure TForm1.WMNCPaint(var Message: TWMNCPaint);
begin
inherited;
end;

This code works in any version of Delphi, including Delphi 8. All we had to do is change the message record type to TWMNCPaint. A little annoying, but fairly easy - once you know about it.

A bit more annoying is the fact that this behavior doesn't affect every message, just some of them. A message handle for WM_NCACTIVATE, for example, is perfectly happy accepting a TMessage record, or in fact any other message record, such as TWMNCPaint. Try it.

Once again, the answer is obvious once you already know it: WM_NCPAINT is already handled in one of TForm's (or any other visual control's) ancestors - TWinControl, where it expects a TWMNCPaint parameter. If your component handles a message that is also handled by an ancestor, and calls the inherited handle, you must use the same message record type.

Object References

Windows messages contain data as two integers, historically names wParam and lParam. These are often used as pointers to more complex data structures. Since normal .NET applications don't use pointers, VCL.NET has to perform some behind-the-scenes magic to convert references to integers and manage their lifetime. One side-effect of this magic is that the information passed by certain messages requires additional handling. Let's take a look at the CM_HINTSHOW message, send by the VCL before displaying a hint:

type
TForm1 = class(TForm)
private
procedure CMHintShow(var Message: TCMHintShow); message CM_HINTSHOW;
end;

{...}

procedure TForm1.CMHintShow(var Message: TCMHintShow);
begin
inherited
Message.HintInfo.HintStr := 'This is my hint';
end;

This code, which works great in Win32, doesn't even compile in Delphi 8. In Delphi 8, TCMHintShow is an object, and HintInfo is a property of that object. The setter method for the HintInfo property can only take an existing HintInfo reference, so you can't simply assign values to HintInfo's members. You have to use a separate reference variable:

procedure TForm1.CMHintShow(var Message: TCMHintShow);
var
HintInfo: THintInfo;
begin
inherited;
HintInfo := Message.HintInfo;
HintInfo.HintStr := 'This is my hint';
end;

This code compiles, but still doesn't work. A little more tweaking, and we get:

procedure TForm1.CMHintShow(var Message: TCMHintShow);
var
HintInfo: THintInfo;
begin
inherited;
HintInfo := Message.HintInfo;
HintInfo.HintStr := 'This is my hint';
Message.HintInfo := HintInfo;
end;

We need to explicitly set the HintInfo property to copy the modified data to a record VCL.NET can pass around using messages. This is what the last line does.

FCL Types

The .NET Framework Class Library (FCL) contains many types that are similar to Delphi's standard types. In Delphi for .NET, standard types are mapped to their .NET equivalents. For example, the string type is mapped to the FCL's System.String class (although Delphi extends string handling to support the language syntax), and the Integer type is mapped to System.Int32. Other types, such as TDateTime, have been reimplemented for .NET.

TDateTime

In Delphi for Win32 (and Kylix), TDateTime is defined as a Double (64-bit floating-point number). Date and time information is stored as the count of days since midnight on 30-Dec-1899.

In Delphi 8, TDateTime is a record, declared in the Borland.Delphi.System unit. TDateTime handles implicit conversions to and from Double values, so existing code that assumes TDateTime is a Double value compiles without warnings. Implicit conversions to System.DateTime and standard operators are also implemented, so Delphi's TDateTime type is fully accessible from other .NET languages. Internally, TDateTime stores its value using the System.DateTime type.

The problem is that System.DateTime was designed to hold dates and times, while Double (the original TDateTime) was designed to hold floating-point numbers. And sometimes, Delphi coders relied on this small, but critical, implementation detail. Since TDateTime was a double, it was easy to perform arithmetic operations on it - for example, subtracting one TDateTime from another to get the difference between them in days. Occasionally, the difference was negative (the first date was later then the second date), and if your code allows that, it will no longer work. Instead, you'll get the absolute value of the difference.

To work around this, you'll have to use actual Double variables instead of TDateTimes. You can use TDateTime's ToOADate method to get a value compatible with previous versions of Delphi.

Conclusion

Delphi 8 for .NET is a great product, letting Delphi developers move into the .NET world while using their existing skills and re-using their existing code. Still, .NET and Win32 are two different platforms, and the transition requires extra caution when trying to use skills and processes which were valid in the past.

Borland has made porting code to VCL.NET a fairly smooth process, but one cannot expect 100% portability between the platforms. I have shown some of the issues I've encountered. I'm sure there are more.

In addition to this article, be sure to read the Delphi 8 help topic, "Language Issues in Porting VCL Applications to Delphi 8 for .NET". It's a little hard to find (it doesn't seem to be in the contents or the index). If you have Delphi 8 installed, you can find it at ms-help://borland.bds2/bds2guide/html/LanguageIssues.htmms-help://borland.bds2/bds2guide/html/LanguageIssues.htm.



Nincsenek megjegyzések:

Megjegyzés küldése