Developing Single Source Delphi: Win32, .NET, and Linux

By: Chad Hower

Abstract: Learn how to write clean and maintainable single-source code across Win32, the Microsoft .NET Framework, and Linux without simply IFDEFing everything.

Developing single source Delphi - Win32, .NET, and Linux

 

When presented with cross platform development, most developers resort to spreading IFDEF's all throughout their code. Unfortunately this creates unmaintainable and brittle code.

 

In this article I will demonstrate proven ways of using polymorphism and other object oriented techniques to produce solid and maintainable, single source, source code. While IFDEF's are used, their use is kept to a minimum and are kept in isolated units. I will also cover commonly encountered differences between platforms and how to manage them.

 

Techniques used in this article have been developed during development of both Indy and IntraWeb. Both Indy and IntraWeb are available for all three platforms.

 

 

Diamondback

 

At the time of writing, Diamondback is still a future product. Statements about Diamondback in this article are speculative and based on public statements by Borland and Borland employees. Some statements may change upon Diamondback actual release.

 

How?

 

Cross platform code should be implemented with as few IFDEF's as possible and such uses should be contained and isolated. Rules should be established that determine which units IFDEF's are permitted in, and no IFDEF's should be allowed in other units. This practice is very different than the common practice. The common practice in cross platform development is simply to find each difference and insert an IFDEF on the spot of each difference. IFDEF's should be applied like medicine, not like napalm.

 

If IFDEF's are to be minimized what can be used instead? Polymorphism. Polymorphism is one of the core concepts in object oriented programming. But using polymorphism IFDEF's are minimized, and platform differences are well documented and isolated. Polymorphism encourages better source code implementations by not allowing quick IFDEF hacks to be used.

 

IFDEFfing may be easy in the short term, but in the long term it is much more expensive in terms of maintenance and occurrence of bugs.

 

Platform differences can also be minimized by sticking to the RTL, VCL, Indy, and the YCL. Both the RTL and VCL provided by Borland implement many of the platform differences internally. By using these class libraries the differences are the responsibility of the libraries, and not the user. Indy while originally designed for sockets has built its own system library which takes care of many platform differences as well. The Indy System library can be used without linking to the socket functions and thus can be used in any application, whether or not the application uses sockets. YCL is "Your Component Library". By isolating all platform differences in libraries you can keep code that uses these combined libraries such as applications and higher level libraries free of IFDEF's.

 

 

Cross Platform

 

What exactly does cross platform mean? Cross platform means that one set of source code is designed to be compiled on more than one platform. A platform usually means an operating system such as Windows or Unix, but in recent years platforms themselves can be independent of operating systems such as Java, or .NET. In the context of this article when I say cross platform I mean developing source code that runs on Win32, Linux, and .NET.

 

This article will focus on .NET as it is much more different from Win32 than Linux is. The techniques demonstrated here will work for any platform, but since the .NET platform introduces so many changes that are new to developers, this article contains mostly information on these differences.

 

Platform Specific Features

 

Developing cross platform code requires that all platform differences be identified and handled individually. Since some platforms may contain features that the others do not, it is often necessary to either implement again these features on the other platforms, or conform to the lowest common denominator. That is, use only features available on all the platforms.

 

.NET introduces many new features that are of great use to developers. Some of these features manifest themselves as differences while others are completely new functionality. Some of the largest new .NET features are the FCL (Framework Class Library), namespace, operator overloads, and safe versus unsafe code.

 

Not every developer needs to support Linux. You should determine if you need to support Linux or not as this can lessen the requirements and decrease the amount of work required. While concurrent Win32 and Linux support is easier than concurrent Win32 and .NET in many aspects, it is certainly easier to support two, rather than all three possible platforms.

 

Many of the new features introduced by .NET have affected Delphi as a langauge and are implemented as language features. Such extensions were introduced in Delphi 8. Delphi 8 however supports only .NET and relies on Delphi 7 to support Win32. Delphi 7 does not contain the new language extensions. If Delphi 8 and Delphi 7 are to be used, the new language features in Delphi 8 must be avoided.

 

Diamondback will contain compilers for both .NET and Win32 making cross platform development among these two platforms much easier. The Win32 compiler however will likely not contain all the language features in the .NET compiler, so some care must be excercised in which language extensions may be used. If Linux support is required, none of the new language features can be used in shared code. The last version of the Linux compiler is Kylix 3 and does not support any of the new language features introduced in Delphi 8.

 

 

Pure Code

 

Applictions can be compiled and executed on the .NET platform, yet still be not "pure" .NET applications. "Pure" is a subjective term, and not necessarily an official one. I will define a pure .NET application as having the following attributes:

 

7    100% Safe Cod

7    100% Managed Cod

7    P/Invoke Free

 

The ultimate goal of any .NET application is to use 100 %pure code and isolate all other code into separate assemblies. In a 100% .NET world all applications will run only pure code. Only device drivers, encryption, compression, and other such items shall run as unsafe code.

 

Managed Code

 

Managed code is code that is executed under the control of the .NET framework. Code that can be managed by the .NET framework is IL, or Intermediate Language based code. Unmanaged code contains CPU specific instructions produced by assembly language mnemonics, or a traditional compiler such as C++, or Delphi (.NET versions).

 

