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.
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."
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.
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.
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.
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.