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.