Languages such as C# and Visual Basic.NET produce only managed code. However Delphi, and especially C++ can produce mixed applications which contain both managed and unmanaged code.

 

Mixed mode applications can integrate with the .NET framework and allow easier phased migration from Win32. Mixed mode assemblies are also necessary for integrating with hardware. However, since a mixed mode application contains CPU and operating system specific information, this ties the .NET application to a specific CPU and operating system. Such applications therefore use .NET, but are do not run completely under the .NET framework and are not compatible with other .NET implementations such as Mono, the 64 bit .NET framework, and other future implementations.

 

Many server environments have security restrictions that only permit applications to be run if they are pure managed code. Unmanaged code items must be explicitly trusted by the administrator and are reserved for device drivers and the like.

 

In short, if any part of an application is unmanaged, the whole application should be considered unmanaged. If certain items such as hardware must be integrated with a managed wrapper is created. All such items should be created and deployed as separate isolated assemblies for easier management.

 

 

Safe Code

 

Safe code is code that strictly follows the rules of type safety of the .NET framework. Unsafe code is old code that does not follow such rules. Unsafe code permits use of pointers, machine specific byte orders, and language specific types. Unsafe code requires additional permissions to execute.

 

Assemblies can be checked with a tool called PEVerify to determine if it contains unsafe code. PEVerify is part of the .NET framework SDK.

 

 

P/Invoke Free

 

P/Invoke is an abbreviation for Platform Invoke and refers to the ability to make calls directly to the operating system without using the .NET FCL. In Windows, P/Invoke also allows calls to be made to user or vendor DLL's. P/Invoke is similar to JNI in Java.

 

Using P/Invoke does not make code unsafe or unmanaged. It does however cause the application to rely on items specific to the operating system. This again makes the application unable to run on Mono, and other future implementations. P/Invoke should be avoided and functions in the FCL should be used instead when possible.

 

 

Delphi

 

Enter topic text here.

 

Delphi 7

 

Until Diamondback is available, Delphi 7 should be used for compiling for Win32. This section is specific to Win32. While this section discusses Delphi 7, the Diamondback Win32 compiler will likely have similar options.

 

It might sound as if I am telling you to go backward in time, but it is much easier to develop cross platform Delphi code using the Win32 compiler as the primary environment. Delphi 7 will restrict you to the base functionality and prevent you from using new language features introduced in Delphi 8. Delphi 7 also can help you refrain from writing unsafe code. Borland built in unsafe code checking in Delphi 7 to help developers prepare for .NET, even though Delphi 7 cannot produce executables for the .NET platform.

 

These warnings are available in project options. In the screen shot below you can see they are unchecked. In cross platform code you should enable these three warnings and then proceed to eliminate each of them one by one until they only appear in platform specific code. In non cross platform code you should turn these warnings off as when many of them are in a given project the logging of these warnings to the output window drastically slows down the compiler.

 

 

The Win32 compiler is also better for porting existing code to .NET as it allows you to proceed step by step, eliminating one unsafe item at a time. You can then regularly test your changes before all unsafe code has been eliminated. However if you move your code to the Delphi for .NET compiler immediately, you will have to eliminate all unsafe code at once before you can even retest the application as whole. It is much easier to use the Win32 compiler and perform the chanegs step by step, instead of all at once.

 

Delphi Magic

 

When designing the Delphi for .NET compiler, one of Borland's primary goals was to maintain backwards compatibility of source code. This has been done to an amazing degree, however because of the extensive differences between Win32 and .NET there are still differences in how code compiled by the Delphi for .NET compiler executes from code compiled by the Delphi Win32 compiler. With these differences in mind, it is quite easy to write code that works in both Win32 and .NET. This may not seem like a major accomplishment, but those who have experienced the differences between C++ and C#, or Visual Basic and Visual Basic.NET will appreciate the amount of work that has gone into the Delphi for .NET compiler to preserve backwards compatibility.

 

Delphi maintains backwards compatibility through both implementations in the included libraries RTL and VCL, but also by building in some magic into the compiler itself.

 

One primary area of compiler magic is strings. In Delphi strings have always been a special case handled directly by the compiler. In .NET, strings are objects, but still with a special status. Delphi preserves nearly all former string behaviour, but maps the string type to the .NET string object. So in Delphi it is not only a traditional Delphi string, but it is also a .NET string at the same time. Delphi performs similar magic with TObject and TComponent mapping them to their direct counterparts in the FCL, while still preserving their VCL qualities. For objects Delphi uses a new language feature called class helpers to implement this dual behaviour.

 

.NET also performs object construction, and especially destruction differently than in Win32. Delphi is not able to totally isolate this behaviour, but it does preserve many of the important aspects through its handling of destructors.

 

 

P/Invoke Usage

 

The good news is that Delphi produces managed and safe code. However several places in both the RTL and VCL, P/Invoke is used to access the Win32 API.

 

An entry in Danny Thorpe's blog reads:

"When writing your Delphi code with the intent to run on Mono, be careful to avoid everything Win32 related, including VCL, WinForms, and the SysUtils unit."

 

