RAD Studio for Microsoft .NET
|
The VCL in RAD Studio was created with backward compatibility as the primary goal. However, there are some ways in which the managed environment of .NET imposes differences in the way VCL applications must work. This document describes most of these differences, and indicates some of the steps you should take to port a VCL application to the .NET environment.
This document does not attempt to describe the new extensions to the Delphi language. It is limited to the way existing Delphi code maps to the new RAD Studio language and VCL framework. This document does contain links into specific topics within the Delphi Language Guide, where new language features are explained in detail.
This topic covers the following material:
Pointer types are not CLS compliant, and are not considered "safe" in the context of the .NET Common Language Runtime environment. The port of the VCL has, therefore, eliminated pointers, replacing them with appropriate alternatives such as dynamic arrays, indexes into an array or string, class references, and so on. When porting a VCL application, one of the first steps is to locate where you use pointer types and replace them as appropriate.
Untyped pointers are considered unsafe code. If your code includes untyped pointers, the .NET utility PEVerify will fail to verify it. Code that cannot be verified for type safety cannot be executed in a secured environment, such as a web server, SQL database server, web browser client, or a machine with restricted security policies.
In the VCL, untyped pointers have been replaced with more strongly-typed values. In most cases, where you used to find an untyped pointer, you will now find TObject. For example, the elements of TList are now of type TObject, rather than of type Pointer. Your code can cast any type to an object, and cast a TObject to any other type (even value types such as Integer, Double, and so on). Casting TObject to another type will generate a runtime error if the object is not, in fact, an instance of the type to which you are casting it. That is, this cast has the same semantics as using the as operator.
In some cases, the Pointer type has been replaced with a more precise type. For example, on TObject, the ClassInfo function returns a value of type Type rather than an untyped pointer.
Untyped pointers that were used for parameters whose type varied depending on context have typically been replaced by overloading the routine and using var parameters with the possible types. In the case of untyped pointers that are used with API calls to unmanaged code (such as the Windows API or calls to a data access layer such as the BDE) the untyped pointer is replaced with System.IntPtr. Thus, for example, the TBookmark type, defined in the Db unit, now maps to IntPtr.
Code that used the address operator (@) to convert a value to an untyped pointer must now change. When the untyped pointer has changed to TObject, usually all you need to do is eliminate the @ operator. On value types, you may need to replace the @ operator with a typecast to TObject, so that the value is "boxed". Thus, the following code
var P: Pointer; I: Integer; begin I := 5; P := @I;
could be converted to
var P: TObject; I: Integer; begin I := 5; P := TObject(I);
When the untyped pointer has changed to IntPtr, you need to use the Marshal class to allocate a chunk of unmanaged memory and copy a value to it, rather than just using the @ operator. Thus the following code:
var P: Pointer; R: TRect; begin R := Rect(0, 0, 100, 100); P := @R; CallSomeAPI(P);
would be converted to
var P: IntPtr; R: TRect; begin R := Rect(0, 0, 100, 100); P := Marshal.AllocHGlobal(Marshal.SizeOf(TypeOf(TRect))); try Marshal.StructureToPtr(TObject(R), P, False); CallSomeAPI(P); finally Marshal.FreeHGlobal(P); end;
A special case for untyped pointers is when they represent procedure pointers. In managed code, procedure pointers are replaced by .NET delegates, which are more strongly typed. Declarations of procedural types are delegate declarations in RAD Studio. You can obtain a delegate for a method or global routine using the @ operator. The code looks the same as obtaining a procedure pointer on the Win32 platform, so in many cases there is nothing you need to change when porting code. However, it is important to keep in mind that when you use the @ operator, you get a newly-created delegate, not a pointer.
If you are passing a procedure pointer to an unmanaged API using the @ operator, for example,
Handle := SetTimer(0, 0, 1, @TimerProc);
the only reference to the delegate is the one passed to the API call because the delegate is created on the fly. This means that the garbage collector will eventually dispose of the delegate after the return of the unmanaged API. If, as in this case, the unmanaged code may call the procedure after the return of the API call, you will encounter a runtime exception because the delegate no longer exists. You can work around this situation by assigning the delegate to a global variable, and passing the global variable to the unmanaged API.
When you call the Windows API GetProcAddress to obtain a procedure pointer, it is returned as an IntPtr. This value is not a delegate. You can’t cast it to a delegate and call it. Instead, typically such code is translated to use Platform Invoke to call an unmanaged API. GetProcAddress is useful to determine whether the API is available so that you do not get a runtime exception when you use Platform Invoke. Thus, code such as the following:
type TAnimateWindowProc = function(hWnd: HWND; dwTime: DWORD; dwFlags: DWORD): BOOL; stdcall; var AnimateWindowProc: TAnimateWindowProc = nil; UserHandle: HMODULE; begin UserHandle := GetModuleHandle('USER32'); if UserHandle <> 0 then @AnimateWindowProc := GetProcAddress(UserHandle, 'AnimateWindow'); ... if AnimateWindowProc <> nil then AnimateWindowProc(Handle, 100, AW_BLEND or AW_SLIDE);
Would be translated to the .NET platform as follows
[DllImport('user32.dll', CharSet = CharSet.Ansi, SetLastError = True, EntryPoint = 'AnimateWindow')] function AnimateWindow(hWnd: HWND; dwTime: DWORD; dwFlags: DWORD): BOOL; external; var UserHandle: HMODULE; CanAnimate: Boolean; begin UserHandle := GetModuleHandle('USER32'); if UserHandle <> 0 then CanAnimate := GetProcAddress(UserHandle, 'AnimateWindow') <> nil else CanAnimate := False; ... if CanAnimate then AnimateWindow(Handle, 100, AW_BLEND or AW_SLIDE);
Code that uses the PChar type usually serves one of three purposes:
PChar version |
String version |
AnsiExtractQuotedStr |
AnsiDequotedStr or DequotedStr |
AnsiLastChar, AnsiStrLastChar |
(use index operator and string length) |
AnsiStrComp, StrComp |
CompareStr, AnsiCompareStr, WideCompareStr |
AnsiStrIComp, StrIComp |
CompareText, AnsiCompareText, WideCompareText |
AnsiStrLComp, StrLComp |
System.String.Compare (StartsStr) |
AnsiStrLIComp, StrLIComp |
System.String.Compare (StartsText) |
AnsiStrLower, StrLower |
AnsiLowerCase, WideLowerCase, |
AnsiStrUpper, StrUpper |
UpCase, AnsiUpperCase, WideUpperCase |
AnsiStrPos, StrPos, AnsiStrScan, StrScan |
Pos |
AnsiStrRScan, StrRScan |
LastDelimiter |
StrLen |
Length |
StrEnd, StrECopy |
(no equivalent) |
StrMove, StrCopy, StrLCopy, StrPCopy, StrPLCopy |
Copy |
StrCat, StrLCat |
|
StrFmt |
Format, FmtStr |
StrLFmt |
FormatBuf |
FloatToText |
FloatToStrF |
FloatToTextFmt |
FormatFloat |
TextToFloat |
FloatToStr |
When a PChar type is used to navigate through a string, you must rewrite the code, replacing the PChar with an Integer that represents an index into the string. When rewriting such code, you must is recognize when you have reached the end of the string. When using the PChar type, there is a null character at the end of the string, and the code typically recognizes the end of the string by finding this null character. With a string-and-index approach, there is no such null character and you must use the string length to identify the end of the string. Be careful to check that the index is not past the end of the string before reading a character or you will get a runtime error.
In Delphi for Win32, it is common to find code similar to the following:
S1 := 'This is a test string'; Stream.WriteBuffer(S1[1], Length(S1));
On the Win32 platform, this code results in the entire string being written to the stream. On the .NET platform however, this same code produces a quite different result. On the .NET platform, the compiler generates a call to the Char overloaded version of WriteBuffer, with the result being only a single character (S1[1]) being written to the stream.
Other typed pointers have been eliminated from the VCL. Typically, they are replaced by the type to which the original pointed. If the pointer type was the parameter to a procedure call, it is typically converted to a var parameter so that the resulting code still passes a reference rather than a copy of the argument. Sometimes, it is useful to change a value type into a class type so that rather than passing a typed pointer, your code passes an object reference.
In RAD Studio, the string type maps to the .NET String type, and you can freely access the members of String using a Delphi string type, as demonstrated in the following example:
var S: string; begin S := 'This is a string'; // Note the typecast is not necessary. // S := System.String(S).PadRight(25); // Direct access to string class members S := S.PadRight(25); S := ('This is a new string').PadRight(25);
The biggest difference for strings in RAD Studio is that the string type is now a Unicode wide string rather than an AnsiString. This simplifies code for some locales, because you no longer need to worry about multibyte character sets. However, you must examine your code for any assumptions about the size of a Char, because it is now two bytes rather than one. You can still use strings with one-byte characters, but you must now declare them as AnsiString rather than string. The compiler converts between wide and narrow strings if you use an explicit typecast or if you implicitly cast them by assigning to a variable or parameter of the other type.
If your code calls any of the AnsiXXX routines for manipulating strings, you may want to change these to the corresponding wide string version of the routine. The AnsiXXX routines have (deprecated) overloads that map to the wide versions, and the overloaded routines accept wide strings for their parameters; this avoids implicit conversion back and forth between wide and single-byte strings.
Following the CLR value-type semantics, typically operations on strings return a copy of the string rather than alter the existing string. This may make some code less efficient, because there is more copying going on. For example, consider the following:
var S: string; begin S := 'This is a string'; S[3] := 'a'; S[4] := 't';
When compiled using on the Win32 platform, the character substitutions only require a single byte of memory to change each time. In RAD Studio, each substitution results in a copy of the entire string. Because of this, it is a good idea to use a StringBuilder instance when you are manipulating string values. StringBuilder allocates a chunk of unmanaged memory and manipulates the string the way you expect. When you are finished, you can convert the result to a string by calling the ToString method.
In RAD Studio, an uninitialized string has the value of nil. The compiler will automatically compensate if you compare an uninitialized string with an empty string. That is, if you have a line such as
if S <> '' then ...
The compiler handles the comparison and treats the uninitialized string as an empty string. However, unlike code compiled on the Win32 platform, other string operations do not automatically treat an uninitialized string like an empty string. This can lead to Null Object exceptions at runtime.
Unlike Delphi for Win32, in RAD Studio, there is no distinction between an explicit typecast and the as operator. In both cases, the cast only succeeds if the variable being cast is really an instance of the type to which you cast it. This means that code which used to work (by casting between incompatible data types) may now generate a runtime exception.
Perhaps the most common situation where the change to typecasts causes a problem is in the use of the message cracker types. In the VCL on Win32, the Messages unit defined a number of record types to represent the parameters of a Windows message. These records were all the same size, with the fields laid out to extract the information from the Windows message. Thus, you could have the message parameters in one form (say, TMessage), and typecast it to another (say TWMMouse), and extract the information you wanted. This worked because the two types were the same size, and an explicit typecast did not raise an exception when you reinterpreted the type with the cast. Such a reinterpret cast is not allowed in .NET, and the same code would lead to an invalid cast exception in RAD Studio.
To work around this situation, the message cracker types in RAD Studio are not records at all, but classes. Instead of casting a TMessage value to another type such as TWMMouse, you must instantiate the other type, passing the original TMessage as a parameter. That is, instead of
procedure MyFunction(Msg: TMessage); var MouseMsg: TWMMouse; begin if Msg.Msg = WM_MOUSE then with Msg as TWMMouse do ... end;
you would do something like the following:
procedure MyFunction(Msg: TMessage); var MouseMsg: TWMMouse; begin if Msg.Msg = WM_MOUSE then with TWMMouse.Create(Msg) do ... end;
To convert in the other direction (from a specialized message type to TMessage), you can use the new UnwrapMessage function that is declared in the Messages unit.
Another technique that involves what is now an invalid typecast is when you need to access the protected members of a class that is declared in another unit. In Delphi for Win32, you can declare a descendant of the class whose members you want to see:
type TPeekAtWinControl = class(TWinControl);
Then, by casting an arbitrary TWinControl descendant to TPeekAtWinControl, you could access the protected methods of TWinControl, because TPeekAtWinControl was defined in the same unit.
In general, this technique does not work in RAD Studio, because the arbitrary TWinControl descendant is not, in fact, an instance of TPeekAtWinControl. The cast leads to an invalid cast exception at runtime.
Because this is a widely used technique in Win32, the compiler will recognize this pattern and allow it. However, the compiler can't know what assembly a unit will be linked into when it compiles the source code. If the units are linked into assemblies, this technique will fail at runtime with a type exception.
When you need to cross assembly boundaries, one workaround is to introduce an interface that provides access to the protected members in question. Some of the classes in the VCL ( TControl, TWinControl, TCustomForm) now use this technique, and you can find the addition of interfaces to access protected members (IControl, IWinControl, IMDIForm).
Specific language issues with programming in Delphi on the memory-managed .NET platform are explained in the topic Memory Management Issues on the .NET Platform.
Because of differences in the way objects are instantiated and freed, it is not possible to have a BeforeDestruction or AfterConstruction method on a RAD Studio class. Any classes that override these methods must be rewritten.
The fact that these methods and the OldCreateOrder property do not exist in the VCL on the .NET platform impacts forms and data modules that relied on OldCreateOrder being False. The OnCreate and OnDestroy events now act as if the OldcreateOrder property is set to True, and will only be called from the constructor or destructor.
Most of the VCL is designed for working with the Windows API. This is handled in a way analogous to the way Systems.Windows.Forms works: The VCL is a managed API that calls into the Windows API, marshaling between the managed structures on the VCL side and the unmanaged types that the Windows API uses. Some units, particularly in the RTL, have been ported so that they sit on top of CLR rather than the Windows API. Such units are more flexible, because they can work with any .NET environment, even those that do not support the Windows operating system (for example, the Compact Framework, Mono, and so on). Units that require the Windows operating system are tagged with the platform directive. In units that are not tagged with the platform directive, any methods or classes that require Windows are tagged with the platform directive.
In order to maintain relative platform independence in RTL units, some methods functions that rely on Windows have been moved into the WinUtils unit. In addition, some classes have been changed to rely more on CLR than Windows.
TObject, Exception, TPersistent, and TComponent, all map directly to classes implemented in the .NET Framework. In this way they integrate more smoothly with other .NET applications. Because the corresponding CLR classes (System.Object, System.Exception, System.Marshal, and System.Component) do not include all the methods that the VCL requires, the missing methods are supplied by Delphi class helper declarations. In most cases, this mechanism is transparent. However, there are a few cases where it requires you to make minor tweaks to your code. For example, with TComponent, FComponentState is now a property of TComponentHelper rather than a true field of TComponent. This means that you can’t use the Include and Exclude methods on FComponentState, because when passed a property, they operate on a copy of the property value, which does not alter FComponentState. Thus code such as
Exclude(FComponentState, csUpdating);
Must be rewritten as
FComponentState := FComponentState – [csUpdating];
TThread has also been changed to map to the CLR thread object. This means that the Thread handle is no longer an ordinal type, but is rather a reference to the underlying CLR thread object. It also means that TThread no longer supports a ThreadID, which is not supported by the CLR thread object. If your thread class requires a ThreadID, you should change it to derive from TWin32Thread instead.
Many Windows APIs have changed to use a more managed interface. Often, the types of parameters have changed, typically to eliminate pointers. One common change is the PChar types have been replaced by string or StringBuilder.
When your application calls a Windows API, it is making a call into an unmanaged DLL. Because of this, all parameter values must be marshaled into unmanaged memory, where Windows can work with it, and results are then unmarshalled back into managed memory. In most cases, this marshaling is handled automatically, based on the attributes that have been added to API declarations or type declarations. There are some cases, however, when your code must explicitly handle the marshaling – especially when dealing with a pointer on a structure. To do this marshaling, use the System. Marshal class. Another class that can be very useful when marshaling data to or from unmanaged memory is the BitConverter class. For example, the Marshal class does not include a method to read or write a double value, but it can read or write Int64 values, which are the same size, and the BitConverter class can convert these to or from doubles:
// copy double into unmanaged memory: Mem := Marshal.AllocHGlobal(SizeOf(Int64)); Marshal.WriteInt64(Mem, BitConverter.DoubleToInt64Bits(DoubleVariable)); ... // copy double from unmanaged memory DoubleVariable := BitConverter.Int64BitsToDouble(Marshal.ReadInt64(Mem));
When using the marshal class, remember that you must always free any unmanaged memory you allocate – the garbage collector does not collect unmanaged memory.
One of the changes in the way RAD Studio applications work with Windows is the way message handlers work. The basics of declaring and using messages handlers is the same, but the message-cracker types have changed from records to classes, and you can no longer simply typecast from one message-cracker type to another. Most of this has already been covered in the section on typecasts, but there are a few additional issues that bear mentioning:
The VCL defines and uses a number of private message types. These are, for the most part, defined in the Controls unit, and have identifiers of the form CM_XXX or CN_XXX. Because of the extra overhead in marshaling messages, several of the CM_XXX message types have been changed or eliminated, replaced by other mechanisms that are less expensive in the .NET environment. The following table lists the message types that have changed, and how the same task is accomplished in RAD Studio:
Message type |
Change |
CM_FOCUSCHANGED |
Replaced by a protected method (FocusChanged) on TWinControl. Replace message handlers by an override to the FocusChanged method. Instead of sending messages, call FocusChanged using the IWinControl interface. |
CM_MOUSEENTER |
Meaning of LPARAM has changed. It used to pass an object reference to the child control where the mouse entered – now it passes the index of that child in the FWinControls or FControls list. |
CM_MOUSELEAVE |
Meaning of LPARAM has changed. It used to pass an object reference to the child control where the mouse exited – now it passes the index of that child in the FWinControls or FControls list. |
CM_BUTTONPRESSED |
Replaced by a protected method (ButtonPressed) on TSpeedButton. This was only used by TSpeedButton. The CMButtonPressed message handler was replaced by ButtonPressed, which is called directly. |
CM_WINDOWHOOK |
Retired. TApplication.HookMainWindow and TApplication.UnhookMainWindow are both public methods that can be called directly. |
CM_CONTROLLISTCHANGE |
Replaced by a protected method (ControlListChange) on TWinControl. Replace message handlers by an override to the ControlListChange method. |
CM_GETDATALINK |
Replaced by a protected method (GetDataLink) on various data-aware controls. Call this using the new IDataControl interface. When creating your own data-aware control (that does not descend from an existing class in DBCtrls), you must implement IDataControl if the control is to work in a DBCGrid. |
CM_CONTROLCHANGE |
Replaced by a protected method (GetDataLink) on various data-aware controls. Call this using the new IDataControl interface. When creating your own data-aware control (that does not descend from an existing class in DBCtrls), you must implement IDataControl if the control is to work in a DBCGrid. |
CM_CHANGED |
Meaning of LPARAM has changed. It used to pass an object reference, now it passes a hash code for the object that changed. |
CM_DOCKCLIENT |
Replaced by a protected method (DockClient) on TWinControl. Replace message handlers by an override to the DockClient method. |
CM_UNDOCKCLIENT |
Replaced by a protected method (UndockClient) on TWinControl. Replace message handlers by an override to the UndockClient method. |
CM_FLOAT |
Replaced by a protected method (FloatControl) on TControl. Replace message handlers by an override to the FloatControl method. |
CM_ACTIONUPDATE |
Retired. TApplication.DispatchAction was promoted to public, and is called directly rather than using a message. |
CM_ACTIONEXECUTE |
Retired. TApplication.DispatchAction was promoted to public and is called directly rather than using a message. |
Sometimes, Windows API calls require the use of the Single Threaded Apartment (STA) model to function properly on some operating systems. For example, on some versions of Windows 98, the Open and Save dialogs do not work unless your RAD Studio application uses the Single Threaded Apartment model. Any portion of the VCL that uses COM requires this model.
The threading model is established when the process first starts up. If you are creating an executable, this is easy: just add the [STAThreadAttribute] attribute to the line immediately preceding the begin statement in the dpr file. When creating a DLL, you can’t force the threading model. However, you can call the CheckThreadingModel procedure in the SysUtils unit to raise an exception when the application calls a method that requires a particular threading model.
This restriction is fairly common in .NET. By default, Microsoft Visual Studio adds the STAThreadAttribute attribute to applications it creates.
The Variant type is very different in RAD Studio. Whereas the Win32 compiler maps Variant onto the record type that COM uses for Variants, in RAD Studio, a Variant is more general. Any object (which in RAD Studio is any type) can act be manipulated as a Variant. Thus, in RAD Studio, you could assign a control to a Variant.
The Delphi Variant type is a Delphi language notion that is not CLS compliant. If you are writing code in RAD Studio that uses Variants, to the outside world will see, these will map to only as System. Object. Thus, to code written in other languages, the flexibility in type conversions that Delphi Variants support provide is not available.
If your code uses Variants, chances are it should still work. However, because Variants are no longer based on the TVarRec type, any code that works with the internals of a Win32 Variant by getting into the underlying TVarRec record must be rewritten for .NET.
The COM Interop layer automatically marshals Objects (and hence Variants). Thus, you can use RAD Studio Variants with COM. However, when using RAD Studio Variants with COM, you should restrict the types you assign to the Variant to COM-compatible types.
In Delphi for Win32, the compiler enforces COM restrictions on the kinds of data that can be assigned to an OleVariant. In RAD Studio, OleVariant is simply a synonym for Variant. It does nothing to ensure that the Variant value is a COM-compatible type.
Custom Variants are completely different in RAD Studio. Because Variants are just objects, you do not need to do anything at all to create a custom Variant – any class you define is already a Variant type. However, to work well as a custom Variant, it helps to implement some CLR interfaces: IComparable, IConvertible, and ICloneable. The Delphi compiler can use these to implement Variant operations. Even with these interfaces, however, other, arbitrary Variant types, can’t be converted into your Variant (class) unless you implement a FromObject method:
class function FromObject(AObject: System.Object): TObject; static;
FromObject takes an arbitrary source object (the Variant to convert to your class type) and returns the corresponding instance of your class as a TObject.
RAD Studio can link Windows resources (res files) into your assemblies. This means that when first porting an application, you do not need to change the way you declare and use resources, and it will still work. In some cases, this is what you want to do anyway. For example, if you use custom cursors, it is simpler to use the Windows API LoadCursor function to add the cursor to TScreen.Cursors than to bring in the overhead of using Cursor and then obtaining a handle to the underlying cursor. However, for resources that are not Windows-specific (such as bitmaps, icons, and strings) you will probably want to update to a .NET resources file.
When you use the resourcestring keyword, RAD Studio automatically creates the string resources as .NET resources rather than Windows resources. This happens automatically and there is nothing special you need to do. The one thing to watch out for is that you no longer can use the PResStringRec type.
You can convert bitmaps into .NET resources using the ResourceWriter class. The resulting resources file can be linked into your RAD Studio application, or deployed as a satellite assembly. To use these converted bitmaps, LoadFromResourceName has new overloads for working with .NET resources (and the old version of LoadFromResourceName as well as the LoadFromResourceID method have been deprecated.) Thus, for example, if your bitmaps are in a resources file with a name such as MyResources.en-US.resources, you can load your bitmap as follows
MyBitmap.LoadFromResourceName('MyFirstBitmap', 'MyResources', System.Assembly.GetCallingAssembly);
Note that this example assumes the resources are compiled into the assembly that is making the method call that contains this line. If the resources are compiled into a different assembly, you can use System.Assembly.GetAssembly (using a type that is defined in the relevant assembly) or System.Assembly.GetExecutingAssembly (to obtain the currently executing assembly).
The signature for the OnCompare event in the TTreeView class has changed in the VCL for .NET. Existing code will cause a runtime exception when the event handler is called.
In Delphi 7, the signature was:
TTVCompareEvent = procedure(Sender: TObject; Node1, Node2: TTreeNode; Data: Integer; var Compare: Integer) of object;
In Delphi for .NET, the new signature is:
TTCompareEvent = procedure(Sender: TObject; Node1, Node2: TTreeNode; Data: TTag; var Compare: Integer) of object;
Copyright(C) 2008 CodeGear(TM). All Rights Reserved.
|
What do you think about this topic? Send feedback!
|