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 they 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 as project leader of both Indy and IntraWeb. Both Indy and IntraWeb are available for all three platforms.
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.
Cross platform code should be implemented with as few IFDEF's as possible
If IFDEF's are to be minimized what can be used instead? Polymorphism. Polymorphism is one of the core concepts in object-oriented programming. By 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 about 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 these features again 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), namespaces, 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 Linux support is easier than .NET in many aspects, it is certainly easier to support two of three possible platforms than supporting all three.
Many of the new features introduced by .NET are language features. These language features were introduced into the Delphi language with Delphi 8. However at this time Delphi 8 can only compile for the .NET platform and Delphi 7 must be used to compile for Win32. Because of this, the new language features cannot be used in cross platform code at this time. When Delphi 9 is released most of the new language features will be ported to the Win32 compiler and cross platform code will be able to take advantage of them.
When Delphi 9 is released most of the new language features will be ported to the Win32 compiler and cross platform code will be able to take advantage of them
Safe versus Unsafe
If you have done any investigation into the .NET world, you probably have heard the terms "Safe Code" and "Unsafe Code". These terms are also known as "Managed Code" and "Unmanaged Code" respectively. In short, managed code is code that executes inside the control and rules of the .NET framework. While unsafe code is old code that has less rules, but through a "compatibility option" can be called from safe code. The ultimate goal of any .NET application is to use 100% safe code and isolate all unsafe code into separate assemblies necessary when accessing hardware, etc. In a 100% .NET world all applications will run only safe code and only drivers, encryption, and other such items shall run as unsafe code. Unsafe code is also operating system specific while safe code can run on any operating system which the .NET framework is implemented on.
Many servers also restrict applications that run on them to only safe code. Any unsafe assemblies are an exception and must be permitted by the system administrator.
Using Delphi 7
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 Delphi 7 as the primary IDE. 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, because when there are many of them in a given project, the logging of these warnings to the output window drastically slows down the compiler.
Delphi 7 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 Delphi 8 immediately you will have to eliminate all unsafe code at once before you can even retest the application as whole, instead of piece by piece.
Delphi Magic
When designing Delphi 8, one of Borland's primary goals was to maintain backward 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 Delphi 8 executes from code compiled by Delphi 7. With these differences in mind, it is quite easy to write code that works in both Delphi 7 and Delphi 8. This may not seem like a major feat, 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 Delphi 8 to preserve backward compatibility.
Delphi maintains backward compatibility through both implementations in the included libraries RTL and VCL, but also by building in some magic into the compiler itself.
Delphi maintains backwards compatibility through both implementations in the included libraries RTL and VCL
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 behavior, 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 behavior.
.NET Also performs object construction and especially destruction differently than in Win32. Delphi is not able to totally isolate this behavior, but it does preserve many of the important aspects through its handling of destructors.
.NET Common Error
If you have worked with .NET or used .NET applications, you may have encountered this error:
This is the error message that an end user will see if an error 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 but not initialized yet, or it referenced an object that has been destroyed. Consider the following code in Delphi 8:
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 7, but with slightly different results. When the same code is executed in Delphi 7, 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 no pointers, they cannot go wild.
It's a .NET AV
Let's examine the history of the Access Violation.
· In Windows 3.0 it was called a UAE, or Unrecoverable Application Error.
· In Windows 3.1 it was called a GPF, or General Protection Fault.
· In Win32 it was called a AV, or Access Violation.
· In .NET, it’s called "Object references not set to an instance of an object"
While it’s 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:
· Pointers
· Untyped variables
· Win32
· Globals
· Sets
· AfterConstruction / BeforeDestruction
· 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:
· String Indexes
· Object references
· IntPtr
· 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.
Pointers are the pinnacle of unsafe code and are no longer available
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 .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 on 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 it’s 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 backward compatibility globals can still be used.
How is that? How can globals be gone, yet still be 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, he 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:
· Strings
· TList / TStrings
· Initialization and Finalization
· Units
· Variants
· Typecasts
· Constructors and Destructors
Strings
Strings have changed significantly in their implementation and inner workings. Many of the changes have been isolated and backward 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:
In Win32 this code compiles to instructions that modify just one character while leaving the existing string intact. In .NET, the code still works, but 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 were 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 been initialized but still be empty. In C# it is necessary to check 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 typecast an 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 behavioral 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 in 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 as 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 even 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 backward compatibility. The methods that 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 Delphi 9.
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, it’s 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:
Hard cast:
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 lead to access violations or other errors later on, if it was not compatible. Because hard casts do not perform type checking, they 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
Constructor and destructor behavior have changed considerably. This topic is easily overlooked, but requires space for a paper of its own. For more information on constructors and destructors in .NET, consult my Constructors and Destructors in .NET article.
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 the end it is a good thing. After all, it’s not a 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.
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.
Platform Differences
Platform differences should be implemented 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.
Endpoint Polymorphs
A base abstract class is created which defines the interface. Then for each platform a descendant is created which contains the platform specific code. Then a single set of IFDEF's is used to create the specific platform class, and the user code uses the base interface. As an example let’s take Indy's network interface.
TIdStack
TIdStackWindows
TIdStackLinux
TIdStackDotNet
TIdStack is the base abstract which the developer users. For each platform there is a specific descendant, which implements the platform specific code. The developer never directly interacts with any of the descendants, but instead calls methods in TIdStack, which are then actually executed in a particular descendant by overrides.
In the IdStack unit the following global exists:
var
GStackClass: TIdStackClass = nil;
Each platform then has its own package (project) file, which only includes one descendant. So the Windows project includes IdStackWindows.pas, which contains TIdStackWindows. The others are not included in the Windows project, but only their specific ones. In the initialization section the variable is initialized.
initialization
GStackClass :=
{$IFDEF LINUX} TIdStackLinux; {$ENDIF}
{$IFDEF MSWINDOWS} TIdStackWindows; {$ENDIF}
{$IFDEF DOTNET} TIdStackDotNet; {$ENDIF}
end.
So when a stack class is needed, the following code is used:
var
LMyStack: TIdStack;
begin
LMyStack := GStackClass.Create;
..
This will create the proper descendant, although you will only see what is available in TIdStack. But code that is executed (implementation) is actually in TIdStackWindows (or other depending on platform) based on overrides of methods. Each descendant implements its own functionality. This is similar to how TStrings and its descendants such as TStringList operate.
Classes can also be self-registering. When a class is linked in by the project, it self registers itself at runtime. The advantage is that the user can choose by deciding to link in different classes to enable features automatically. The disadvantage is that the user must remember to link the units to the project. Indy's coder and authentication mechanism use this method.
This same pattern can be implemented using interfaces, but a base abstract is a better choice as it allows for sharing of implementations. Interfaces are a better choice when the ancestor varies.
Polypmorphic Ancestor
Sometimes it may be necessary to implement platform specific functionality in a class that is used as a base for other classes. This can be done by extending the endpoint polymorph pattern. TIdStream implements this. The basic hierarchy is shown below.
TIdStreamVCLBase
TIdStreamVCLWin32 / TIdStreamVCLDotNet
TIdStreamVCL
TIdStreamVCLBase implements the interface as well as some basic functionality. Platform specific code is then implemented by overriding methods in TIdStreamVCLWin32 and TIdStreamVCLDotNet. TIdStream exists as an empty implementation to provide a common class name for further inheritance. TIdStream is declared as follows:
unit IdStreamVCL;
{$I IdCompilerDefines.inc}
interface
uses
{$IFDEF DotNet}
IdStreamVCLDotNet;
{$ELSE}
IdStreamVCLWin32;
{$ENDIF}
type
TIdStreamVCL = class( {$IFDEF DotNet} TIdStreamVCLDotNet
{$ELSE} TIdStreamVCLWin32 {$ENDIF} );
implementation
end.
Notice that there is no implementation. This is purposeful. To add an implementation, descend a new class from TIdStreamVCL and add the implementation to the descendant. This further separates and isolates the IFDEF conditionals.
Indy
Indy is a good example of how to segregate platform code. While Indy 10 is not complete and some parts still need to be ported you can see the basic structure. The code still contains many DOTNETEXCLUDE directives, but these were 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:
· System
· Core
· Protocols
Each package is built upon the package above it. System is the package that contains all the platform specific code and is not specific 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.