This is not specific to Mono. It is just that currently Mono is the only other .NET implementation. This will change in the future as more implementations become available.

 

The VCL units (not to be confused with RTL) use the Windows unit. This is not a major problem however as .NET's WinForms rely directly on the Win32 API also. This means that VCL applications are dependent on Windows and will not run with only the .NET framework. But the same restriction exists for .NET's WinForms based applications.

 

The unit of concern is Windows. Windows of course maps procedures and functions to the Win32 API. By itself, this is not the issue. However many core units in Delphi have Windows in their uses clause. One of notable concern is the SysUtils unit. The use of the Windows unit, or any unit that uses the Windows unit makes applications dependent on Windows. Any such application will not execute on Mono, or other future server environments where .NET is implemented.

 

 

SysUtils Patch

 

An unofficial patch to the source of the SysUtils unit for Delphi 8 is available. This patch is available on SourceForge and was created by Andreas Hausladen.

 

This patch alters the source code in SysUtils and removes the dependence on the Windows unit for some procedures. For others it disables the functionality. Details are available in the included documentation.

 

 

Unsafe Code

 

As mentioned previously, unsafe code should be avoided. However unsafe code can be used as a migration path to .NET. By using unsafe code applications can be ported in phases, instead of all at once.

 

Delphi allows the use of unsafe code by using the following directive:

 

{$UNSAFECODE ON}

 

Individual procedures must then also be marked as unsafe. An example of this is shown here:

 

procedure Foo; unsafe;

begin







end;

 

.NET Common Error

 

If you have worked with .NET or used and .NET applications you may have encountered this error:

 

This is the error message that an end user will see if it is left unhandled. When running in the debugger it will appear as a specific exception type:

 

If you have not encountered this error yet, you will. Especially during development.

 

 

Object reference not set to something or other

 

This error signifies that a variable was referenced was not initialized yet, or referenced an object that has been destroyed. Consider the following code in Delphi for .NET:

 

program Project3;

{$APPTYPE CONSOLE}

 

uses

Classes,

SysUtils;

 

var

LStrings: TStringList;

begin

LStrings.Add('Test');

end.

 

When the program is executed it will reference LStrings which is not initialized yet. This will cause this error shown previously.

 

The same code can be run in Delphi for Win32, but with slightly different results. When the same code is executed in Delphi for Win32, it yields a different error as seen below.

 

Why does the same code produce an Access Violation in Win32, and a "Object is not set to an instance of an object" in .NET? The answer is simple - yet complicated at the same time.

 

Object is not ready yet (XP style)

 

The short and possibly logical answer is that "Object is not set to an instance of an object" is really an Access Violation. And in fact - it mostly is. The difference is a lot in just the name, but also in the handling and cause. While the root of the cause is the same, the conditions it can occur under are more restricted. An Access Violation is a very wide ranging error that can occur any time code tries to access something it should not. Access to an uninitialized object or already freed object is just one of these possible conditions.

 

"Object is not set to an instance of an object" on the other hand can only occur in this one specific condition and thus is much easier to find and remedy. Because .NET protects memory much closer, memory corruption is not possible in the traditional sense. Since there are not pointers, they cannot go wild.

 

 

It's a .NET AV

 

Let's examine the history of the Access Violation.

 

7    In Windows 3.0 it was called a UAE, or Unrecoverable Application Error.

7    In Windows 3.1 it was called a GPF, or General Protection Fault.

7    In Win32 it was called a AV, or Access Violation.

7    In .NET, its called "Object references not set to an instance of an object"

 

While its true that in each version the error became more restricted and specific, each error is just a narrowed down version of its predecessor. While a "Object is not set to an instance of an object" is not exactly the same as an Access Violation and a blanket statement as such is not accurate, understanding that it is the direct evolution of the Access Violation will help you to debug and prevent it.

 

.NET - Things remembered

 

In .NET several things that we as developers have come to rely on as mainstays are now no longer available. While some of them may come as a "shock to the system" for many experienced developers, the changes are all good and even necessary ones. After you have survived the initial trauma you will come to realize that these are beneficial changes in the long run.

 

Some of the most prominent items that are no longer available in .NET are:

7    Pointers

7    Untyped variables

7    Win32

7    Globals

7    Sets

7    AfterConstruction / BeforeDestruction

7    Class cracking

 

Pointers

 

Pointers are the pinnacle of unsafe code and are no longer available. In modern object oriented code pointers have been relegated to infrequent use, mostly for calling operating system API's or interacting with external DLL's. There is no magic replacement for the use of pointers, but instead is a multi faceted approach which includes the use of:

7    String Indexes

7    Object references

7    IntPtr

7    Dynamic arrays

 

Because the pointer is no longer permitted this means that the address of operator (the @ symbol) is no longer permitted. However there is one exception. The @ symbol can be used with procedure pointers. In this one specific case the @ operator is permitted because the Delphi compiler recognizes this specific case and translates it to a .NET delegate. That is, internally no pointer is used but the use of the @ symbol is preserved for backwards compatibility.

 

 

Buffers (Untyped Variables)

 

Since untyped variables relied on the use of pointers they are also no longer available in safe code. Use of untyped variables must be replaced with object references, or method overloads. An example of this is covered later with TStream.

 

 

