The Microsoft .NET Architecture from the Delphi Perspective

Overview

Every few years a new technology comes along that turns our industry upside down. Some of these technologies thrive, some mutate into something else, and many are revealed to be half-baked marketing fluff. Almost in military fashion, the arrival of a new technology is invariably preceded by an artillery barrage of hype. Experienced programmers know to keep their heads down until the hype barrage subsides. After all, the proof will be in the technical pudding, so to speak.

Your reaction to Microsoft's .NET initiative will depend somewhat on your background. If you have previous experience with Java or Delphi, you might wonder what all the fuss is about. But, if you have been slogging it out in the trenches writing applications for Windows in C++ (or, heaven forbid, in C), you might be overwhelmed with joy.

The purpose of this chapter is to explain some of the technologies that make up the .NET initiative and to show how they fit into the world of Windows programming in general and Delphi programming in particular. We will begin by installing and configuring the Delphi for .NET Preview compiler. Then we will broadly cover some of the .NET technologies. Finally, we will discuss these technologies in greater detail, weaving in some Delphi code for illustration.

  Note 

This chapter and the following one were by Marco with extensive help from John Bushakra, who works for Borland's RAD tools documentation department.

Installing the Delphi for NET Preview

The Delphi for .NET Preview requires but does not install .NET Framework Runtime, which is freely downloadable from Microsoft's MSDN website. As a developer, you'll probably want to take an extra step and install the more complete .NET Framework SDK, which includes documentation and tools for developers (but is also much larger). You should install the .NET Framework Runtime or SDK before you install the Delphi for .NET Preview.

The Preview compiler is compatible with the .NET Framework SDK and service pack 1. If you have applied service pack 2 or later, then you will have to perform the extra step of rebuilding the precompiled units (the dcuil files) installed with the Preview. This requirement might go away in subsequent updates to the Delphi for .NET Preview compiler.

  Note 

A few months after Delphi 7 shipped (in November 2002), Borland released a significant update of the Delphi for .NET Preview. As a registered Delphi user, you can download updates to the Preview from the Borland website. Do this even before installing the version that comes in the Delphi box, as you'll need to uninstall it to update to a newer version.

  Warning 

I've used the November 2002 version of the Delphi for .NET Preview to test the examples of this and the .NET chapter, although most of them will also compile against the original version that shipped with Delphi 7. By the time you read this, a newer version might be available; check the author's website for updates of the examples.

After installing the .NET Framework SDK, insert the Delphi for .NET Preview CD and run the setup program. The Preview will install itself in a separate directory from your Delphi 7 installation, and none of your Delphi 7 settings are affected by installing the Preview.

Previously I mentioned that you would need to rebuild the precompiled units shipped with the compiler, if you have installed service pack 2 for the .NET Framework. If this applies to you, open a command window and navigate to the source tl directory under the root of the compiler installation directory. There, you will find a file called rebuild.bat, which you must execute to perform the rebuild. You might see some errors and warnings; these are known issues at the time of this writing, and might be cleared up in subsequent updates to the compiler. Once the batch file has completed, you will be ready to go.

The dccil compiler is located in the bin directory. Along with the compiler, in this directory is a file called dccil.cfg that specifies the default unit search path (the -U compiler switch), which is the units directory under the root installation directory. As packaged, the Delphi for .NET Preview compiler is strictly for use with a command window.

However, Borland has made available an unsupported plug-in to the Delphi 7 IDE on the Borland Developers Network website, referred to as Delphi for .NET common line compiler IDE integration. Using the plug-in, you can control the dccil compiler from within the IDE, as you can see in the menu installed by this plug-in.

Note, however, that the plug-in does not give you the full capability of designing forms and all the other things Delphi is famous for. The Delphi for .NET Preview compiler is intended to give you some advance warning about new language features and a glimpse into how the Delphi run-time library might look in the .NET context. You can download this plug-in from the Code Central area of the Borland Developer Network website, bdn.borland.com (if you cannot find it, look under the number 18889).

Another valuable resource you might want to work with is the Reflector. This tool was written by Lutz Roeder (who happens to work at Microsoft, by the way) and is available on his own website, www.aisto.com/roeder/dotnet.

  Note 

Reflector is like Microsoft's Intermediate Language Disassembler (ILDASM) tool—it allows you to inspect .NET assemblies (executables and DLLs) and their types and members.

Testing the Installation

Now it's time to test your Delphi for .NET Preview installation by compiling a simple console program that prints a message.

You can start this project using Delphi 7, or you can type in the text with your favorite editor:

program HelloWorld;
 
{$APPTYPE CONSOLE}
 
uses
 Borland.Delphi.SysUtils;
 
begin
 WriteLn ('Hello, Delphi! - Today is ' +
 DateToStr (Now));
end.

Notice how similar the code looks to the Delphi applications you're used to. The only difference is the uses clause. Borland has organized the units of the Delphi library into namespaces similar to those of the Common Language Runtime (CLR). I will discuss this in more detail in the next chapter.

