The Delphi Object Model (PART III)
Sample Chapter of Delphi in a Nutshell
Product:
Delphi all versions
Category:
Skill Level:
Scoring:
Last Update:
05/21/2000
Search Keys:
delphi delphi3000 article Object Model OOP interfaces
Times Scored:
11
Visits:
4947
Uploader: Stefan Walther
Company: bluestep.com IT-Consulting
Reference: Ray Lischner - O"Reilly
Question/Problem/Abstract:
Delphi's support for object-oriented programming is rich and powerful. In addition to traditional classes and objects, Delphi also has interfaces (similar to those found in COM and Java), exception handling, and multithreaded programming. This chapter covers Delphi's object model in depth. You should already be familiar with standard Pascal and general principles of object-oriented programming.
Answer:
Reprinted with permission from O'Reilly & Associates
Messages
You should be familiar with Windows messages: user interactions and other events generate messages, which Windows sends to an application. An application processes messages one at a time to respond to the user and other events. Each kind of message has a unique number and two integer parameters. Sometimes a parameter is actually a pointer to a string or structure that contains more complex information. Messages form the heart of Windows event-driven architecture, and Delphi has a unique way of supporting Windows messages.
In Delphi, every object--not only window controls--can respond to messages. A message has an integer identifier and can contain any amount of additional information. In the VCL, the Application object receives Windows messages and maps them to equivalent Delphi messages. In other words, Windows messages are a special case of more general Delphi messages.
A Delphi message is a record where the first two bytes contain an integer message identifier, and the remainder of the record is programmer-defined. Delphi's message dispatcher never refers to any part of the message record past the message number, so you are free to store any amount or kind of information in a message record. By convention, the VCL always uses Windows-style message records (TMessage), but if you find other uses for Delphi messages, you don't need to feel so constrained.
To send a message to an object, fill in the message identifier and the rest of the message record and call the object's Dispatch method. Delphi looks up the message number in the object's message table. The message table contains pointers to all the message handlers that the class defines. If the class does not define a message handler for the message number, Delphi searches the parent class's message table. The search continues until Delphi finds a message handler or it reaches the TObject class. If the class and its ancestor classes do not define a message handler for the message number, Delphi calls the object's DefaultHandler method. Window controls in the VCL override DefaultHandler to pass the message to the window procedure; other classes usually ignore unknown messages. You can override DefaultHandler to do anything you want, perhaps raise an exception.
Use the message directive to declare a message handler for any message. See Chapter 5 for details about the message directive.
Message handlers use the same message table and dispatcher as dynamic methods. Each method that you declare with the dynamic directive is assigned a 16-bit negative number, which is really a message number. A call to a dynamic method uses the same dispatch code to look up the dynamic method, but if the method is not found, that means the dynamic method is abstract, so Delphi calls AbstractErrorProc to report a call to an abstract method.
Because dynamic methods use negative numbers, you cannot write a message handler for negative message numbers, that is, message numbers with the most-significant bit set to one. This limitation should not cause any problems for normal applications. If you need to define custom messages, you have the entire space above WM_USER ($0F00) available, up to $7FFF. Delphi looks up dynamic methods and messages in the same table using a linear search, so with large message tables, your application will waste time performing method lookups.
Delphi's message system is entirely general purpose, so you might find a creative use for it. Usually, interfaces provide the same capability, but with better performance and increased type-safety.
Memory Management
Delphi manages the memory and lifetime of strings, Variants, dynamic arrays, and interfaces automatically. For all other dynamically allocated memory, you--the programmer--are in charge. It's easy to be confused because it seems as though Delphi automatically manages the memory of components, too, but that's just a trick of the VCL.
Components Versus Objects
The VCL's TComponent class has two fancy mechanisms for managing object lifetimes, and they often confuse new Delphi programmers, tricking them into thinking that Delphi always manages object lifetimes. It's important that you understand exactly how components work, so you won't be fooled.
Every component has an owner. When the owner is freed, it automatically frees the components that it owns. A form owns the components you drop on it, so when the form is freed, it automatically frees all the components on the form. Thus, you don't usually need to be concerned with managing the lifetime of forms and components.
When a form or component frees a component it owns, the owner also checks whether it has a published field of the same name as the component. If so, the owner sets that field to nil. Thus, if your form dynamically adds or removes components, the form's fields always contain valid object references or are nil. Don't be fooled into thinking that Delphi does this for any other field or object reference. The trick works only for published fields (such as those automatically created when you drop a component on a form in the IDE's form editor), and only when the field name matches the component name.
Memory management is thread-safe, provided you use Delphi's classes or functions to create the threads. If you go straight to the Windows API and the CreateThread function, you must set the IsMultiThread variable to True. For more information, see Chapter 4, Concurrent Programming.
Ordinarily, when you construct an object, Delphi calls NewInstance to allocate and initialize the object. You can override NewInstance to change the way Delphi allocates memory for the object. For example, suppose you have an application that frequently uses doubly linked lists. Instead of using the general-purpose memory allocator for every node, it's much faster to keep a chain of available nodes for reuse. Use Delphi's memory manager only when the node list is empty. If your application frequently allocates and frees nodes, this special-purpose allocator can be faster than the general-purpose allocator. Example 2-17 shows a simple implementation of this scheme. (See Chapter 4 for a thread-safe version of this class.)
Example 2-17: Custom Memory Management for Linked Lists
type
TNode = class
private
fNext, fPrevious: TNode;
protected
// Nodes are under control of TLinkedList.
procedure Relink(NewNext, NewPrevious: TNode);
constructor Create(Next: TNode = nil; Previous: TNode = nil);
procedure RealFree;
public
destructor Destroy; override;
class function NewInstance: TObject; override;
procedure FreeInstance; override;
property Next: TNode read fNext;
property Previous: TNode read fPrevious;
end;
// Singly linked list of nodes that are free for reuse.
// Only the Next fields are used to maintain this list.
var
NodeList: TNode;
// Allocate a new node by getting the head of the NodeList.
// Remember to call InitInstance to initialize the node that was
// taken from NodeList.
// If the NodeList is empty, allocate a node normally.
class function TNode.NewInstance: TObject;
begin
if NodeList = nil then
Result := inherited NewInstance
else
begin
Result := NodeList;
NodeList := NodeList.Next;
InitInstance(Result);
end;
end;
// Because the NodeList uses only the Next field, set the Previous
// field to a special value. If a program erroneously refers to the
// Previous field of a free node, you can see the special value
// and know the cause of the error.
const
BadPointerValueToFlagErrors = Pointer($F0EE0BAD);
// Free a node by adding it to the head of the NodeList. This is MUCH
// faster than using the general-purpose memory manager.
procedure TNode.FreeInstance;
begin
fPrevious := BadPointerValueToFlagErrors;
fNext := NodeList;
NodeList := Self;
end;
// If you want to clean up the list properly when the application
// finishes, call RealFree for each node in the list. The inherited
// FreeInstance method frees and cleans up the node for real.
procedure TNode.RealFree;
begin
inherited FreeInstance;
end;
You can also replace the entire memory management system that Delphi uses. Install a new memory manager by calling SetMemoryManager. For example, you might want to replace Delphi's suballocator with an allocator that performs additional error checking. Example 2-18 shows a custom memory manager that keeps a list of pointers the program has allocated and explicitly checks each attempt to free a pointer against the list. Any attempt to free an invalid pointer is refused, and Delphi will report a runtime error (which SysUtils changes to an exception). As a bonus, the memory manager checks that the list is empty when the application ends. If the list is not empty, you have a memory leak.
Example 2-18: Installing a Custom Memory Manager
unit CheckMemMgr;
interface
uses Windows;
function CheckGet(Size: Integer): Pointer;
function CheckFree(Mem: Pointer): Integer;
function CheckRealloc(Mem: Pointer; Size: Integer): Pointer;
var
HeapFlags: DWord; // In a single-threaded application, you might
// want to set this to Heap_No_Serialize.
implementation
const
MaxSize = MaxInt div 4;
type
TPointerArray = array[1..MaxSize] of Pointer;
PPointerArray = ^TPointerArray;
var
Heap: THandle; // Windows heap for the pointer list
List: PPointerArray; // List of allocated pointers
ListSize: Integer; // Number of pointers in the list
ListAlloc: Integer; // Capacity of the pointer list
// If the list of allocated pointers is not empty when the program
// finishes, that means you have a memory leak. Handling the memory
// leak is left as an exercise for the reader.
procedure MemoryLeak;
begin
// Report the leak to the user, but remember that the program is
// shutting down, so you should probably stick to the Windows API
// and not use the VCL.
end;
// Add a pointer to the list.
procedure AddMem(Mem: Pointer);
begin
if List = nil then
begin
// New list of pointers.
ListAlloc := 8;
List := HeapAlloc(Heap, HeapFlags, ListAlloc * SizeOf(Pointer));
end
else if ListSize >= ListAlloc then
begin
// Make the list bigger. Try to do it somewhat intelligently.
if ListAlloc < 256 then
ListAlloc := ListAlloc * 2
else
ListAlloc := ListAlloc + 256;
List := HeapRealloc(Heap, HeapFlags, List,
ListAlloc * SizeOf(Pointer));
end;
// Add a pointer to the list.
Inc(ListSize);
List[ListSize] := Mem;
end;
// Look for a pointer in the list, and remove it. Return True for
// success, and False if the pointer is not in the list.
function RemoveMem(Mem: Pointer): Boolean;
var
I: Integer;
begin
for I := 1 to ListSize do
if List[I] = Mem then
begin
MoveMemory(@List[I], @List[I+1], (ListSize-I) * SizeOf(Pointer));
Dec(ListSize);
Result := True;
Exit;
end;
Result := False;
end;
// Replacement memory allocator.
function CheckGet(Size: Integer): Pointer;
begin
Result := SysGetMem(Size);
AddMem(Result);
end;
// If the pointer isn't in the list, don't call the real
// Free function. Return 0 for success, and non-zero for an error.
function CheckFree(Mem: Pointer): Integer;
begin
if not RemoveMem(Mem) then
Result := 1
else
Result := SysFreeMem(Mem);
end;
// Remove the old pointer and add the new one, which might be the
// same as the old one, or it might be different. Return nil for
// an error, and Delphi will raise an exception.
function CheckRealloc(Mem: Pointer; Size: Integer): Pointer;
begin
if not RemoveMem(Mem) then
Result := nil
else
begin
Result :=SysReallocMem(Mem, Size);
AddMem(Result);
end;
end;
procedure SetNewManager;
var
Mgr: TMemoryManager;
begin
Mgr.GetMem := CheckGet;
Mgr.FreeMem := CheckFree;
Mgr.ReallocMem := CheckRealloc;
SetMemoryManager(Mgr);
end;
initialization
Heap := HeapCreate(0, HeapFlags, 0);
SetNewManager;
finalization
if ListSize <> 0 then
MemoryLeak;
HeapDestroy(Heap);
end.
If you define a custom memory manager, you must ensure that your memory manager is used for all memory allocation. The easiest way to do this is to set the memory manager in a unit's initialization section, as shown in Example 2-18. The memory management unit must be the first unit listed in the project's uses declaration.
Ordinarily, if a unit makes global changes in its initialization section, it should clean up those changes in its finalization section. A unit in a package might be loaded and unloaded many times in a single application, so cleaning up is important. A memory manager is different, though. Memory allocated by one manager cannot be freed by another manager, so you must ensure that only one manager is active in an application, and that the manager is active for the entire duration of the application. This means you must not put your memory manager in a package, although you can use a DLL, as explained in the next section.
Memory and DLLs
If you use DLLs and try to pass objects between DLLs or between the application and a DLL, you run into a number of problems. First of all, each DLL and EXE keeps its own copy of its class tables. The is and as operators do not work correctly for objects passed between DLLs and EXEs. Use packages (described in Chapter 1) to solve this problem. Another problem is that any memory allocated in a DLL is owned by that DLL. When Windows unloads the DLL, all memory allocated by the DLL is freed, even if the EXE or another DLL holds a pointer to that memory. This can be a major problem when using strings, dynamic arrays, and Variants because you never know when Delphi will allocate memory automatically.
The solution is to use the ShareMem unit as the first unit of your project and every DLL. The ShareMem unit installs a custom memory manager that redirects all memory allocation requests to a special DLL, BorlndMM.dll. The application doesn't unload BorlndMM until the application exits. The DLL magic takes place transparently, so you don't need to worry about the details. Just make sure you use the ShareMem unit, and make sure it is the first unit used by your program and libraries. When you release your application to your clients or customers, you will need to include BorlndMM.dll.
If you define your own memory manager, and you need to use DLLs, you must duplicate the magic performed by the ShareMem unit. You can replace ShareMem with your own unit that forwards memory requests to your DLL, which uses your custom memory manager. Example 2-19 shows one way to define your own replacement for the ShareMem unit.
Example 2-19: Defining a Shared Memory Manager
unit CheckShareMem;
// Use this unit first so all memory allocations use the shared
// memory manager. The application and all DLLs must use this unit.
// You cannot use packages because those DLLs use the default Borland
// shared memory manager.
interface
function CheckGet(Size: Integer): Pointer;
function CheckFree(Mem: Pointer): Integer;
function CheckRealloc(Mem: Pointer; Size: Integer): Pointer;
implementation
const
DLL = 'CheckMM.dll';
function CheckGet(Size: Integer): Pointer; external DLL;
function CheckFree(Mem: Pointer): Integer; external DLL;
function CheckRealloc(Mem: Pointer; Size: Integer): Pointer;
external DLL;
procedure SetNewManager;
var
Mgr: TMemoryManager;
begin
Mgr.GetMem := CheckGet;
Mgr.FreeMem := CheckFree;
Mgr.ReallocMem := CheckRealloc;
SetMemoryManager(Mgr);
end;
initialization
SetNewManager;
end.
The CheckMM DLL uses your custom memory manager and exports its functions so they can be used by the CheckShareMem unit. Example 2-20 shows the source code for the CheckMM library.
Example 2-20: Defining the Shared Memory Manager DLL
library CheckMM;
// Replacement for BorlndMM.dll to use a custom memory manager.
uses
CheckMemMgr;
exports
CheckGet, CheckFree, CheckRealloc;
begin
end.
Your program and library projects use the CheckShareMem unit first, and all memory requests go to CheckMM.dll, which uses the error-checking memory manager. You don't often need to replace Delphi's memory manager, but as you can see, it isn't difficult to do.
TIP:
The memory manager that comes with Delphi works well for most applications, but it does not perform well in some cases. The average application allocates and frees memory in chunks of varying sizes. If your application is different and allocates memory in ever-increasing sizes (say, because you have a dynamic array that grows in small steps to a very large size), performance will suffer. Delphi's memory manager will allocate more memory than your application needs. One solution is to redesign your program so it uses memory in a different pattern (say, by preallocating a large dynamic array). Another solution is to write a memory manager that better meets the specialized needs of your application. For example, the new memory manager might use the Windows API (HeapAllocate, etc.).
Old-Style Object Types
In addition to class types, Delphi supports an obsolete type that uses the object keyword. Old-style objects exist for backward compatibility with Turbo Pascal, but they might be dropped entirely from future versions of Delphi.
Old-style object types are more like records than new-style objects. Fields in an old-style object are laid out in the same manner as in records. If the object type does not have any virtual methods, there is no hidden field for the VMT pointer, for example. Unlike records, object types can use inheritance. Derived fields appear after inherited fields. If a class declares a virtual method, its first field is the VMT pointer, which appears after all the inherited fields. (Unlike a new-style object, where the VMT pointer is always first because TObject declares virtual methods.)
An old-style object type can have private, protected, and public sections, but not published or automated sections. Because it cannot have a published section, an old object type cannot have any runtime type information. An old object type cannot implement interfaces.
Constructors and destructors work differently in old-style object types than in new-style class types. To create an instance of an old object type, call the New procedure. The newly allocated object is initialized to all zero. If you declare a constructor, you can call it as part of the call to New. Pass the constructor name and arguments as the second argument to New. Similarly, you can call a destructor when you call Dispose to free the object instance. The destructor name and arguments are the second argument to Dispose.
You don't have to allocate an old-style object instance dynamically. You can treat the object type as a record type and declare object-type variables as unit-level or local variables. Delphi automatically initializes string, dynamic array, and Variant fields, but does not initialize other fields in the object instance.
Unlike new-style class types, exceptions in old-style constructors do not automatically cause Delphi to free a dynamically created object or call the destructor.