Win32

 

The Win32 API is no longer available in safe code either. However, since Windows is the only current platform for which .NET is released it is still common place to call the Win32 API directly. Doing so makes such code unsafe and thus should be avoided or isolated into a separate unmanaged assembly.

 

Instead of calling the Win32 API, the FCL classes should be used instead. If a Win32 API call must be called a P/Invoke can be used. P/Invoke is a way for safe code to call unsafe code. Delphi does much of the P/Invoke work automatically in a manner similar to calling an extern in Delphi 7. Most Win32 API's have already been mapped in the Windows unit and can be called directly by using this unit.

 

 

Globals

 

Globals of all kinds are no longer available. This includes global variables, procedures, and functions.

 

How can globals be eliminated? For most developers globals have always been there and it is impossible to imagine how development can occur without globals. In fact if you go back far enough to have used line numbered basic you may remember similar confusion when someone told you that there was a new basic without line numbers. Such a thing just was not comprehensible until one actually saw it and how it worked.

 

In .NET everything is a class. Some developers may begin to have a "smalltalk attack", but its .NET is not smalltalk. Because everything is a class, no global procedures or variables can exist because they are not part of a class.

 

To replace globals, statics can be used. A static is a variable or method that exists on the class, rather than the instance of a class. If that is not clear, imagine the object TFoo. And instances of TFoo called Foo1, Foo2, and Foo3. If a static variable named Count exists, only one Count will exist for all instances of TFoo, instead of one per instance. So if Foo1.Count := 4, then when we access Foo2.Count, it will be 4 because its the same variable. In fact, we do not even need an instance, we can simply access it from the class itself as TFoo.Count. Delphi has always had static methods that could be called on the class, but the introduction of static variables is new in Delphi 8.

 

Many developers simply make a TGlobal class and add all their globals to it, and then access them simply by prefixing them with TGlobal.

 

 

 

Delphi Globals

 

As I just stated, globals are gone. However in Delphi 8 for backwards compatibility globals can still be used.

 

How is that? How can globals be gone, yet still here? Delphi 8 can still use globals because of a bit of compiler magic. More about this later in the Units topic.

 

Sets

 

Sets are another item that are both gone, and still here. Sets are not available in .NET, but through some compiler magic are available in Delphi 8. When using Delphi code, sets are available and function nearly the same as before.

 

However if a C# user or Visual Basic user tries to use your set, they will encounter some difficulty. Delphi exports sets as integer values and indexes. They can in fact use them, but the usage will be a bit awkward and require boolean operations. If you are using sets, you should only use them in Delphi code that is not exported. If you need to export set functionality you should rethink the interface and export it as a class instead.

 

 

Things Different

 

Other items have been preserved in .NET, but their functioning has changed. The major items you will encounter are:

7    Strings

7    TList / TStrings

7    Initialization and Finalization

7    Units

7    Variants

7    Typecasts

7    Constructors and Destructors

 

Strings

 

Strings have changed significantly in their implementation and inner workings. Many of the changes have been isolated and backwards compatibility has been preserved wherever possible. However there are still changes that are very important to be aware of.

 

The biggest change to strings is that all strings are now Unicode. Each character in a Unicode string consists of two bytes, instead of one as before. This does not affect normal string operations, but many developers regularly use strings as dynamic buffers for binary data. Strings can no longer contain binary data.

 

Instead of using strings for binary data other specifically suited constructs such as byte arrays, or binary streams should be used instead.

 

 

Immutable

 

Strings in .NET are immutable. Immutable is just a fancy word for saying that they are not changeable. How can strings not be changeable? Dynamic strings have been a tenet of programming for a long time.

 

Strings in .NET can be changed of course, but not internally. This means that you can write code to change a string, but internally a new string must be made and the alterations copied to the new string. This is implemented transparently to the developer, but can have very negative impacts on performance.

 

Consider the following code:

 

s[5] := 'z';

 

In Win32 this code compiles to instructions that modify just one character while leaving the existing string intact. In .NET, the code still work however its implementation is very different. Because strings are immutable, .NET must create a new string and copy the results into the new string, and then finally destroy the old string.

 

In short, remember that any change to a string causes a string copy to be performed. If only a few changes are performed, it can be done very quickly. But large amounts of changes such as search and replaces and parsing will run very slow under .NET if not redesigned.

 

 

Empty String

 

In Win32 strings that are declared as part of a class are initialized to an empty string with a value of ''. In .NET strings instead are objects and initialized to nil, which is a different value than ''. nil is the value of the object reference, while '' is the value of the data of the object.

 

Consider the following class:

 

type

TMyObject = class(TObject)

protected

FMyString: string;

public

procedure Test;

end;

 

implementation

 

procedure TMyObject.Test;

begin

if FMyString = nil then begin

raise Exception.Create('string is nil');

end;

end;

 

In .NET, this is the proper way to check for a string that has no value. However this code does not work in Win32 and if this was required it would break a lot of backward compatibility. So Borland added another item of compiler magic so that the following code is the same as above:

 

procedure TMyObject.Test;

begin

if FMyString = '' then begin

