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.
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:
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.
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.
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';
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.
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  := Separator; SepStr  := #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.
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):
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;