Save the file as HelloWorld.dpr and open a command window. Navigate to the location of the file and type

dccil HelloWorld.dpr

The result should be an executable image, HelloWorld.exe, which you can run from the command line to produce a single line of output. If you are using the IDE plug-in, compiling and running the program will involve pressing Ctrl+F9 or F9 after you've enabled IDE hijacking (see the Hijack IDE menu item in the screenshot above).

If this program seems boring, you can begin your exploration of .NET using the windowed version of the hello world program instead of the console version. Called HelloWin, this program creates and shows a window on the screen, writing into its caption. It also shows the call to Application.Run, used to activate the application's message processing loop:

program HelloWin;
 
uses
 System.Windows.Forms,
 Borland.Delphi.SysUtils;
 
var
 aForm: Form;
 
begin
 aForm := Form.Create;
 aForm.Text := 'Hello Delphi';
 Application.Run (aForm);
end.

In .NET presentations, the previous code raises the enthusiasm of programmers used to Microsoft development tools. As a Delphi programmer, you might have used almost this same code since 1995, so you might wonder where this excitement comes from. The output of these programs is far from interesting, but you can at least prove they are not classic Win32 applications by running ILDASM on them (directly from the plug-in's menu). You can see the output for the HelloWorld executable in Figure 24.1. Notice that the unit's global code is wrapped in a pseudo class called uUnit. Unlike Delphi, the .NET runtime doesn't account for global procedures and functions, but only class methods. But I'm getting ahead of myself; let's start from the beginning and look at the NET platform.

click to expand
Figure 24.1: The HelloWorld demo as seen in ILDASM

Microsoft s NET Platform

Microsoft's .NET platform comprises many different specifications and initiatives. The core functionality of the .NET platform has been given over to the European Computer Manufacturer's Association (ECMA) and is undergoing the standardization process. At the time of this writing, the C# language specification has just passed ECMA's process and is heading for ISO standardization.

  Note 

You can find the standard documents for the C# programming language and the Common Language Infrastructure (CLI) at www.ecma.ch. An interesting element of the CLI specification is the standard naming convention for variables and methods.

From the programmer's standpoint, the core feature of the .NET platform is its managed environment, which allows the execution of intermediate code compiled by many different programming languages (provided they conform to a common definition of the base data types). This environment embeds many features ranging from sophisticated memory management up to integrated security, to name just two. On top of this managed environment, Microsoft has built a large class library, covering diverse areas of development (Windows-based forms, web development, web services development, XML processing, database access, and many more).

This is only a short overview. To get more into the details we need to learn the precise terms used in the .NET Platform, most of which are indicated by of three-letter acronyms, introduced in the following subsections.

The Common Language Infrastructure (CLI)

The CLI is the main pillar of the .NET platform. It was submitted to the ECMA in December 2001. The CLI comprises many different specifications:

Common Type System (CTS)  The CTS lays the groundwork for integrating disparate programming languages. It specifies how types are declared and used, and language compilers must conform to this specification in order to take advantage of the .NET platform's cross-language integration.

Extensible Metadata  The CLI specifies that every unit of deployment (an assembly) must be a self-describing entity. Therefore, an assembly must carry with it data that fully identifies the assembly (its name, version, and optional culture and public key), data that describes the types defined within the assembly, data listing other files referenced, and any special security permissions that are required. Furthermore, the metadata system is extensible, so an assembly might also contain user-defined descriptive elements, known as custom attributes.

  Note 

The term culture is used by Microsoft as an extension of the term's language (the language used for the user messages of a library) and locale (the dates and number formatting settings for a country). The idea is to cover anything peculiar to a country, or portions of a country.

Common Intermediate Language (CIL)  CIL is the programming language of a virtual execution environment with an abstract microprocessor. Compilers targeting the .NET platform do not generate native CPU instructions, but instead generate instructions in the intermediate language of the abstract processor. In this respect, the CLI is similar to Java byte code.

P/Invoke  Programs that execute in the .NET runtime each play in their own private sandbox. Unlike the traditional Win32 programming model, a large and complicated layer of software exists between them and the operating system. But the .NET runtime does not completely replace the Win32 API, so there must be a way to bridge between the two worlds. This bridge is called Platform Invocation Service (in short, P/Invoke or PInvoke).

Framework Class Library (FCL)  The .NET Framework Class Library (FCL), or .NET Framework for short (or even .NET Fx), is a class hierarchy with a design similar to that of Borland's VCL. Features also available in the VCL are actually quite similar, although the .NET Framework has a much larger set of classes (covering many more areas of programming). The architectural difference between the .NET Framework and the VCL is that the .NET Framework is not only callable from other languages, it can be directly extended by other languages. This means that, as a Delphi programmer, you have the ability to directly inherit from classes in the .NET Framework, just as you would inherit from any other Delphi class. Moreover, the CLI gives you the ability to extend a class written in any language that has a compiler that targets the .NET runtime. The factorable part of the FCL means that parts of the class hierarchy can be factored out; for example, to create a stripped-down version for use on a handheld device.

Extended Portable Executable (PE) File Format  Microsoft is using its standard Portable Executable (PE) file format (the file format used by standard Win32 executable files) to support the metadata requirements of the CLI. The advantage of using this format is that the operating system can load a .NET application the same way it loads a native Win32 application. Common loading is about where the similarity ends though, since all managed code is in a special section. The framework modifies the loader so when it finds it is dealing with a .NET entity, it passes control over to the CLR that then works out how to call the managed entry points.

The Common Language Runtime (CLR)

The CLI is a specification, and the CLR is Microsoft's implementation of that specification. Not surprisingly, the CLR is a superset of the specification. To a programmer, the CLR is an all-encompassing run-time library that unifies the vast array of services provided by the Windows operating system and presents them to you in an object-oriented framework.

On a larger scale, the CLR is responsible for every aspect of the .NET universe: loading the executable image, verifying its identity and type safety, compiling CIL code into native CPU instructions, and managing the application during its lifetime. CIL code that is intended to be run in the CLR is called managed code, whereas all other code (such as Intel executable code produced by Delphi 7) is unmanaged.

Common Language Specification (CLS)

Closely related to the Common Type System, the CLS is a subset of that specification that defines the rules that govern how types created in different programming languages can interoperate. Not all languages are created equal, and some languages have features that can't be found elsewhere. The CLS tries to find a happy medium, specifying those items that a large set of languages can implement. Microsoft tried to make the CLS as small as possible, yet still accommodate a large set of languages.

Some features of Delphi are not CLS compliant. This does not mean the code cannot be executed by the CLR and work on the .NET platform; it only means you are using a language feature that can't be supported by other languages, so that particular part of your code cannot be used by other .NET applications if they are written in languages other than Delphi.

  Note 

If you have experience with Java, you might find some of these items familiar. The .NET platform shares many concepts with the Java platform, in particular the intermediate language and virtual execution system, although with the core difference of Java being pcode interpreted by default, whereas .NET is invariably JIT compiled. There are also relevant analogies in the class libraries. In most cases, however, don't take corresponding Java concepts for granted, because differences in the implementation details might significantly affect how you use an apparently similar feature.

The various specifications of the CLI give a glimmer of hope about cross-platform development. But, you probably won't hear the "write once, run anywhere," mantra from Microsoft. This is due to the fact that they consider the user interface to be a key part of an application, and a normal PC screen compared to, say, a mobile phone screen has inherently different capabilities. There are two major ongoing efforts to implement the CLI on other operating systems. The Rotor project (officially called Microsoft Shared Source CLI or MS SSCLI) is written by the Microsoft Research team. Rotor consists of a large donation of software, under Microsoft's Shared Source license, and the necessary tools to build it on the FreeBSD, Win2, and Mac OS X 10.2 operating systems.

The second well-known CLI implementation on another operating system is the Mono Project, which supports Win32 and Linux. Mono is building what is essentially a clean-room implementation of the CLI on Linux and is released with a much more open license. Borland has a wait-and-see approach about Mono; there has been no word about whether Delphi will be a player in that arena.

The Rotor project seems to be aimed toward educators and people who are just curious about how the CLR is implemented, because its license is very open to academia but prohibits any commercial use. The Mono project, however, may give Microsoft some serious competition. The most serious stumbling block to true portability will probably continue to be the graphical user interface. The Rotor project does not include any GUI elements, while Mono has started developing WinForms using the WINE library. There is also a GTK# project associated with Mono, which is a binding of the GTK+ toolkit to the C# language.

Today's computing environment is a nebulous mass of diverse possibilities. Handheld devices have complex operating systems of their own, and Microsoft has a keen interest in this arena. Microsoft is working on a version of the .NET platform called the .NET Compact Framework, which is intended for handheld devices. The .NET platform could also serve as a springboard for tomorrow's technologies. Sixty-four bit processors are just around the corner, and .NET will no doubt be running there.

Does this mean you can run your managed application on Linux, 64-bit Windows, and your PDA? No. It is not reasonable to expect a user interface geared for a 1600×1200 pixel display on a 21-inch monitor to port to a handheld device. So, regarding .NET as a cross-platform tool, you will gain some advantage, but you should not expect your application to be a straight port especially if you are porting to another operating system or a handheld device.

In the following sections, we will examine some components of the CLI in more detail.

Assemblies

Microsoft coined the term assembly to denote a "unit of deployment" in the .NET runtime environment. In manufacturing, an assembly is a group of separate but related parts that are put together to form a single functional unit. Sometimes an assembly consists of only one part, but it might consist of many. So it is with assemblies in .NET.

Typically, an assembly consists of only one file—either a single executable or a dynamic link library. An assembly could consist of a group of related DLLs, or a DLL and associated resources like images or strings. Going back to the manufacturing analogy, each assembly, whether it consists of one part or multiple parts, contains a packing slip called a manifest. A manifest describes the contents of something, and an assembly manifest contains the metadata for an assembly. The manifest is the implementation of the extensible metadata system described in the CLI.

As you look through the installation directory for the Delphi for .NET Preview compiler, you will find a units folder. In this folder are a number of files with the extensions .dcua and .dcuil. These are not PE files, so they cannot be examined with ILDASM.

A .dcua file catalogs the namespaces and types in a .NET assembly (which, keep in mind, can consist of multiple executable files). A .dcuil file corresponds to a namespace. The .dcuil file contains all the compiler symbols for a namespace, as well as references to the .dcua files that contribute to that namespace (an assembly can contribute types to more than one namespace).

  Note 

A namespace is a hierarchical containment mechanism that organizes classes and other types. We will discuss namespaces in more detail in Chapter 25, "Delphi for .NET Preview: The Language and the RTL."

An application is built against a certain set of assemblies; the application is said to reference these assemblies. When the set of assemblies changes, the compiler must rebuild any .dcuil files that contain references to .dcua files that are no longer in the set. Likewise, if a change is made to an assembly, the compiler must rebuild the corresponding .dcua and .dcuil files for that assembly. A .dcuil file is roughly equivalent to a Delphi .dcu file, but a .dcua file does not have a corresponding file type in the Delphi for Win32 universe.

Different project types produce different types of .NET assemblies: A Delphi program produces an executable assembly; a library project produces a DLL assembly.

The Intermediate Language

The Common Language Runtime is an implementation of a virtual execution system, or virtual machine. Like all virtual machines, the CLR has its own abstract microprocessor. As already mentioned, the assembly language of the virtual processor is called Common Intermediate Language (CIL), although before being promoted as a standard it was called Microsoft Intermediate Language (MSIL), a terms you'll still see around often. Compilers that target the CLR do not generate code in the native instruction set of any specific, real microprocessor. Instead, .NET compilers target the abstract processor of the CLR. The hardware abstraction built into the CLR hints at some cross-platform viability. Remember, Microsoft's CLR is but one implementation of the CLI; any hardware/operating system platform that has a CLI-compliant execution environment built for it could be targeted.

Of course, there is no real microprocessor that can execute CIL directly, so it must be compiled to the instruction set of the native hardware prior to execution. This is the job of the Just in Time (JIT) compiler. Here is where the CLR differs from other virtual machine implementations (like Java). The CLR is not an interpreter, nor does it execute bytecode. On the .NET platform, CIL is always compiled to native CPU instructions, and once compiled it is cached in memory; so chances are good that it will never have to be recompiled.

  Note 

In some memory-constrained environments (such as a PDA), compiled code can be discarded. In this case, the code would need to be recompiled if it was ever reloaded.

Compiling IL is not a very expensive operation (MS Research has spent years developing technology to allow the JIT compilation to be as negligible as possible) but does imply a little overhead and it must be repeated every time you run even the same program. Most applications will see a little increase in startup time (what's particularly slow is loading all the .NET framework itself with the first .NET application run in a Windows session); however, this is limited because not all code in an application is compiled at once. The JIT compiler works in conjunction with the loader, and so IL code is not compiled until it is called (on a method by method basis).

JIT compilation is the normal case on the .NET platform, but it is possible to compile a managed executable into native instructions and store the native image on disk. By doing so, you avoid the negative impact of JIT compilation on your application's startup time. The .NET Framework runtime contains a utility called the Native Image Generator (Ngen.exe) to accomplish this.

The native image created by Ngen is stored in the native image cache. The next time the CLR tries to load the assembly, it looks in the native image cache first. If a native image of the assembly is found, it is preferred over the IL version. Note that you must also deploy the IL version of the assembly, because the native image does not contain any metadata. In addition, the end user or administrator could remove your native image from the cache. In this case, there would be no native image to find, and the CLR would revert back to the usual JIT compilation of the IL version.

The ability to generate a native executable image can be helpful, but you should profile your application under both environments (JIT and native image) to see whether the detrimental effects of the JIT compiler are that bad. The JIT compiler is, after all, a real compiler for a specific microprocessor, and as such it can do some performance optimizations of its own. It employs good algorithms to reduce its overhead and also introduces optimizations to the compiled code (somewhat like Delphi does in its compilations).

Looking over the IL code generated by your compiler can be highly educational. Microsoft provides a utility called the IL Disassembler (ildasm.exe) that you can use to dissect your assemblies at the lowest level. Located in the bin directory of your .NET Framework SDK installation, ildasm can load any assembly and its metadata: the manifest, the classes, their methods and properties, and, of course, the IL code generated by the compiler. We will look more at ILDASM in this chapter and the next. The Reflector tool mentioned earlier is also useful in this regard.

Managed and Safe Code

Simply put, managed code is any code that is loaded, JIT compiled, and executed under the auspice of the CLR. Like all executable and library modules on the Windows platform, managed code modules are stored in Microsoft's Portable Executable (PE) file format. A managed PE file contains additional header information and, when loaded, jumps into the runtime's execution engine (to a function in MSCorEE.dll).

The runtime initializes, and then it looks for the module's entry point. The IL code in the entry point is JIT compiled to native CPU instructions. Finally, execution begins at the module's entry point. The situation is similar for a library module; the PE file directs the loader to jump to a different function in MSCorEE.dll.

In contrast, unmanaged code consists not of IL, but rather of traditional, native CPU instructions. Unmanaged code executes outside of the runtime and therefore can't take advantage of the services provided by the CLR—at least not without special measures.

Unmanaged code can create .NET Framework classes using COM Interop services. The .NET Framework class is wrapped in a COM proxy and exposed to unmanaged code as if it were a COM object. The COM Interop bridge goes the other way too, allowing a COM objects in a COM server to be accessed by managed code. Finally, the Platform Invoke services of the CLR allow managed code to call the Win32 API directly.

  Note 

The Delphi for .NET Preview compiler produces fully managed code. There is currently no support for mixing managed and unmanaged (native) code within the same module, as you can with Microsoft's Visual C++ .NET (which uses a mechanism called IJW: It Just Works).

A module is completely self-describing, because it contains both IL code and metadata that describes the data elements used by the code. Taking the IL code and the metadata together, the CLR can perform another level of verification beyond the static checking done by the compiler. This process, which is always performed unless a system administrator turns it off, verifies that code is type safe. Verifiably type-safe code is known as safe code. Safe code passes the following type-safety checks:

  • Only valid operations are invoked on objects. This includes parameter validation, return type verification, and visibility checks.
  • Objects are always assigned to compatible types.
  • The code uses no explicit pointers, as they might refer to invalid memory locations.

As you'd expect, unsafe code fails to pass these checks. But, just because code is not verifiably type safe does not mean it is unsafe; it simply means the code could not be verified, either due to a limitation in the verification process, or perhaps in the compiler itself. When it will be released as a finished product, the Delphi for .NET compiler is expected to generate verifiably type-safe code.

Some Delphi language constructs are not CLS compliant, but this is different than not being verifiably type safe. Non-CLS-compliant language constructs will be covered more in Chapter 25.

The .NET Framework SDK contains a PEVerify utility that exhaustively analyzes a managed PE for type safety (peverify.exe). The Delphi 7 IDE plug-in mentioned previously lets you automatically run PEVerify on your code after every build.

The Common Type System

The Common Type System (CTS) is the bulldozer that levels the playing field for programming languages in the .NET framework. The CTS fully specifies the primitive types and object types known to the CLR. These types are used to define an object model that is shared among all languages that target the CLR.

The Component Object Model (COM) has been the usual way to achieve binary compatibility and language interoperability on the Windows platform. The CTS goes beyond that, allowing languages as different as Eiffel, C#, and Delphi to integrate with each other. Components written in these disparate languages can pass objects among themselves and directly extend their capabilities through inheritance. This level of integration of programming languages is unprecedented.

All types defined by the CTS fall into two categories: value types and reference types. Value types, as their name implies, have pass-by-value semantics. For example, say you have a variable that is a value type. If you pass this variable as a parameter to a function and modify the parameter within the function, the original variable will be unaffected. Examples of value types include scalar types, enumerations, and records. Aggregate types such as Delphi records (or C# structures) are known as value classes within the CTS.

On the other hand, reference types have alias, or pass-by-reference, semantics. If you have a variable that is a reference type (for example, an instance of a class) and you pass that variable as a parameter to a function, any changes you make to the parameter will also affect the variable. Examples of reference types include class types and interfaces. Pointer types are also reference types, as are delegates, which will be discussed shortly.

Objects and Properties

Like Delphi, the CTS implements a single-inheritance model. A class must inherit from one and only one ancestor and may declare itself to be an implementer of zero or more interfaces. Other familiar object-oriented attributes of the CTS are private, public, and protected visibility of classes and class members (with other visibility specifiers available, as discussed in the next chapter). These CTS visibility specifiers have meanings similar to those in Delphi; however, they are more restricted, in line with C++ semantics. (In Chapter 25 we'll look at how Delphi's visibility specifiers map to the CTS versions, and examine the specific changes made to the language thus far to accommodate additional features of the CTS.)

As you peruse the .NET literature, you will notice similarities between the capabilities of CTS class types and Delphi classes. The traditional object-oriented features of fields and methods are supported, of course. In addition, the CTS implements properties in a way that is conceptually similar to the familiar Delphi notion. Properties in CTS can have read and write access methods that restrict or compute values on the fly, or they can simply mask private fields. However there are also many differences, including the fact that property get/set methods must have the same visibility as the property itself to ensure languages that don't support property syntax can still access the property. Although Delphi doesn't enforce this in the source code, it modifies compiled code behind the scenes if necessary.

Events and Delegates

One reason the Win32 API has survived so long is that at its lowest levels it is based on fundamental concepts, such as using the address of a function as a callback mechanism. The entire Windows user interface event system is based on callback functions (and some of events in the VCL framework are built on top of even that system). The callback mechanism is so powerful that it surely must make its way into the CTS. Using callbacks in a type-safe, language-neutral way relies on a reference type called a delegate.

CTS delegates are different from ordinary function pointers, in that they can reference both static and instance methods of a class. The declaration of a delegate must match the signature of the methods the delegate will reference. In Delphi for .NET, usage of delegates is similar to that of familiar procedural types:

type
 TMyClass = class
 public
 procedure myMethod;
 end;
 
var
 threadDelegate: System.Threading.ThreadStart;
 tmc: TMyClass;
 aThread: System.Threading.Thread;
 
begin
 tmc := TMyClass.Create;
 threadDelegate := @tmc.myMethod;
 aThread := Thread.Create (threadDelegate);
 aThread.Start;

The threadDelegate variable is of type System.Threading.ThreadStart, which is a CLR delegate class. Methods you assign to a delegate have a signature matching the delegate's, which in this case is a procedure taking no parameters. (You can find this code snippet in the Delegate sample folder.)

The compiler is hiding a lot of complexity here. Behind the scenes, the compiler must create an instance of the class System.MulticastDelegate. The delegate—the function being encapsulated (myMethod in this case)—is invoked using methods on the MulticastDelegate class. This class supports the simultaneous encapsulation of multiple functions in a single delegate. In a user interface event model, this translates to having multiple listeners for an event.

  Note 

Incidentally, the fact that delegates are classes tells you why you can declare a delegate outside the scope of a class. Because delegates are instances of System.MulticastDelegate (or a compiler-generated descendant class), you can declare them anywhere you can declare a class.

Each specific language compiler implements some form of semantic sugar to make the creation of events and the addition and removal of event listeners less painful. For example, in C#, Microsoft used the += and -= operators to add and remove functions from the underlying delegate. Delphi for .NET uses set semantics for the same purpose. In Chapter 25 we'll explore this topic, showing how the functions Include and Exclude are used to assign event handlers. We'll also look at how Delphi's := assignment operator works, with regard to assigning event handlers in the .NET universe.

Garbage Collection

The garbage collector is that part of the CLR that performs automatic management of memory allocation and deallocation. All Delphi 7 offers in this respect is reference counting for interface-based variables (see Chapter 2 for details). When there are no more references to an object, the memory for it is reclaimed. This is also the basic idea of the CLR garbage collector, but the similarities stop there. The CLR's garbage collector can detect that two objects are still referring one to the other, but there is no other reference to them, so they can be discarded, something a reference counting system (like the one in Delphi) fails to handle.

Using a GC means you can create objects, reference one from another as you need, and simply forget about the hassles of deleting those objects. The system will do all that's needed for you. That's all you really need to know. Note that this implies that you don't need to balance object creation and destruction in your code architecture, nor do you need to use try/finally blocks to make sure objects are destroyed; these are just a couple of specific circumstances in which you'll benefit from a GC.

Only when you need to write low-level classes will you have to remember to free unmanaged resources, such as window or file handles. The CLR class System.Object contains a protected Finalize method that you can override to free up unmanaged resources. You must be aware of some important things before you begin overriding Finalize. (If you want some in-depth information on this topic, see the sidebar "Issues with Overriding Finalize.")

Issues with Overriding Finalize

Overriding the Finalize method is inefficient because it requires your object to make at least two trips through the garbage collector. The job of the garbage collector is to reclaim memory, and it can't do this until it runs your object's Finalize method. So, the garbage collector must give special treatment to all objects with Finalize methods. It puts them on a special list, which has the side effect of keeping them alive through this round of garbage collection (because there is now another reference to the object). Eventually, a thread walks through the special list, runs each object's Finalize method, and removes the object from the list. Removing the object from the list takes away the last reference to it, so next time the garbage collector runs, the object's memory can be reclaimed.

But inefficiency is not the only problem with finalizers. You also can never be sure when your object's finalizer will run. If you are relying on the Finalize method to give back unmanaged resources, the object might hold onto these resources longer than you expect.

When Finalize runs, it does not run in the same thread of execution as your object. This means all thread synchronization and blocking must be avoided, because it's not your thread you are blocking, it's the CLR's finalizer thread. On a related note, if an exception is thrown from Finalize the CLR catches it, not you, and the exception will be swallowed.

The recommended approach for freeing up unmanaged resources is to implement the dispose pattern. This is a strategy for providing an object both an automatic way to dispose the external resources it refers to when it is garbage collected and also a way to free the same resources upon the direct invocation of a method. In real terms, coding this pattern equates to implementing an interface called IDisposable. IDisposable consists of one method: Dispose. The IDisposable interface gives you deterministic control over freeing up resources used in your objects. Unlike Finalize, the Dispose method is public and is meant to be called by you (or users of your class), not by the garbage collector.

You may have read .NET literature explaining how the C# language implements destructor semantics by having the compiler create a Finalize method on your class behind the scenes. To implement the IDisposable interface in C#, you must do so directly and wire everything so that the Dispose method can be called directly or from the automatically generated Finalize method. This is not the way things work in Delphi for .NET.

Creating a destructor for your class does not cause the compiler to implement a finalizer behind the scenes. Instead, it causes the compiler to implement the IDisposable interface. But nothing is stopping you from implementing IDisposable directly, so if you want to rely on the compiler's behind-the-scenes magic, you must follow a strict pattern. Your class destructor must be declared exactly as shown here:

destructor Destroy; override;

When the compiler sees this declaration, it generates code to mark your class as an implementor of IDisposable. Then, using a feature of CIL, your destructor is marked as the implementation of the Dispose method (this can be done even though the name of the method is Destroy, not Dispose).

The usual way to dispose of objects in Delphi is to call the Free method on the object. In Delphi for .NET, the Free method is implemented so that it tests to see whether the object implements the IDisposable interface. If you follow the previous destructor signature, the compiler implements it for you; Free then calls Dispose, which winds up in your Destroy method.

Let's create a project and declare a class with a destructor that follows the pattern. Then you'll use ILDASM to inspect the generated code. The code is available in Listings 24.1 and 24.2.

Listing 24.1: The Project of the DestructorTest Example

program DestructorTest;
 
{$APPTYPE CONSOLE}
 
uses
 MyClass in 'MyClass.pas';
 
var
 test : TMyClass;
 
begin
 WriteLn ('DestructorTest starts');
 test := TMyClass.Create;
 test.Free;
 WriteLn ('DestructorTest ends');
end.

Listing 24.2: The Unit of the DestructorTest Example

unit MyClass;
 
interface
 
type
 TMyClass = class
 public
 destructor Destroy; override;
 end;
 
implementation
 
destructor TMyClass.Destroy;
begin
 WriteLn ('In destructor (which is actually the IDisposable.Dispose method)');
end;
 
end.

After compiling this program you can run it; but we are not so much interested in running the program as in inspecting it with ILDASM. Start ILDASM and select File | Open. Navigate to the directory where DestructorTest.exe is located, and open it. You will see a window similar to that shown in Figure 24.2.

click to expand
Figure 24.2: The ILDASM output for the DestructorTest example

The tree entry labeled MyClass represents the namespace created to hold all the symbols in the unit. Expand the MyClass node and notice the entries for TMyClass and Unit. Delphi for .NET creates a CLR class for each unit to implement initialization and finalization and also to let you still write global routines; they become methods of that Unit class. Expand the node for TMyClass and notice the Destroy node, which has a pink block. This node represents the Destroy method in TMyClass. Double-click the Destroy node to open a window displaying the full IL code for this method, as shown in Figure 24.3.

click to expand
Figure 24.3: The ILDASM IL window for the Destroy destructor

The line you're looking for uses the .override directive:

.override [mscorlib]System.IDisposable::Dispose

This is an explicit override, and it says that the Destroy method is the implementation of Dispose in the IDisposable interface. The .override directive is how the thread of execution winds up in your destructor when Dispose is called from Free.

  Note 

The way IDisposable was dealt with illustrates a familiar pattern in the ongoing work on the Delphi for .NET compiler. Unlike C#, Delphi is an old language with a large and loyal following and a sizable existing code base. Borland could not sweep everything aside and mandate a new way of doing things that would break all existing code. The implementation of IDisposable was done in a way that should allow Delphi programmers to continue using familiar paradigms and minimize the impact of porting their existing code to the .NET platform.

Garbage Collection and Efficiency

Among programmers, the use of a garbage collector (GC) is the topic of many debates. Most programmers like the fact that a GC helps them reduce the chance of memory management errors. Some programmers, though, fear that a GC might not be efficient enough, a fear that often prevented the widespread use of this technology. The inefficient GC of the early Java virtual machines didn't really help in this respect. Even Microsoft has a hard time convincing all programmers to use its GC, up to the point that they overrate their solution depicting it as perfect. Even in Microsoft technical documentation about the GC, you'll find lots of hype and little facts.

  Note 

This is not to say that the GC doesn't do an adequate job; quite the contrary. But it works differently than it is presented. At present, your only way to know how the GC works is to write test case programs and study their effect.

As a starting point for your exploration, you can use the GarbageTest example. Use a Windows memory analysis tool to see how the memory is affected by program execution. The GarbageTest example declares the following class of large objects (about 10 KB):

type
 TMyClass = class
 private
 data: Integer;
 list: array [1..10240] of Char;
 s: string;
 public
 constructor Create;
 end;

The simplest test code is the following:

for i := 1 to 10000 do
begin
 mc := TMyClass.Create;
 WriteLn (mc.s);
end;

Writing this simple program in Delphi will flood the memory, because there is no call to Free. In .NET, however, the GC reuses the single memory block, so memory consumption is flat. If you save each object in an array declared as

objlist: array [1..10000] of TMyClass;

the memory consumption will increase at each cycle of the loop, causing problems! Keeping the object references in memory and then releasing them regularly or randomly can provide more complex tests. You'll find a few snippets in the program code, but you'll have to exercise your knowledge and imagination to build other significant test cases.

Deployment and Versioning

Most Windows software developers have at some point felt the pain caused by deploying shared libraries. The .NET Framework can adapt to a number of different deployment scenarios. In the simplest scenario, the end user can copy files to a directory and go. When it's time to remove the application from the machine, the end user can remove the files, or the entire directory, if it doesn't contain any data. In this scenario there is no installer, no uninstaller, and no registry to deal with.

  Tip 

You can package your application for the Microsoft Installer if you want to and still deploy to a single directory, with no Registry impact.

Outside of the easy scenario, the deployment options become complex and a bit bewildering, because the problem itself is complicated. How do you enable developers to deploy new versions of shared components, while at the same time ensuring that the new version won't break an application that depends on behavior provided by a prior version? The only answer is to develop a system that allows differing versions of the same component to be deployed side-by-side.

The first thing you need to learn about deployment in the .NET Framework is that the days of deploying shared components to C:WindowsSystem32 are gone. Instead, the .NET Framework includes two kinds of assemblies, well-defined rules governing how the system searches for them, and an infrastructure that supports the deployment of multiple versions of the same assembly on the same machine.

Before talking about the two kinds of assemblies, I will explain the two kinds of deployment:

Public, or Global  A publicly deployed assembly is one you intend to share either among your own applications or with applications written by other people. The .NET Framework specifies a well-known location in which you are to deploy such assemblies.

Private  A privately deployed assembly is one you do not intend to share. It is typically deployed to your application's base installation directory.

The type of assembly you create depends on how you intend to deploy it. The two types of assemblies are as follows:

Strong Name Assemblies  A strong name assembly is digitally signed. The signature consists of the assembly's identity (name, version, and optional culture plus a key pair with public and private components). All global assemblies must be strong name assemblies. The name, version, and culture are attributes of the assembly, stored in its metadata. How you create and update these attributes depends largely on your development tools.

Everything Else  There is no official name for an assembly that does not have a strong name. An assembly that does not have a strong name can only be deployed privately, and it cannot take advantage of the side-by-side versioning features of the CLR.

The key pair that completes the signing of a strong name assembly is generated by a tool provided by the .NET Framework SDK, called SN (sn.exe). SN creates a key file, which is referenced by the assembly in an attribute. The signing of an assembly can only take place when it is created, or built. However, in the form of signing called delayed signing, developers work only with the public part of the key. Later, when the final build of the assembly is prepared, the private key is added to the signature. Currently the Delphi for .NET compiler is weak on custom attributes. Because attributes are required to express the identity and reference the key file, support for strong name assemblies isn't quite complete at the time of writing.

Strong name assemblies are usually intended to be shared, so you deploy them in the Global Assembly Cache (GAC) (but they can be deployed privately as well). Because the GAC is a system folder with a specialized structure, you can't just copy the assembly there. Instead, you must use another tool contained in the .NET Framework runtime, called GACUTIL (gacutil.exe). GACUTIL installs files into and removes files from the GAC.

The GAC is a special system folder with a hierarchy that supports side-by-side deployment of assemblies, as you can see in Figure 24.4. The intent is to hide the structure of the GAC so you don't have to worry about the complexities involved. GACUTIL reads the assembly's identity from its metadata and uses that information to construct a folder within the GAC to hold the assembly.

click to expand
Figure 24.4: .NET's Global Assembly Cache as see in Windows Explorer

  Note 

You can see the true folder hierarchy by opening a command window and navigating to the C:Windowsassembly directory. This hierarchy is hidden from you if you use Windows Explorer to view it.

When you create a reference to a strong name assembly in the GAC, you are creating a reference to a specific version of that assembly. If a later version of that same assembly is deployed on the machine, it will be put into a separate location in the GAC, leaving the original intact. Because your assembly referenced a specific version, that's the version it always gets.

What s Next?

Your reaction to the .NET initiative will depend on your programming background. If you come from the Java environment, from a traditional Windows tool (even if object-oriented), or from Delphi, you may find many new features or many concepts you are already familiar with. The .NET platform is a technical achievement that offers a staggering array of options and possibilities. Compared with the earlier Microsoft system interfacing technologies (the Windows API and COM), the progress is significant. The best part is that you can take advantage of the platform using the tools and languages you feel are best for the job, soon including Delphi.

In Chapter 25 we will examine specific changes to the Delphi language. We will look at deprecated features and types, new features that have already been added to the language, and examples demonstrating the use of .NET Framework classes from Delphi for .NET.



Mastering Delphi 7
Mastering Delphi 7
ISBN: 078214201X
EAN: 2147483647
Year: 2006
Pages: 279

Flylib.com © 2008-2020.
If you may any questions please contact us: flylib@qtcs.net