raise Exception.Create('String is nil');

end;

end;

 

This has an additional benefit though. In .NET nil and '' are actually separate states, but rarely differentiated and of very marginal use. In Delphi checking for '', will check for '' or nil. While the above code works fine, a direct C# port would not function the same. In C# one must check for both '' and nil as the string may have since been initialized, but be empty. In C# it is necessary to checks for both. The Delphi equivalent of what must be done in C# is shown below:

 

procedure TMyObject.Test;

begin

if (FMyString = '') or (FMyString = nil) then begin

raise Exception.Create('String is nil');

end;

end;

 

 

 

String Help

 

So far I have explained many of the problems that you will encounter with strings in .NET. But fortunately there are some new features related to strings as well.

 

StringBuilder
 

StringBuilder is a .NET class for manipulating strings. StringBuilder unlike String is mutable, meaning you can change it without causing constant reallocation. When you know that you will be working on a string you should use a StringBuilder and then use or copy the final result when your changes are complete.

 

StringBuilder solves the problem in .NET, but StringBuilder is not available in Win32. A StringBuilder class is available on the Borland Developer Network for Win32 which allows you to use StringBuilder in both Win32 and .NET.

 

 

AnsiString
 

AnsiStrings are still available in Delphi and produce single byte strings. However AnsiStrings should not be explicitly used as everything in .NET is Unicode, and each time an AnsiString is passed to a .NET method or procedure that uses Unicode, a conversion will have to be performed to pass it in, and one to return it. This has a severe impact on performance. AnsiStrings are only safe in Delphi only code, but should still be replaced with Unicode strings for string work, and other means for binary work.

 

 

 

TIdBuffer
 

StringBuilder is a good solution to managing dynamic Unicode strings. It however still manages strings as Unicode, and in many cases only ASCII is required. A good example of this is for TCP commands.

 

Indy implements a buffered class that handles binary data, as well as ASCII values. TIdBuffer because it implements its storage as bytes is suited to binary data, but also handles string input and output as ASCII and thus is suited to ASCII handling requirements. TIdBuffer is also a smart class that manages memory by buffering and predicting data usage, thus reducing reallocations.

 

TList / TStrings

 

TList and TStrings accept an additional "tag" value known as Object for each item in the list. In Win32 this type was of Pointer and commonly typecast in and out to other values. For example:

 

MyStringList.AddObject('Line one', Pointer(1));

 

Since pointers are not available in .NET, this no longer works. However pointers are compatible with TObject, so the code can be changed to:

 

MyStringList.AddObject('Line one', TObject(1));

 

This code compiles in both .NET and Win32. But how can we type cast a integer to a TObject in .NET? Isn't .NET strictly type safe? Yes, .NET is very strict about types. But remember that everything in .NET is an object. An integer can be typecast to an Object because it is an object.

 

 

 

 

Initialization and Finalization

 

Delphi developers have come to rely on initialization and finalization sections to initialize global variables, create global objects, and more. When using code directly in Delphi 8 initialization and finalization sections work very similar to how they do in Win32 versions of Delphi.

 

 

 

Initialization

 

Initialization sections are now called in an unpredictable order and interdependencies between them are not permitted.

 

When code is exported into an assembly and used by a non Delphi language the initialization and finalization sections work very differently. In .NET, classes are not initialized until they are actually used. This causes delayed initialization, and in some cases initialization sections are never called at all. Since Delphi exports units as classes, and the initialization sections as class initializers this causes a behaviour change. In such situations initialization sections are often called much later than before, and in many cases never at all. Initialization sections which self register classes are particularly problematic as they never get called to register the class, and thus the class is never used, and thus never has its initializer called.

 

This can be solved by using IdBaseComponent in Indy which iterates through the list of unit classes and forces manual calls to the initialization sections.

 

 

Finalization

 

Finalization sections are not guaranteed to be executed either. However because of implicit destruction, most cases are unaffected. However if you are performing logging or other such items in finalization sections your code may no longer execute, or may execute in different orders than before. Code should be moved to object finalizers.

 

 

Units

 

Each unit it Delphi is now a class because .NET does not support globals. So each global is actually exported to .NET as a member of a class that represents the unit.

 

Delphi preserves the notion of globals, but global procedures and variables when exported to C# or Visual Basic users must be qualified by using the generated class.

 

TStream

 

TStream traditionally has relied heavily on untyped arguments. While this provided an "easy cheat" to allow many data types to be read and written, it is incompatible with .NET. TStream in .NET now uses method overloads with a specific overload for each supported data type.

 

The constants for the Seek method have also changed, so unless you are repositioning in a loop the Position property should be used instead for both clarity and use in cross platform code.

 

 

TStream - Win32

 

TStream in Win32 uses the following two methods to read and write. These two methods allow many types of data to be passed so long as the user passes them correctly

 

procedure ReadBuffer(var Buffer; Count: Longint);

procedure WriteBuffer(const Buffer; Count: Longint);

 

TStream - .NET

 

.NET requires the use of overloads. Just one of the methods now requires many overloads to implement similar functionality.

 

function Read(var Buffer: array of Byte; Offset, Count: Longint): Longint; overload; virtual; abstract;

