Libraries and Packages
Overview
Windows executable files come in two flavors: programs (EXEs) and dynamic link libraries (DLLs). When you write a Delphi application, you typically generate a program file. However, Delphi applications often use calls to functions stored in dynamic libraries. Each time you call a Windows API function directly, you actually access a dynamic library. It is easy to generate a DLL in the Delphi environment. However, some problems can arise due to the nature of DLLs. Writing a dynamic library in Windows is not always as simple as it seems, because the dynamic library and the calling program need to agree on calling conventions, parameter types, and other details. This chapter covers the basics of DLL programming from the Delphi point of view.
The second part of the chapter will focus on a specific type of dynamic link library: the Delphi package. Packages in Delphi provide a good alternative to plain DLLs, although not so many Delphi programmers take advantage of them outside the realm of component writing. Here I'll share with you some tips and techniques for using packages to partition a large application.
The Role of DLLs in Windows
Before delving into the development of DLLs in Delphi and other programming languages, I'll give you a short technical overview of DLLs in Windows, highlighting the key elements. We will start by looking at dynamic linking, then see how Windows uses DLLs, and end with some general rules to follow when writing DLLs.
What Is Dynamic Linking?
First, it is important to fully understand the difference between static and dynamic linking of functions or procedures. When a subroutine is not directly available in a source file, the compiler adds the subroutine to an internal symbol table. Of course, the Delphi compiler must have seen the declaration of the subroutine and know about its parameters and type, or it will issue an error.
After compilation of a normal—static—subroutine, the linker fetches the subroutine's compiled code from a Delphi compiled unit (or static library) and adds it to the program's code. The resulting executable file includes all the code of the program and of the units involved. The Delphi linker is smart enough to include only the minimum amount of code from the program's units and to link only the functions and methods that are actually used. This is why it is called "smart linker."
Note |
A notable exception to this rule is the inclusion of virtual methods. The compiler cannot determine in advance which virtual methods the program will call, so it has to include them all. For this reason, programs and libraries with too many virtual functions tend to generate larger executable files. While developing the VCL, the Borland developers had to balance the flexibility obtained with virtual functions against the reduced size of the executable files achieved by limiting the virtual functions. |
In the case of dynamic linking, which occurs when your code calls a DLL-based function, the linker uses the information in the external declaration of the subroutine to set up an import table in the executable file. When Windows loads the executable file in memory, first it loads all the required DLLs, and then the program starts. During this loading process, Windows fills the program's import table with the addresses of the DLL functions in memory. If for some reason the DLL is not found or a referenced routine is not present in a DLL that is found, the program won't even start.
Each time the program calls an external function, it uses this import table to forward the call to the DLL code (which is now located in the program's address space). Note that this scheme does not involve two different applications. The DLL becomes part of the running program and is loaded in the same address space. All the parameter passing takes place on the application's stack (because the DLL doesn't have a separate stack) or in CPU registers. Because a DLL is loaded into the application's address space, any memory allocations of the DLL or any global data it creates reside in the address space of the main process. Thus, data and memory pointers can be passed directly from the DLL to the program and vice versa. This can also be extended to passing object references, which can be quite troublesome as the EXE and the DLL might have a different compiled class (and you can use packages exactly for this purpose, as we'll see later in this chapter).
There is another approach to using DLLs that is even more dynamic than the one I just discussed: At run time, you can load a DLL in memory, search for a function (provided you know its name), and call the function by name. This approach requires more complex code and takes some extra time to locate the function. The execution of the function, however, occurs with the same speed as calling an implicitly loaded DLL. On the positive side, you don't need to have the DLL available to start the program. We will use this approach in the DynaCall example later in the chapter.
What are DLLs For?
Now that you have a general idea of how DLLs work, we can focus on the reasons for using them. The first advantage is that if different programs use the same DLL, the DLL is loaded in memory only once, thus saving system memory. DLLs are mapped into the private address space of each process (each running application), but their code is loaded in memory only once.
Note |
To be more precise, the operating system will try to load the DLL at the same address in each application's address space (using the preferred base address specified by the DLL). If that address is not available in a particular application's virtual address space, the DLL code image for that process will have to be relocated—an operation that is expensive in terms of both performance and memory use, because the relocation happens on a per-process basis, not system-wide. |
Another interesting feature is that you can provide a different version of a DLL, replacing the current one, without having to recompile the application using it. This approach will work, of course, only if the functions in the DLL have the same parameters as the previous version. If the DLL has new functions, it doesn't matter. Problems may arise only if a function in the older version of the DLL is missing in the new one or if a function takes an object reference and the classes, base classes, or even compiler versions don't match.
This second advantage is particularly applicable to complex applications. If you have a very big program that requires frequent updates and bug fixes, dividing it into several executables and dynamic libraries allows you to distribute only the changed portions instead of a single large executable. Doing so makes sense for Windows system libraries in particular: You generally don't need to recompile your code if Microsoft provides an updated version of Windows system libraries—for example, in a new version of the operating system or a service pack.
Another common technique is to use dynamic libraries to store nothing but resources. You can build different versions of a DLL containing strings for different languages and then change the language at run time, or you can prepare a library of icons and bitmaps and then use them in different applications. The development of language-specific versions of a program is particularly important, and Delphi includes support for it through its Integrated Translation Environment (ITE).
Another key advantage is that DLLs are independent of the programming language. Most Windows programming environments, including most macro languages in end-user applications, allow a programmer to call a function stored in a DLL. This flexibility applies only to the use of functions, though. To share objects in a DLL across programming languages, you should move to the COM infrastructure or the .NET architecture.
Rules for Delphi DLL Writers
Delphi DLL programmers need to follow several rules. A DLL function or procedure to be called by external programs must follow these guidelines:
- It must be listed in the DLL's exports clause. This makes the routine visible to the outside world.
- Exported functions should also be declared as stdcall, to use the standard Win32 parameter-passing technique instead of the optimized register parameter-passing technique (which is the default in Delphi). The exception to this rule is if you want to use these libraries only from other Delphi applications. Of course you can also use another calling convention, provided the other compiler understands it (like cdecl, which is the default on C compilers).
- The types of a DLL's parameters should be the default Windows types (mostly C-compatible data types), at least if you want to be able to use the DLL within other development environments. There are further rules for exporting strings, as you'll see in the FirstDLL example.
- A DLL can use global data that won't be shared by calling applications. Each time an application loads a DLL, it stores the DLL's global data in its own address space, as you will see in the DllMem example.
- Delphi libraries should trap all internal exceptions, unless you plan to use the library only from other Delphi programs.
Using Existing DLLs
You have already used existing DLLs in examples in this book, when calling Windows API functions. As you might remember, all the API functions are declared in the system Windows unit. Functions are declared in the interface portion of the unit, as shown here:
function PlayMetaFile(DC: HDC; MF: HMETAFILE): BOOL; stdcall; function PaintRgn(DC: HDC; RGN: HRGN): BOOL; stdcall; function PolyPolygon(DC: HDC; var Points; var nPoints; p4: Integer): BOOL; stdcall; function PtInRegion(RGN: HRGN; p2, p3: Integer): BOOL; stdcall;
Then, in the implementation portion, instead of providing each function's code, the unit refers to the external definition in a DLL:
const gdi32 = 'gdi32.dll'; function PlayMetaFile; external gdi32 name 'PlayMetaFile'; function PaintRgn; external gdi32 name 'PaintRgn'; function PolyPolygon; external gdi32 name 'PolyPolygon'; function PtInRegion; external gdi32 name 'PtInRegion';
Note |
Windows.PAS makes heavy use of the {$EXTERNALSYM identifier} directive. This directive has little to do with Delphi itself; it applies to C++Builder. The symbol prevents the corresponding Delphi symbol from appearing in the C++ translated header file. This action helps keep the Delphi and C++ identifiers in synch, so that code can be shared between the two languages. |
The external definition of these functions refers to the name of the DLL they use. The name of the DLL must include the .DLL extension, or the program will not work under Windows NT/2000/XP (although it will work under Windows 9x). The other element is the name of the DLL function. The name directive is not necessary if the Delphi function (or procedure) name matches the DLL function name (which is case sensitive).
To call a function that resides in a DLL, you can provide its declaration in the interface section of a unit and external definition in the implementation section, as shown earlier, or you can merge the two in a single declaration in the implementation section of a unit. Once the function is properly defined, you can call it in your Delphi application code just like any other function.
Tip |
Delphi includes the Delphi language translation of a large number of Windows APIs, as you can see in the many files available in Delphi's SourceRtlWin folder. More Delphi units referring to other APIs are available as part of the Delphi Jedi project at www.delphi-jedi.org. |
Using a C++ DLL
As an example, I've written a DLL in C++ with some trivial functions, just to show you how to call DLLs from a Delphi application. I won't explain the C++ code in detail (it's basically C code) but will focus instead on the calls between the Delphi application and the C++ DLL. In Delphi programming, it is common to use DLLs written in C or C++.
Suppose you are given a DLL built in C or C++. You'll generally have in your hands a .DLL file (the compiled library), an .H file (the declaration of the functions inside the library), and a .LIB file (another version of the list of exported functions for the C/C++ linker). This LIB file is useless in Delphi; the DLL file is used as-is, and the H file must be translated into a Delphi unit with the corresponding declarations.
In the following listing, you can see the declaration of the C++ functions I've used to build the CppDll library example. The complete source code and the compiled version of the C++ DLL and the source code of the Delphi application using it are in the CppDll directory. You should be able to compile this code with any C++ compiler; I've tested it only with Borland C++Builder. Here are the C++ declarations of the functions:
extern "C" __declspec(dllexport) int WINAPI Double (int n); extern "C" __declspec(dllexport) int WINAPI Triple (int n); __declspec(dllexport) int WINAPI Add (int a, int b);
The three functions perform some basic calculations on the parameters and return the result. Notice that all the functions are defined with the WINAPI modifier, which sets the proper parameter-calling convention; they are preceded by the __declspec(dllexport) declaration, which makes the functions available to the outside world.
Two of these C++ functions also use the C naming convention (indicated by the extern "C" statement), but the third one, Add, doesn't. This difference affects the way you call these functions in Delphi. The internal names of the first two functions correspond to their names in the C++ source code file. But because I didn't use the extern "C" clause for the Add function, the C++ compiler uses name mangling. This technique is used to include information about the number and type of parameters in the function name, which the C++ language requires in order to implement function overloading. The result when using the Borland C++ compiler is a funny function name: @Add$qqsii. You must use this name in the Delphi code to call the Add DLL function (which explains why you'll generally avoid C++ name mangling in exported functions and declare them all as extern "C"). The following are the declarations of the three functions in the Delphi CallCpp example:
function Add (A, B: Integer): Integer; stdcall; external 'CPPDLL.DLL' name '@Add$qqsii'; function Double (N: Integer): Integer; stdcall; external 'CPPDLL.DLL' name 'Double'; function Triple (N: Integer): Integer; stdcall; external 'CPPDLL.DLL';
As you can see, you can either provide or omit an alias for an external function. I've provided one for the first function (there was no alternative, because the exported DLL function name @Add$qqsii is not a valid Delphi identifier) and for the second, although in the second case it was unnecessary. If the two names match, you can omit the name directive, as I did for the third function. If you are not sure of the actual names of the functions exported by the DLL, you can use Borland's TDump command-line program, available in the Delphi BIN folder, using the -ee command-line switch.
Remember to add the stdcall directive to each definition, so that the caller module (the application) and the module being called (the DLL) use the same parameter-passing convention. If you fail to do so, you will get unpredictable values passed as parameters, a bug that is very hard to trace.
Note |
When you have to convert a large C/C++ header file to the corresponding Delphi declarations, instead of doing a manual conversion you can use a tool to partially automate the process. One of these tools is HeadConv, written by Bob Swart. You'll find a copy on his website, www.drbob42.com. The tool is being extended by Project Jedi, under the name of DARTH project (www.delphi-jedi.org/team_darth_home). Notice, though, that automatic header translation from C/C++ to Delphi is not possible; the Delphi language is more strongly typed than C/C++, so you have to use types more precisely. |
To use this C++ DLL, I've built a Delphi example named CallCpp. Its form has only the buttons used to call the functions of the DLL and some visual components for input and output parameters (see Figure 10.1). Notice that to run this application, you should have the DLL in the same directory as the project, in one of the directories on the path, or in the Windows main folder (Windows, WinNT…) or the Windows' system folder (WindowsSystem, WinNTSystem32…). If you move the executable file to a new directory and try to run it, you'll get a run-time error indicating that the DLL is missing:
Figure 10.1: The output of the CallCpp example when you have clicked each of the buttons
Creating a DLL in Delphi
Besides using DLLs written in other environments, you can use Delphi to build DLLs that can be used by Delphi programs or with any other development tool that supports DLLs. Building DLLs in Delphi is so easy that you might overuse this feature. In general, I suggest you try to build packages instead of plain DLLs. As I'll discuss later in this chapter, packages often contain components, but they can also include plain noncomponent classes, allowing you to write object-oriented code and to reuse it effectively. Of course, packages can contain also simple routines, constants, variables, etc.
As I've already mentioned, building a DLL is useful when a portion of a program's code is subject to frequent changes. In this case, you can often replace the DLL, keeping the rest of the program unchanged. Similarly, when you need to write a program that provides different features to different groups of users, you can distribute different versions of a DLL to those users.
Your First Delphi DLL
As a starting point in exploring the development of DLLs in Delphi, I'll show you a library built in Delphi. The primary focus of this example will be to show the syntax you use to define a DLL in Delphi, but it will also illustrate a few considerations involved in passing string parameters. To start, select the File ® New ® Other command and choose the DLL option in the New page of the Object Repository. Doing so creates a very simple source file that begins with the following definition:
library Project1;
The library statement indicates that you want to build a DLL instead of an executable file. Now you can add routines to the library and list them in an exports statement:
function Triple (N: Integer): Integer; stdcall; begin try Result := N * 3; except Result := -1; end; end; function Double (N: Integer): Integer; stdcall; begin try Result := N * 2; except Result := -1; end; end; exports Triple, Double;
In this basic version of the DLL, you don't need a uses statement; but in general, the main project file includes only the uses and exports statements, whereas the function declarations are placed in a separate unit. In the final source code of the FirstDll example, I've changed the code slightly from the version listed here, to show a message each time a function is called. You can accomplish this two ways; the simplest is to use the Dialogs unit and call the ShowMessage function.
The code requires Delphi to link a lot of VCL code into the application. If you statically link the VCL into this DLL, the resulting size will be a few hundred KB. The reason is that the ShowMessage function displays a VCL form that contains VCL controls and uses VCL graphics classes; those indirectly refer to things like the VCL streaming system and the VCL application and screen objects. In this case, a better alternative is to show the messages using direct API calls, using the Windows unit and calling the MessageBox function, so that the VCL code is not required. This code change brings the size of the application down to less than 50 KB.
Note |
This huge difference in size underlines the fact that you should not overuse DLLs in Delphi, to avoid compiling the VCL code in multiple executable files. Of course, you can reduce the size of a Delphi DLL by using run-time packages, as detailed later in this chapter. |
If you run a test program like the CallFrst example (described later) using the API-based version of the DLL, its behavior won't be correct. In fact, you can click the buttons that call the DLL functions several times without first closing the message boxes displayed by the DLL. This happens because the first parameter of the MessageBox API call is zero. Its value should instead be the handle of the program's main form or the application form— information you don't have at hand in the DLL.
Overloaded Functions in Delphi DLLs
When you create a DLL in C++, overloaded functions use name mangling to generate a different name for each function. The type of the parameters is included right in the name, as you saw in the CppDll example.
When you create a DLL in Delphi and use overloaded functions (that is, multiple functions using the same name and marked with the overload directive), Delphi allows you to export only one of the overloaded functions with the original name, indicating its parameters list in the exports clause. If you want to export multiple overloaded functions, you should specify different names in the exports clause to distinguish the overloads. This technique is demonstrated by this portion of the FirstDLL code:
function Triple (C: Char): Integer; stdcall; overload; function Triple (N: Integer): Integer; stdcall; overload; exports Triple (N: Integer), Triple (C: Char) name 'TripleChar';
Note |
The reverse is possible as well: You can import a series of similar functions from a DLL and define them all as overloaded functions in the Delphi declaration. Delphi's OpenGL.PAS unit contains a series of examples of this technique. |
Exporting Strings from a DLL
In general, functions in a DLL can use any type of parameter and return any type of value. There are two exceptions to this rule:
- If you plan to call the DLL from other programming languages, you should try using Windows native data types instead of Delphi-specific types. For example, to express color values, you should use integers or the Windows ColorRef type instead of the Delphi native TColor type, doing the appropriate conversions (as in the FormDLL example, described in the next section). For compatibility, you should avoid using some other Delphi types, including objects (which cannot be used by other languages) and Delphi strings (which can be replaced by PChar strings). In other words, every Windows development environment must support the basic types of the API, and if you stick to them, your DLL will be usable with other development environments. Also, Delphi file variables (text files and binary file of record) should not be passed out of DLLs, but you can use Win32 file handles.
- Even if you plan to use the DLL only from a Delphi application, you cannot pass Delphi strings (and dynamic arrays) across the DLL boundary without taking some precautions. This is the case because of the way Delphi manages strings in memory—allocating, reallocating, and freeing them automatically. The solution to the problem is to include the ShareMem system unit both in the DLL and in the program using it. This unit must be included as the first unit of each of the projects. Moreover, you have to deploy the BorlndMM.DLL file (the name stands for Borland Memory Manager) along with the program and the specific library.
In the FirstDLL example, I've included both approaches: One function receives and returns a Delphi string, and another receives as parameter a PChar pointer, which is then filled by the function. The first function is written as usual in Delphi:
function DoubleString (S: string; Separator: Char): string; stdcall; begin try Result := S + Separator + S; except Result := '[error]'; end; end;
The second function is quite complex because PChar strings don't have a simple + operator, and they are not directly compatible with characters; the separator must be turned into a string before being adding. Here is the complete code; it uses input and output PChar buffers, which are compatible with any Windows development environment:
function DoublePChar (BufferIn, BufferOut: PChar; BufferOutLen: Cardinal; Separator: Char): LongBool; stdcall; var SepStr: array [0..1] of Char; begin try // if the buffer is large enough if BufferOutLen > StrLen (BufferIn) * 2 + 2 then begin // copy the input buffer in the output buffer StrCopy (BufferOut, BufferIn); // build the separator string (value plus null terminator) SepStr [0] := Separator; SepStr [1] := #0; // append the separator StrCat (BufferOut, SepStr); // append the input buffer once more StrCat (BufferOut, BufferIn); Result := True; end else // not enough space Result := False; except Result := False; end; end;
This second version of the code is certainly more complex, but the first can be used only from Delphi. Moreover, the first version requires you to include the ShareMem unit and to deploy the file BorlndMM.DLL, as discussed earlier.
Calling the Delphi DLL
How can you use the library you've just built? You can call it from within another Delphi project or from other environments. As an example, I've built the CallFrst project (stored in the FirstDLL directory). To access the DLL functions, you must declare them as external, as with the C++ DLL. This time, however, you can copy and paste the definition of the functions from the source code of the Delphi DLL, adding the external clause, as follows:
function Double (N: Integer): Integer; stdcall; external 'FIRSTDLL.DLL';
This declaration is similar to those used to call the C++ DLL. This time, however, you have no problems with function names. Once they are redeclared as external, the functions of the DLL can be used as if they were local functions. Here are two examples, with calls to the string-related functions (an example of the output is visible in Figure 10.2):
Figure 10.2: The output of the CallFrst example, which calls the DLL you've built in Delphi
procedure TForm1.BtnDoubleStringClick(Sender: TObject); begin // call the DLL function directly EditDouble.Text := DoubleString (EditSource.Text, ';'); end; procedure TForm1.BtnDoublePCharClick(Sender: TObject); var Buffer: string; begin // make the buffer large enough SetLength (Buffer, 1000); // call the DLL function if DoublePChar (PChar (EditSource.Text), PChar (Buffer), 1000, '/') then EditDouble.Text := Buffer; end;
Advanced Features of Delphi DLLs
Beside this introductory example, you can do a few extra things with dynamic libraries in Delphi. You can use some new compiler directives to affect the name of the library, you can call a DLL at run time, and you can place entire Delphi forms inside a dynamic library. These are the topics of the following sections.
Changing Project and Library Names
For a library, as for a standard application, you end up with a library name matching a Delphi project filename. Following a technique similar to that introduced in Kylix for compatibility with standard Linux naming conventions for shared object libraries (the Linux equivalent of Windows DLLs), Delphi 6 introduced special compiler directives you can use in libraries to determine their executable filename. Some of these directives make more sense in the Linux world than on Windows, but they've all been added anyway:
- $LIBPREFIX is used to add something in front of the library name. Paralleling the Linux technique of adding lib in front of library names, this directive is used by Kylix to add bpl at the beginning of package names. It is necessary because Linux uses a single extension (.SO) for libraries, whereas in Windows you can have different library extensions, something Borland uses for packages (.BPL).
- $LIBSUFFIX is used to add text after the library name and before the extension. This text can be used to specify versioning information or other variations on the library name and can be quite useful on Windows.
- $LIBVERSION is used to add a version number after the extension—something very common in Linux, but that you should generally avoid on Windows.
These directives can be set in the IDE from the Application page of the Project Options dialog box, as you can see in Figure 10.3. As an example, consider the following directives, which generate a library called MarcoNameTest60.dll:
library NameTest; {$LIBPREFIX 'Marco'} {$LIBSUFFIX '60'}
Figure 10.3: The Application page of the Project Options dialog box now has a Library Name section.
Note |
Delphi 6 packages introduced the extensive use of the $LIBSUFFIX directive. For this reason, the VCL package now generates the VCL.DCP file and the VCL70.BPL file. The advantage of this approach is that you won't need to change the requires portions of your packages for every new version of Delphi. Of course, this is helpful when you move projects from Delphi 6 to Delphi 7, because past versions of Delphi didn't provide this feature. When you reopen Delphi 5 packages you still have to upgrade their source code, an operation the Delphi IDE does automatically for you. |
Calling a DLL Function at Run Time
Up to now, you've referenced in your code the functions exported by the libraries, so the DLLs were loaded along with the program. I mentioned earlier that you can also delay the loading of a DLL until the moment it is needed, so you can use the rest of the program in case the DLL is not available.
Dynamic loading of a DLL in Windows is accomplished by calling the LoadLibrary API function, which searches for the DLL in the program folder, in the folders on the path, and in some system folders. If the DLL is not found, Windows will show an error message, something you can skip by calling Delphi's SafeLoadLibrary function. This function has the same effect as the API it encapsulates, but it suppresses the standard Windows error message and should be the preferred way to load libraries dynamically in Delphi.
If the library is found and loaded (something you know by checking the return value of LoadLibrary or SafeLoadLibrary), a program can call the GetProcAddress API function, which searches the DLL's exports table, looking for the name of the function passed as a parameter. If GetProcAddress finds a match, it returns a pointer to the requested procedure. Now you can cast this function pointer to the proper data type and call it.
Whichever loading functions you've used, don't forget to call FreeLibrary at the end, so that the DLL can be properly released from memory. In fact, the system uses a reference-counting technique for libraries, releasing them when each loading request has been followed by a freeing request.
The example I've built to show dynamic DLL loading is named DynaCall. It uses the FirstDLL library built earlier in this chapter (to make the program work, you have to copy the DLL from its source folder into the folder as the DynaCall example). Instead of declaring the Double and Triple functions and using them directly, this example obtains the same effect with somewhat more complex code. The advantage, however, is that the program will run even without the DLL. Also, if new compatible functions are added to the DLL, you won't have to revise the program's source code and recompile it to access those new functions. Here is the core code of the program:
type TIntFunction = function (I: Integer): Integer; stdcall; const DllName = 'Firstdll.dll'; procedure TForm1.Button1Click(Sender: TObject); var HInst: THandle; FPointer: TFarProc; MyFunct: TIntFunction; begin HInst := SafeLoadLibrary (DllName); if HInst > 0 then try FPointer := GetProcAddress (HInst, PChar (Edit1.Text)); if FPointer <> nil then begin MyFunct := TIntFunction (FPointer); SpinEdit1.Value := MyFunct (SpinEdit1.Value); end else ShowMessage (Edit1.Text + ' DLL function not found'); finally FreeLibrary (HInst); end else ShowMessage (DllName + ' library not found'); end;
Warning |
As the library uses the Borland memory manager, the program dynamically loading it must do the same. So you need to add the ShareMem unit in the project of the DynaCall example. Oddly enough, this was not so with past versions of Delphi, in case the library didn't effectively use strings. Be warned that if you omit this inclusion, you'll get a harsh system error, which can even stall the debugger on the FreeLIrbary call. |
How do you call a procedure in Delphi, once you have a pointer to it? One solution is to convert the pointer to a procedural type and then call the procedure using the procedural-type variable, as in the previous listing. Notice that the procedural type you define must be compatible with the definition of the procedure in the DLL. This is the Achilles' heel of this method—there is no actual check of the parameter types.
What is the advantage of this approach? In theory, you can use it to access any function of any DLL at any time. In practice, it is useful when you have different DLLs with compatible functions or a single DLL with several compatible functions, as in this case. You can call the Double and Triple methods by entering their names in the edit box. Now, if someone gives you a DLL with a new function receiving an integer as a parameter and returning an integer, you can call it by entering its name in the edit box. You don't even need to recompile the application.
With this code, the compiler and the linker ignore the existence of the DLL. When the program is loaded, the DLL is not loaded immediately. You might make the program even more flexible and let the user enter the name of the DLL to use. In some cases, this is a great advantage. A program may switch DLLs at run time, something the direct approach does not allow. Note that this approach to loading DLL functions is common in macro languages and is used by many visual programming environments.
Only a system based on a compiler and a linker, such as Delphi, can use the direct approach, which is generally more reliable and also a little faster. In my opinion, the indirect loading approach of the DynaCall example is useful only in special cases, but it can be extremely powerful. On the other hand, I see a lot of value in using dynamic loading for packages including forms, as you'll see toward the end of this chapter.
Placing Delphi Forms in a Library
Besides writing a library with functions and procedures, you can place a complete form built with Delphi into a dynamic library. This can be a dialog box or any other kind of form, and it can be used not only by other Delphi programs, but also by other development environments or macro languages with the ability to use dynamic link libraries. Once you've created a new library project, all you need to do is add one or more forms to the project and then write exported functions that will create and use those forms.
For example, a function activating a modal dialog box to select a color could be written like this:
function GetColor (Col: LongInt): LongInt; cdecl; var FormScroll: TFormScroll; begin // default value Result := Col; try FormScroll := TFormScroll.Create (Application); try // initialize the data FormScroll.SelectedColor := Col; // show the form if FormScroll.ShowModal = mrOK then Result := FormScroll.SelectedColor; finally FormScroll.Free; end; except on E: Exception do MessageDlg ('Error in library: ' + E.Message, mtError, [mbOK], 0); end; end;
What makes this different from the code you generally write in a program is the use of exception handling:
- A try/except block protects the whole function. Any exception generated by the function will be trapped, and an appropriate message will be displayed. You handle every possible exception because the calling application might be written in any language—in particular, one that doesn't know how to handle exceptions. Even when the caller is a Delphi program, it is sometimes helpful to use the same protective approach.
- A try/finally block protects the operations on the form, ensuring that the form object will be properly destroyed even when an exception is raised.
By checking the return value of the ShowModal method, the program determines the result of the function. I've set the default value before entering the try block to ensure that it will always be executed (and also to avoid the compiler warning indicating that the result of the function might be undefined).
You can find this code snippet in the FormDLL and UseCol projects, available in the FormDLL folder. (There's also a WORDCALL.TXT file showing how to call the routine from a Word macro.). The example also shows that you can add a modeless form to the DLL, but doing so causes far too much trouble. The modeless form and the main form are not synchronized, because the DLL has its own global Application object in its own copy of the VCL. This situation can be partially fixed by copying the Handle of the application's Application object to the Handle of the library's Application object. Not all of the problems are solved with the code that you can find in the example. A better solution might be to compile the program and the library to use Delphi packages, so that the VCL code and data won't be duplicated. But this approach still causes a few troubles: it's generally advised that you don't use Delphi DLLs and packages together. So what is the best suggestion I can give you? For making the forms of a library available to other Delphi programs, use packages instead of plain DLLs!
Libraries in Memory Code and Data
Before I discuss packages, I want to focus on a technical element of dynamic libraries: how they use memory. Let's start with the code portion of the library, then we'll focus on its global data. When Windows loads the code of a library, like any other code module, it has to do a fixup operation. This fixup consists of patching addresses of jumps and internal function calls with the actual memory address where they've been loaded. The effect of this operation is that the code-loaded memory depends on where it has been loaded.
This is not an issue for executable files, but might cause a significant problem for libraries. If two executables load the same library at the same base address, there will be only one physical copy of the DLL code in the RAM (the physical memory) of the machine, thus saving memory space. If the second time the library is loaded the memory address is already in use, it needs to be relocated, that is, moved with a different fixup applied. So you'll end up with a second physical copy of the DLL code in RAM.
You can use the dynamic loading technique, based on the GetProcAddress API function, to test which memory address of the current process a function has been mapped to. The code is as follows:
procedure TForm1.Button3Click(Sender: TObject); var HDLLInst: THandle; begin HDLLInst := SafeLoadLibrary ('dllmem'); Label1.Caption := Format ('Address: %p', [ GetProcAddress (HDLLInst, 'SetData')]); FreeLibrary (HDLLInst); end;
This code displays, in a label, the memory address of the function, within the address space of the calling application. If you run two programs using this code, they'll generally both show the same address. This technique demonstrates that the code is loaded only once at a common memory address.
Another technique to get more information about what's going on is to use Delphi's Modules window, which shows the base address of each library referenced by the module and the address of each function within the library, as shown here:
It's important to know that the base address of a DLL is something you can request by setting the base address option. In Delphi this address is determined by the Image Base value in the linker page of the Project Options dialog box. In the DllMem library, for example, I've set it to $00800000. You need to have a different value for each of your libraries, verifying that it doesn't clash with any system library or other library (package, ActiveX, and so on) used by the executable. Again, this is something you can figure out using the Module window of the debugger.
Although this doesn't guarantee a unique placement, setting a base address for the library is always better than not setting one; in this case a relocation always takes place, but the chance that two different executables will relocate the same library at the same address are not high.
Note |
You can also use Process Explorer from http://www.sysinternals.com to examine any process on any machine. This tool even has an option to highlight relocated DLLs. Check the effect of running the same program with its libraries on different operating systems (Windows 2000, Windows XP, and Windows ME) and settle on an unused area. |
This is the case for the DLL code, but what about the global data? Basically, each copy of the DLL has its own copy of the data, in the address space of the calling application. However, it is possible to share global data between applications using a DLL. The most common technique for sharing data is to use memory-mapped files. I'll use this technique for a DLL, but it can also be used to share data directly among applications.
This example is called DllMem for the library and UseMem for the demo application. The DLL code has a project file that exports four subroutines:
library dllmem; uses SysUtils, DllMemU in 'DllMemU.pas'; exports SetData, GetData, GetShareData, SetShareData; end.
The actual code is in the secondary unit (DllMemU.PAS), which contains the code for the four routines that read or write two global memory locations. These memory locations hold an integer and a pointer to an integer. Here are the variable declarations and the two Set routines:
var PlainData: Integer = 0; // not shared ShareData: ^Integer; // shared procedure SetData (I: Integer); stdcall; begin PlainData := I; end; procedure SetShareData (I: Integer); stdcall; begin ShareData^ := I; end;
Sharing Data with Memory Mapped Files
For the data that isn't shared, there isn't anything else to do. To access the shared data, however, the DLL has to create a memory-mapped file and then get a pointer to this memory area. These operations require two Windows API calls:
- CreateFileMapping requires as parameters the filename (or $FFFFFFFF to use a virtual file in memory), some security and protection attributes, the size of the data, and an internal name (which must be the same to share the mapped file from multiple calling applications).
- MapViewOfFile requires as parameters the handle of the memory-mapped file, some attributes and offsets, and the size of the data (again).
Here is the source code of the initialization section, which is executed every time the DLL is loaded into a new process space (that is, once for each application that uses the DLL):
var hMapFile: THandle; const VirtualFileName = 'ShareDllData'; DataSize = sizeof (Integer); initialization // create memory mapped file hMapFile := CreateFileMapping ($FFFFFFFF, nil, Page_ReadWrite, 0, DataSize, VirtualFileName); if hMapFile = 0 then raise Exception.Create ('Error creating memory-mapped file'); // get the pointer to the actual data ShareData := MapViewOfFile ( hMapFile, File_Map_Write, 0, 0, DataSize);
When the application terminates and the DLL is released, it has to free the pointer to the mapped file and the file mapping:
finalization UnmapViewOfFile (ShareData); CloseHandle (hMapFile);
The UseMem demo program's form has four edit boxes (two with an UpDown control connected), five buttons, and a label. The first button saves the value of the first edit box in the DLL data, getting the value from the connected UpDown control:
SetData (UpDown1.Position);
If you click the second button, the program copies the DLL data to the second edit box:
Edit2.Text := IntToStr(GetData);
The third button is used to display the memory address of a function, with the source code shown at the beginning of this section. The last two buttons have basically the same code as the first two, but they call the SetShareData procedure and the GetShareData function.
If you run two copies of this program, you can see that each copy has its own value for the plain global data of the DLL, whereas the value of the shared data is common. Set different values in the two programs and then get them in both, and you'll see what I mean. This situation is illustrated in Figure 10.4.
Figure 10.4: If you run two copies of the UseMem program, you'll see that the global data in its DLL is not shared.
Warning |
Memory-mapped files reserve a minimum of a 64 KB range of virtual addresses and consume physical memory in 4 KB pages. The example's use of 4-byte Integer data in shared memory is rather expensive, especially if you use the same approach for sharing multiple values. If you need to share several variables, you should place them all in a single shared memory area (accessing the different variables using pointers or building a record structure for all of them). |
Using Delphi Packages
In Delphi, component packages are an important type of DLL. Packages allow you to bundle a group of components and then link the components either statically (adding their compiled code to the executable file of your application) or dynamically (keeping the component code in a DLL, the run-time package that you'll distribute along with your program, along with all other packages you will need). In Chapter 9, "Writing Delphi Components," you saw how to build a package. Now I want to underline some advantages and disadvantages of the two forms of linking for a package. You need to keep many elements in mind:
- Using a package as a DLL makes the executable files much smaller.
- Linking the package units into the program allows you to distribute only part of the package code. The size of the executable file of an application plus the size of the required package DLLs is always much bigger than the size of the statically linked program. The linker includes only the code used by the program, whereas a package must link in all the functions and classes declared in the interface sections of all the units contained in the package.
- If you distribute several Delphi applications based on the same packages, you might end up distributing less code, because the run-time packages are shared. In other words, once the users of your application have the standard Delphi run-time packages, you can ship them very small programs.
- If you run several Delphi applications based on the same packages, you can save some memory space at run time; the code of the run-time packages is loaded in memory only once among the multiple Delphi applications.
- Don't worry too much about distributing a large executable file. Keep in mind that when you make minor changes to a program, you can use any of various tools to create a patch file, so that you distribute only a file containing the differences, not a complete copy of the files.
- If you place a few of your program's forms in a run-time package, you can share them among programs. When you modify these forms, however, you'll generally need to recompile the main program as well, and distribute both of them again to your users. The next section discusses this complex topic in detail.
- A package is a collection of compiled units (including classes, types, variables, routines), which don't differ at all from the units inside a program. The only difference is in the build process. The code of the package units and that of the units of the main program using them remains identical. This is arguably one of the key advantages of packages over DLLs.
Package Versioning
A very important and often misunderstood element is the distribution of updated packages. When you update a DLL, you can ship the new version, and the executable programs requiring this DLL will still work (unless you've removed existing exported functions or changed some of their parameters).
When you distribute a Delphi package, however, if you update the package and modify the interface portion of any unit of the package, you may need to recompile all the applications that use the package. This step is required if you add methods or properties to a class, but not if you add new global symbols (or modify anything not used by client applications). There is no problem if you make changes affecting only the implementation section of the package's units.
A DCU file in Delphi has a version tag based on its timestamp and a checksum computed from the interface portion of the unit. When you change the interface portion of a unit, every other unit based on it should be recompiled. The compiler compares the timestamp and checksum of the unit from previous compilations with the new timestamp and checksum, and decides whether the dependent unit must be recompiled. For this reason, you must recompile each unit when you get a new version of Delphi that has modified system units.
In Delphi 3 (when packages were first introduced), the compiler added an extra entry function to the package library named with a checksum of the package, obtained from the checksum of the units it contained and the checksum of the packages it required. This checksum function was then called by programs using the package so that an older executable would fail at startup.
Delphi 4 and following versions up to Delphi 7 have relaxed the run-time constraints of the package. (The design-time constraints on DCU files remain identical, though.) The checksum of the packages is no longer checked, so you can directly modify the units that are part of a package and deploy a new version of the package to be used with the existing executable file. Because methods are referenced by name, you cannot remove any existing method. You cannot even change its parameters, because of name-mangling techniques that protect a package's method against changes in parameters.
Removing a method referenced from the calling program will stop the program during the loading process. If you make other changes, however, the program might fail unexpectedly during its execution. For example, if you replace a component placed on a form compiled in a package with a similar component, the calling program might still be able to access the component in that memory location, although it is now different!
If you decide to follow this treacherous road of changing the interface of units in a package without recompiling all the programs that use it, you should at least limit your changes. When you add new properties or nonvirtual methods to the form, you should be able to maintain full compatibility with existing programs already using the package. Also, adding fields and virtual methods might affect the internal structure of the class, leading to problems with existing programs that expect a different class data and virtual method table (VMT) layout.
Warning |
Here I'm referring to the distribution of compiled programs divided between EXEs and packages, not to the distribution of components to other Delphi developers. In this latter case the versioning rules are more stringent, and you must take extra care in package versioning. |
Having said this, I recommend never changing the interface of any unit exported by your packages. To accomplish this, you can add to your package a unit with form-creation functions (as in the DLL with forms presented earlier) and use it to access another unit, which defines the form. Although there is no way to hide a unit that is linked into a package, if you never directly use the class defined in a unit, but use it only through other routines, you'll have more flexibility in modifying it. You can also use form inheritance to modify a form within a package without affecting the original version.
The most stringent rule for packages is the following one used by component writers: For long-term deployment and maintenance of code in packages, plan on having a major release with minor maintenance releases. A major release of your package will require all client programs to be recompiled from source; the package file should be renamed with a new version number, and the interface sections of units can be modified. Maintenance releases of that package should be restricted to implementation changes to preserve full compatibility with existing executables and units, as is generally done by Borland with its Update Packs.
Forms Inside Packages
In Chapter 9, I discussed the use of component packages in Delphi applications. Now I'm discussing the use of packages and DLLs for partitioning an application, so I'll begin talking about the development of packages holding forms. I've mentioned earlier in this chapter that you can use forms inside DLLs, but doing so causes quite a few problems. If you are building both the library and the executable file in Delphi, using packages results in a much better and cleaner solution.
At first sight, you might believe that Delphi packages are solely a way to distribute components to be installed in the environment. However, you can also use packages as a way to structure your code but, unlike DLLs, retain the full power of Delphi's OOP. Consider this: A package is a collection of compiled units, and your program uses several units. The units the program refers to will be compiled inside the executable file, unless you ask Delphi to place them inside a package. As discussed earlier, this is one of the main reasons for using packages.
To set up an application so that its code is split among one or more packages and a main executable file, you only need to compile some of the units in a package and then set up the options of the main program to dynamically link this package. For example, I made a copy of the "usual" color selection form and renamed its unit PackScrollF; then I created a new package and added the unit to it, as you can see in Figure 10.5.
Figure 10.5: The structure of the package hosting a form in Delphi's Package Editor
Before compiling this package, you should change its default output directories to refer to the current folder, not the standard /Projects/Bpl subfolder of Delphi. To do this, go to the Directories/Conditional page of the package Project Options, and set the current directory (a single dot, for short) for the Output directory (for the BPL) and DCP output directory. Then compile the package and do not install it in Delphi—there's no need to.
At this point, you can create a normal application and write the standard code you'll use in a program to show a secondary form, as in the following listing:
uses PackScrollF; procedure TForm1.BtnChangeClick(Sender: TObject); var FormScroll: TFormScroll; begin FormScroll := TFormScroll.Create (Application); try // initialize the data FormScroll.SelectedColor := Color; // show the form if FormScroll.ShowModal = mrOK then Color := FormScroll.SelectedColor; finally FormScroll.Free; end; end; procedure TForm1.BtnSelectClick(Sender: TObject); var FormScroll: TFormScroll; begin FormScroll := TFormScroll.Create (Application); // initialize the data and UI FormScroll.SelectedColor := Color; FormScroll.BitBtn1.Caption := 'Apply'; FormScroll.BitBtn1.OnClick := FormScroll.ApplyClick; FormScroll.BitBtn2.Kind := bkClose; // show the form FormScroll.Show; end;
One of the advantages of this approach is that you can refer to a form compiled into a package with the same code you'd use for a form compiled in the program. If you compile this program, the unit of the form will be bound to the program. To keep the form's unit in the package, you'll have to use run-time packages for the application and manually add the PackWithForm package to the list of run-time packages (this is not suggested by the Delphi IDE, because you have not installed the package in the development environment).
Once you've performed this step, compile the program; it will behave as usual. But now the form is in a DLL package, and you can modify the form in the package, recompile it, and run the application to see the effects. Notice, though, that for most changes affecting the interface portion of the package's units (for example, adding a component or a method to the form), you should also recompile the executable program calling the package.
Note |
You can find the package and the program testing it in the PackForm folder of the source code related to the current chapter. The code for the next example is in the same folder. The package and projects are all referenced by the project group (BPG) file within the folder. |
Loading Packages at Run Time
In the previous example, I indicated that the PackWithForm package is a run-time package to be used by the application. This means the package is required to run the application and is loaded when the program starts, just as with the typical use of DLLs. You can avoid both aspects by loading the package dynamically, as you've done with DLLs. The resulting program will be more flexible, start more quickly, and use less memory.
An important element to keep in mind is that you'll need to call the LoadPackage and UnloadPackage Delphi functions rather than the LoadLibrary/SafeLoadLibrary and FreeLibrary Windows API functions. The functions provided by Delphi load the packages, but they also call their proper initialization and finalization code.
Besides this important element—which is easy to accomplish once you know about it—the program will require some extra code, because you cannot refer from the main program to the unit hosting the form. You cannot use the form class directly, nor access its properties or components—at least, not with the standard Delphi code. Both issues, however, can be solved using class references, class registration, and RTTI (run-time type information).
Let me begin with the first approach. In the form unit, in the package, I've added this initialization code:
initialization RegisterClass (TFormScroll);
As the package is loaded, the main program can use Delphi's GetClass function to get the class reference of the registered class and then call the Create constructor for this class reference.
To solve the second problem, I've made the SelectedColor property of the form in the package a published property, so that it is accessible via RTTI. Then I've replaced the code accessing this property (FormScroll.Color) with the following:
SetPropValue (FormScroll, 'SelectedColor', Color);
Summing up all these changes, here is the code used by the main program (the DynaPackForm application) to show the modal form from the dynamically loaded package:
procedure TForm1.BtnChangeClick(Sender: TObject); var FormScroll: TForm; FormClass: TFormClass; HandlePack: HModule; begin // try to load the package HandlePack := LoadPackage ('PackWithForm.bpl'); if HandlePack > 0 then begin FormClass := TFormClass(GetClass ('TFormScroll')); if Assigned (FormClass) then begin FormScroll := FormClass.Create (Application); try // initialize the data SetPropValue (FormScroll, 'SelectedColor', Color); // show the form if FormScroll.ShowModal = mrOK then Color := GetPropValue (FormScroll, 'SelectedColor'); finally FormScroll.Free; end; end else ShowMessage ('Form class not found'); UnloadPackage (HandlePack); end else ShowMessage ('Package not found'); end;
Notice that the program unloads the package as soon as it is done with it. This step is not compulsory. I could have moved the UnloadPackage call in the OnDestroy handler of the form, and avoided reloading the package after the first time.
Now you can try running this program without the package available. You'll see that it starts properly, only to complain that it cannot find the package as you click the Change button. In this program, you don't need to use run-time packages to keep the unit outside your executable file, because you are not referring to the unit in your code. Also, the PackWithForm package doesn't need to be listed in the run-time packages. However, you must use run-time packages for it to work at all, or else your program will include VCL global variables (such as the Application object) and the dynamically loaded package will include another version, because it will refer to the VCL packages anyway.
Warning |
When a program that loads a package dynamically is closed, you may experience access violations. Frequently, they occur because an object whose class is defined in the package is kept in memory even after the package is unloaded. When the program shuts down, it may try to free that object by calling the Destroy method of a non-existing VMT, and thus cause the error. Having said this, I know by experience that these types of errors are very difficult to track and fix. I suggest that you make sure to destroy all the objects before unloading the package. |
Using Interfaces in Packages
Accessing forms' classes by means of methods and properties is much simpler than using RTTI all over the place. To build a larger application, I definitely try to use interfaces and to have multiple forms, each implementing a few standard interfaces defined by the program. An example cannot really do justice to this type of architecture, which becomes relevant for a large program, but I've tried to build a program to show how this idea can be applied in practice.
Note |
If you don't know much about interfaces, I suggest you refer to the related portion of Chapter 2, "The Delphi Programming Language," before reading this section. |
To build the IntfPack project, I've used three packages plus a demo application. Two of the three packages (IntfFormPack and IntfFormPack2) define alternative forms used to select a color. The third package (IntfPack) hosts a shared unit, used by both other packages. This unit includes the definition of the interface. I couldn't add it to both other packages because you cannot load two packages that have the same name with a unit (even by run-time loading).
The IntfPack package's only file is the IntfColSel unit, displayed in Listing 10.1. This unit defines the common interface (you'll probably have a number of them in real-world applications) plus a list of registered classes; it mimics Delphi's RegisterClass approach, but makes available the complete list so that you can easily scan it.
Listing 10.1: The IntfColSel Unit of the IntfPack Package
unit IntfColSel; interface uses Graphics, Contnrs; type IColorSelect = interface ['{3F961395-71F6-4822-BD02-3B475FF516D4}'] function Display (Modal: Boolean = True): Boolean; procedure SetSelColor (Col: TColor); function GetSelColor: TColor; property SelColor: TColor read GetSelColor write SetSelColor; end; procedure RegisterColorSelect (AClass: TClass); var ClassesColorSelect: TClassList; implementation procedure RegisterColorSelect (AClass: TClass); begin if ClassesColorSelect.IndexOf (AClass) < 0 then ClassesColorSelect.Add (AClass); end; initialization ClassesColorSelect := TClassList.Create; finalization ClassesColorSelect.Free; end.
Once you have this interface available, you can define forms that implement it, as in the following example taken from IntfFormPack:
type TFormSimpleColor = class(TForm, IColorSelect) ... private procedure SetSelColor (Col: TColor); function GetSelColor: TColor; public function Display (Modal: Boolean = True): Boolean;
The two access methods read and write the value of the color from some components of the form (a ColorGrid control in this case), whereas the Display method internally calls either Show or ShowModal, depending on the parameter:
function TFormSimpleColor.Display(Modal: Boolean): Boolean; begin Result := True; // default if Modal then Result := (ShowModal = mrOK) else begin BitBtn1.Caption := 'Apply'; BitBtn1.OnClick := ApplyClick; BitBtn2.Kind := bkClose; Show; end; end;
As you can see from this code, when the form is modeless the OK button is turned into an Apply button. Finally, the unit has the registration code in the initialization section, so that it is executed when the package is dynamically loaded:
RegisterColorSelect (TFormSimpleColor);
The second package, IntfFormPack2, has a similar architecture but a different form. You can look it up in the source code (I've not discussed the second form here as its code doesn't add much to the structure of the example).
With this architecture in place, you can build a rather elegant and flexible main program, which is based on a single form. When the form is created, it defines a list of packages (HandlesPackages) and loads them all. I've hard-coded the package in the code of the example, but of course you can search for the packages of the current folder or use a configuration file to make the application structure more flexible. After loading the packages, the program shows the registered classes in a list box. This is the code of the LoadDynaPackage and FormCreate methods:
procedure TFormUseIntf.FormCreate(Sender: TObject); var I: Integer; begin // loads all runtime packages HandlesPackages := TList.Create; LoadDynaPackage ('IntfFormPack.bpl'); LoadDynaPackage ('IntfFormPack2.bpl'); // add class names and select the first for I := 0 to ClassesColorSelect.Count - 1 do lbClasses.Items.Add (ClassesColorSelect [I].ClassName); lbClasses.ItemIndex := 0; end; procedure TFormUseIntf.LoadDynaPackage(PackageName: string); var Handle: HModule; begin // try to load the package Handle := LoadPackage (PackageName); if Handle > 0 then // add to the list for later removal HandlesPackages.Add (Pointer(Handle)) else ShowMessage ('Package ' + PackageName + ' not found'); end;
The main reason for keeping the list of package handles is to be able to unload them all when the program ends. You don't need these handles to access the forms defined in those packages; the run-time code used to create and show a form uses the corresponding component classes. This is a snippet of code used to display a modeless form (an option controlled by a check box):
var AComponent: TComponent; ColorSelect: IColorSelect; begin AComponent := TComponentClass (ClassesColorSelect[LbClasses.ItemIndex]).Create (Application); ColorSelect := AComponent as IColorSelect; ColorSelect.SelColor := Color; ColorSelect.Display (False);
The program uses the Supports function to check that the form really does support the interface before using it, and also accounts for the modal version of the form; but its essence is properly depicted in the preceding four statements.
By the way, notice that the code doesn't require a form. A nice exercise would be to add to the architecture a package with a component encapsulating the color selection dialog box or inheriting from it.
Warning |
The main program refers to the unit hosting the interface definition but should not link this file in. Rather, it should use the run-time package containing this unit, as the dynamically loaded packages do. Otherwise the main program will use a different copy of the same code, including a different list of global classes. It is this list of global classes that should not be duplicated in memory. |
The Structure of a Package
You may wonder whether it is possible to know if a unit has been linked in the executable file or if it's part of a run-time package. Not only is this possible in Delphi, but you can also explore the overall structure of an application. A component can use the undocumented ModuleIsPackage global variable, declared in the SysInit unit. You should never need this variable, but it is technically possible for a component to have different code depending on whether it is packaged. The following code extracts the name of the run-time package hosting the component, if any:
var fPackName: string; begin // get package name SetLength (fPackName, 100); if ModuleIsPackage then begin GetModuleFileName (HInstance, PChar (fPackName), Length (fPackName)); fPackName := PChar (fPackName) // string length fixup end else fPackName := 'Not packaged';
Besides accessing package information from within a component (as in the previous code), you can also do so from a special entry point of the package libraries, the GetPackageInfoTable function. This function returns some specific package information that Delphi stores as resources and includes in the package DLL. Fortunately, you don't need to use low-level techniques to access this information, because Delphi provides some high-level functions to manipulate it.
You can use two functions to access package information:
- GetPackageDescription returns a string that contains a description of the package. To call this function, you must supply the name of the module (the package library) as the only parameter.
- GetPackageInfo doesn't directly return information about the package. Instead, you pass it a function that it calls for every entry in the package's internal data structure. In practice, GetPackageInfo will call your function for every one of the package's contained units and required packages. In addition, GetPackageInfo sets several flags in an Integer variable.
These two function calls allow you to access internal information about a package, but how do you know which packages your application is using? You could determine this information by looking at an executable file using low-level functions, but Delphi helps you again by supplying a simpler approach. The EnumModules function doesn't directly return information about an application's modules; but it lets you pass it a function, which it calls for each module of the application, for the main executable file, and for each of the packages the application relies on.
To demonstrate this approach, I've built a program that displays the module and package information in a TreeView component. Each first-level node corresponds to a module; within each module I've built a subtree that displays the contained and required packages for that module, as well as the package description and compiler flags (RunOnly and DesignOnly). You can see the output of this example in Figure 10.6.
Figure 10.6: The output of the PackInfo example, with details of the packages it uses
In addition to the TreeView component, I've added several other components to the main form but hidden them from view: a DBEdit, a Chart, and a FilterComboBox. I added these components simply to include more run-time packages in the application, beyond the ubiquitous Vcl and Rtl packages. The only method of the form class is FormCreate, which calls the module enumeration function:
procedure TForm1.FormCreate(Sender: TObject); begin EnumModules(ForEachModule, nil); end;
The EnumModules function accepts two parameters: the callback function (in this case, ForEachModule) and a pointer to a data structure that the callback function will use (in this case, nil, because you don't need this). The callback function must accept two parameters— an HInstance value and an untyped pointer—and must return a Boolean value. The EnumModules function will, in turn, call your callback function for each module, passing the instance handle of each module as the first parameter and the data structure pointer (nil in this example) as the second:
function ForEachModule (HInstance: Longint; Data: Pointer): Boolean; var Flags: Integer; ModuleName, ModuleDesc: string; ModuleNode: TTreeNode; begin with Form1.TreeView1.Items do begin SetLength (ModuleName, 200); GetModuleFileName (HInstance, PChar (ModuleName), Length (ModuleName)); ModuleName := PChar (ModuleName); // fixup ModuleNode := Add (nil, ModuleName); // get description and add fixed nodes ModuleDesc := GetPackageDescription (PChar (ModuleName)); ContNode := AddChild (ModuleNode, 'Contains'); ReqNode := AddChild (ModuleNode, 'Requires'); // add information if the module is a package GetPackageInfo (HInstance, nil, Flags, ShowInfoProc); if ModuleDesc <> '' then begin AddChild (ModuleNode, 'Description: ' + ModuleDesc); if Flags and pfDesignOnly = pfDesignOnly then AddChild (ModuleNode, 'Design Only'); if Flags and pfRunOnly = pfRunOnly then AddChild (ModuleNode, 'Run Only'); end; end; Result := True; end;
As you can see in the preceding code, the ForEachModule function begins by adding the module name as the main node of the tree (by calling the Add method of the TreeView1.Items object and passing nil as the first parameter). It then adds two fixed child nodes, which are stored in the ContNode and ReqNode variables declared in the implementation section of this unit.
Next, the program calls the GetPackageInfo function and passes it another callback function, ShowInfoProc, which I'll discuss shortly, to provide a list of the application's or package's units. At the end of the ForEachModule function, if the module is a package the program adds more information, such as its description and compiler flags (the program knows it's a package if its description isn't an empty string).
Earlier, I mentioned passing another callback function (the ShowInfoProc procedure) to the GetPackageInfo function, which in turn calls the callback function for each contained or required package of a module. This procedure creates a string that describes the package and its main flags (added within parentheses), and then inserts that string under one of the two nodes (ContNode and ReqNode), depending on the type of the module. You can determine the module type by examining the NameType parameter. Here is the complete code for the second callback function:
procedure ShowInfoProc (const Name: string; NameType: TNameType; Flags: Byte; Param: Pointer); var FlagStr: string; begin FlagStr := ' '; if Flags and ufMainUnit <> 0 then FlagStr := FlagStr + 'Main Unit '; if Flags and ufPackageUnit <> 0 then FlagStr := FlagStr + 'Package Unit '; if Flags and ufWeakUnit <> 0 then FlagStr := FlagStr + 'Weak Unit '; if FlagStr <> ' ' then FlagStr := ' (' + FlagStr + ')'; with Form1.TreeView1.Items do case NameType of ntContainsUnit: AddChild (ContNode, Name + FlagStr); ntRequiresPackage: AddChild (ReqNode, Name); end; end;
What s Next?
In this chapter, you have seen how you can call functions that reside in DLLs and how to create DLLs using Delphi. After discussing dynamic libraries in general, I focused on Delphi packages, covering in particular how to place forms and other classes in a package. This is a handy technique for dividing a Delphi application into multiple executable files. While discussing packages, I explained how advanced techniques including RTTI and interfaces can be used to obtain dynamic and flexible application architectures.
I'll return to the topic of libraries that expose objects and classes when I discuss COM and OLE in Chapter 12, "From COM to COM+." For the moment, let's move to another topic related to the architecture of Delphi applications: the use of modeling tools and more examples of OOP-related techniques.