function Read(var Buffer: array of Byte; Count: Longint): Longint; overload;

function Read(var Buffer: Byte): Longint; overload;

function Read(var Buffer: Byte; Count: Longint): Longint; overload; platform;

function Read(var Buffer: Boolean): Longint; overload;

function Read(var Buffer: Boolean; Count: Longint): Longint; overload; platform;

function Read(var Buffer: Char): Longint; overload;

function Read(var Buffer: Char; Count: Longint): Longint; overload; platform;

function Read(var Buffer: AnsiChar): Longint; overload;

function Read(var Buffer: AnsiChar; Count: Longint): Longint; overload; platform;

function Read(var Buffer: ShortInt): Longint; overload;

function Read(var Buffer: ShortInt; Count: Longint): Longint; overload; platform;

function Read(var Buffer: SmallInt): Longint; overload;

function Read(var Buffer: SmallInt; Count: Longint): Longint; overload; platform;

function Read(var Buffer: Word): Longint; overload;

function Read(var Buffer: Word; Count: Longint): Longint; overload; platform;

function Read(var Buffer: Integer): Longint; overload;

function Read(var Buffer: Integer; Count: Longint): Longint; overload; platform;

function Read(var Buffer: Cardinal): Longint; overload;

function Read(var Buffer: Cardinal; Count: Longint): Longint; overload; platform;

function Read(var Buffer: Int64): Longint; overload;

function Read(var Buffer: Int64; Count: Longint): Longint; overload; platform;

function Read(var Buffer: UInt64): Longint; overload;

function Read(var Buffer: UInt64; Count: Longint): Longint; overload; platform;

function Read(var Buffer: Single): Longint; overload;

function Read(var Buffer: Single; Count: Longint): Longint; overload; platform;

function Read(var Buffer: Double): Longint; overload;

function Read(var Buffer: Double; Count: Longint): Longint; overload; platform;

function Read(var Buffer: Extended): Longint; overload;

function Read(var Buffer: Extended; Count: Longint): Longint; overload; platform;

 

TStream - Example

 

The use of overloads provides direct replacements in most cases. However there is one case of special note. Consider the following code which writes a string to a stream. Arguably one of the most common types of data used with a stream.

 

s := 'Fill out your session evaluations  mark all good marks...';

LStream.WriteBuffer(s[1], Length(s));

 

TStream Problems

 

The above code appears proper. And in Win32 it is the standard and correct method to write a complete string out to a stream. However since it uses s[1] to pass the pointer to the string in Win32, in Delphi 8 this matches against the overload for the version of the method for a character. Because of this in Win32 this will write out the entire string as desired and expected, but in .NET it will only write out the first character of the string.

 

This difference in functionality is quite severe and difficult to combat. It is also very hard to detect because existing code will not only compile, but run. It just will not perform correctly.

 

In .NET it is unnecessary to pass the count as all data type sizes can be determined. New overloads that do not require the count have been created, but existing ones with a count parameter have been included for backwards compatibility. The methods with accept the count simply ignore the count argument. This is a shame as this count argument could be used to check the count against the actual data size and raise an exception if it differed. If this had been done, cases like the s[1] problem and others could be more easily detected. Of course looking back and making observations is much easier than looking forward. Hopefully this addition will be made in Diamondback.

 

TIdStream is a class in Indy that can assist with these differences. It is not interface compatible with TStream, but it does accept a TStream to its constructor. It then marshals or proxies its consistent interface onto the differing implementations of TStream.

 

Variants

 

Variants have changed *again* in .NET. I will not spend a lot of time on this subject other than to reiterate what I have always said. Variants are evil and have very little place in strongly typed languages.

 

Many users use variants in COM to help convert between data types of differing languages. With .NET's CTS, this is no longer needed. Other developers use them for data fields, but this too is unneeded and always has been. For data access its faster and more efficient to access the Value properties or the specific AsType properties.

 

99% of the time I see variants in use the need is unnecessary. Avoid variants.

 

 

Typecasts

 

In Win32 Delphi supported two types of type casting. Soft casts, and hard casts.

 

Soft cast:

LObject as TMyObject;

 

Hard cast:

TMyObject(LObject);

 

Both of these casts convert LObject into TMyObject. The soft cast checks first to see if LObject is compatible with TMyObject and if not will raise an exception. The hard cast will force LObject into a TMyObject even if it is not compatible. This can later lead to access violations or other errors if it was not compatible. Because hard casts do not perform type checking the are faster and thus many developers used them.

 

.NET does not allow such "unsafe" code to be executed. Delphi 8 still supports both syntaxes, but now both syntaxes result in safe casts.

 

 

Constructors and Destructors

 

Constructors and destructor behaviour has changed considerably. This topic is easily overlooked, but requires space for a paper of its own. Developers should be cautious in this area and research the changes before proceeding.

 

New Friends

 

.NET also includes many new features. Too many features to cover in this short topic, but I will list a few of the most important ones.

 

 

StringBuilder

 

StringBuilder as introduced before is a very handy class for string manipulation. In addition to it being required for efficient string operations in .NET, it contains many methods for easily manipulating strings in many different ways.

 

FCL

 

The FCL is essentially the VCL of the .NET framework. The FCL contains many classes to replace the Win32 API, but also for general programming use as well.

 

Byte Arrays

 

Byte arrays are not new. In fact Delphi has had them since the beginning. Dynamic byte arrays were introduced later but still have been around for several versions. Dynamic byte arrays will be the default replacement for strings formerly used for binary data. Byte arrays are very common in .NET and used by many FCL methods.

 

 

Unicode

 

Unicode is something that everyone agrees we need, but no one wants to work with. Working with Unicode certainly can require some adjustments, but in end its is a good thing. After all, its not an purely ASCII world out there.

 

The biggest impact of Unicode is that each character in a string now requires two bytes. This eliminates the use of strings as binary buffers which is still common practice.

 

 

True Language Interoperability

 

This by far is my absolute favorite feature about .NET. No longer are developers separated into categories by their language and restricted to interaction through COM or DLL's. Anything written in any language can now be used by any other language, and in a way as if it were written in the language that is using it.

 

.NET's language interoperability is the equivalent of Star Trek's universal language translator and will do more to advance development (especially for Delphi developers) than any other recent trend in programming.

 

 

Handling Platform Differences

 

The key to developing single source applications is to determine commonalities between platform code and isolate the differences.

 

Most developers use the path of least resistance, they simply wrap each difference with an IFDEF conditional. While this may be the normal and even generally accepted method of resolution, it is not a good solution. A better solution is to implement platform differences as sets of polymorphic classes. The hierarchy of these classes varies depending on the task. Using these patterns and variations of them you can keep your IFDEF's quite isolated and to a bare minimum.

 

Example

 

An example will be presented that provides two functions, one that returns the date in a string format, and one that returns the time in string format. The functionality will be split out and implemented using platform specific calls for each platform. The platforms demonstrated will be Win32, and .NET. For Linux, the same technique applies.

 

Both demos are simple console applications which print the time and date.

 

 

Special Note

 

Determining what set of functionality to use in a demo and still remain simple was no easy task. Most of the platform differences are either complex and would overshadow the example, or are already implemented by the RTL. Because of this I have chosen a simple example that shows formatting of a date and time as separate functions.

 

It is worth noting that the RTL already takes care of this difference and the Win32 version could in fact work with Delphi for .NET as well. However it would require the SysUtils unit which depends on the Windows unit which uses P/Invoke. Thus the example is both realistic, yet simple in demonstrating the technique.

 

 

Isolated Units

 

A common way to isolated differences is to put all IFDEF's into global functions and procedures, and then move them to separate units. An example of this is the SysUtils unit in Delphi for .NET. The SysUtils unit however was not designed this way, but instead modified later to keep backwards compatibility.

 

Below is the source for the application.

 

program Test;

 

{$APPTYPE CONSOLE}

 

uses

SysUtils,

Global in 'Global.pas';

 

procedure Main;

begin

WriteLn('Date is ' + DateString);

WriteLn('Time is ' + TimeString);

WriteLn('Press any key');

ReadLn;

end;

 

begin

Main;

end.

 

While the application is simple, instead of simply adding IFDEF's inline, they have been identified and isolated into the Global unit. The global unit appears next.

 

unit Global;

 

interface

 

// Procs

function DateString: string;

function TimeString: string;

 

implementation

 

uses

SysUtils;

 

function DateString: string;

begin

{$IFDEF Win32}

Result := DateToStr(Now);

{$ENDIF}

{$IFDEF CLR}

Result := DateTime.Today.ToString('d');

{$ENDIF}

end;

 

function TimeString: string;

begin

{$IFDEF Win32}

Result := TimeToStr(Now);

{$ENDIF}

{$IFDEF CLR}

Result := DateTime.Now.ToString('t');

{$ENDIF}

end;

 

end.

 

While this approach may seem simple and even tempting, it is not the best solution as shall be demonstrated in the next example. This approach requires many IFDEF statements which clutter up the code are regular intervals and make the code difficult to follow.

 

IFDEF Structure

 

Take special note of the IFDEF structure. The form is:

 

{$IFDEF Win32}

Win32 Code

{$ENDIF}

{$IFDEF CLR}

.NET Code

{$ENDIF}

 

While this may not appear special, consider the more common form:

 

{$IFDEF Win32}

Win32 Code

{$ELSE}

.NET Code

{$ENDIF}

 

The first form is preferred and superior to the second more common form. The first form is more easily expanded and does not imply default behaviour for new platforms. The first one is easily expanded for a third condition, while the second one is not. When expanded for three conditions the first form simply becomes:

 

{$IFDEF Win32}

Win32 Code

{$ENDIF}

{$IFDEF CLR}

.NET Code

{$ENDIF}

{$IFDEF Linux}

Linux Code

{$ENDIF}

 

Because it did not define an implicit "else" behaviour, it can be easily expanded for three, four, or as many conditions as needed.

 

 

Polymorphs

 

Polymorphs are a better way to implement platform specifics as they allow easier reuse of common code, and separate the platform differences without peppering the code with IFDEF's. Polymorphs contain a base abstract class which defines the interface. For each platform, a descendant is created which contains the platform specific code. A single set of IFDEF's is used to control which descendant is used.

 

An example hierarchy for Win32 and .NET is shown next:

 

TGlobalBase

TGlobalWin32 / TGlobalDotNet

TGlobal

 

TGlobalBase is an abstract base class which contains methods that can be overridden. The platform specific units contain overrides of methods in TGlobalBase to implement platform specific code. The overridden methods can call inherited, or other base methods to share common code. The final end point is TGlobal which is the class used to instantiate the proper class according to platform. TGlobal is declared as follows:

 

unit Global;

 

interface

 

uses

{$IFDEF Win32}

GlobalWin32;

{$ENDIF}

{$IFDEF CLR}

GlobalDotNet;

{$ENDIF}

 

type

TGlobal = class(

{$IFDEF Win32}

TGlobalWin32

{$ENDIF}

{$IFDEF CLR}

TGlobalDotNet

{$ENDIF}

);

 

implementation

 

end.

 

TGlobal should not contain any other source code. It is used to separate the IFDEF's into a single unit and allow the user code to use the proper class without requiring IFDEF's to be used in the user code. To use the class the user code can simply treat it like any other class:

 

GlobalObject = TGlobal.Create;

 

This will automatically instantiate the proper hierarchy according to platform. Let's now examine the root base class TGlobalBase.

 

unit GlobalBase;

 

interface

 

type

TGlobalBase = class(TObject)

public

function Date: string; virtual; abstract;

function Time: string; virtual; abstract;

end;

 

implementation

 

end.

 

In this example both methods are abstracts. However the base class commonly has non abstract methods to allow inherited to be called. Other non virtual methods can be implemented as well to share common code and for use by the platform specific descendants.

 

Our demo would now look like this:

 

program Win32;

 

{$APPTYPE CONSOLE}

 

uses

SysUtils,

GlobalBase in 'GlobalBase.pas',

GlobalWin32 in 'GlobalWin32.pas',

Global in 'Global.pas';

 

procedure Main;

begin

with TGlobal.Create do try

WriteLn('Date is ' + Date);

WriteLn('Time is ' + Time);

finally Free; end;

WriteLn('Press any key');

ReadLn;

end;

 

begin

Main;

end.

 

The platform specific descendants are implemented as follows.

 

Win32

 

unit GlobalWin32;

 

interface

 

uses

GlobalBase;

 

type

TGlobalWin32 = class(TGlobalBase)

public

function Date: string; override;

function Time: string; override;

end;

 

implementation

 

uses

SysUtils;

 

{ TGlobalWin32 }

 

function TGlobalWin32.Date: string;

begin

Result := DateToStr(Now);

end;

 

function TGlobalWin32.Time: string;

begin

Result := TimeToStr(Now);

end;

 

end.

 

.NET

 

unit GlobalDotNet;

 

interface

 

uses

GlobalBase;

 

type

TGlobalDotNet = class(TGlobalBase)

public

function Date: string; override;

function Time: string; override;

end;

 

implementation

 

{ TGlobalDotNet }

 

function TGlobalDotNet.Date: string;

begin

Result := DateTime.Today.ToString('d');

end;

 

function TGlobalDotNet.Time: string;

begin

Result := DateTime.Now.ToString('t');

end;

 

end.

 

As you can see, this approach is much cleaner than simply isolating the unit.

 

 

Indy

 

Indy is a working example of how to segregate platform code. While Indy 10 is not complete and some items still need to be ported to all platforms, you can see the basic structure. The code still contains many DOTNETEXCLUDE directives, but these are temporary markers marking code that has not been ported. In the end, all such temporary IFDEF's will be removed.

 

Indy contains 3 primary packages:

7    System

7    Core

7    Protocols

 

Each package is built upon the package above it. System is the package that contains all the platform specific code and is not specfic necessarily to sockets. Socket specific code is introduced in the Core level and is not platform specific.

 

Each package also has one unit which is permitted to have IFDEF's. This unit is the Global unit. This allows separation by package function, yet still keeps IFDEF's in specific isolated areas.

 

 

About the Author

 

Chad Z. Hower, a.k.a. Kudzu

"Programming is an art form that fights back"

 

Chad works for Atozed Software, and is the original author of both Internet Direct (Indy) and IntraWeb. Both Indy and IntraWeb have been licensed by Borland for inclusion in Delphi, Kylix and C++ Builder. Chad speaks at 6- 8 conferences each year in Europe and North America, writes frequently for developer magazines, and also posts free articles, programs, utilities and other oddities at Kudzu World.

 

Chad's background includes work in the employment, security, chemical, energy, trading, telecommunications, wireless, and insurance industries. Chad's area of specialty is TCP/IP networking and programming, inter-process communication, distributed computing, Internet protocols, and object-oriented programming. When not programming, he likes to bike, kayak, ski, drive, and do just about anything outdoors.

 

Chad is an ex-patriate who spends his summers in St. Petersburg, Russia, winters in Limassol, Cyprus, and travels extensively year round.

 


 

 


Published on: 10/11/2004 2:18:21 PM

Server Response from: ETNASC04

Copyright© 1994 - 2013 Embarcadero Technologies, Inc. All rights reserved.