Advanced Features of Delphi DLLs

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'}
Click To expand Figure 10.3: The Application page of the Project Options dialog box now has a Library Name section.

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:

  TIntFunction = function (I: Integer): Integer; stdcall;
  DllName = 'Firstdll.dll';
procedure TForm1.Button1Click(Sender: TObject);
  HInst: THandle;
  FPointer: TFarProc;
  MyFunct: TIntFunction;
  HInst := SafeLoadLibrary (DllName);
  if HInst > 0 then
    FPointer := GetProcAddress (HInst,
      PChar (Edit1.Text));
    if FPointer <> nil then
      MyFunct := TIntFunction (FPointer);
      SpinEdit1.Value := MyFunct (SpinEdit1.Value);
      ShowMessage (Edit1.Text + ' DLL function not found');
    FreeLibrary (HInst);
    ShowMessage (DllName + ' library not found');

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;
  FormScroll: TFormScroll;
  // default value
  Result := Col;
    FormScroll := TFormScroll.Create (Application);
      // initialize the data
      FormScroll.SelectedColor := Col;
      // show the form
      if FormScroll.ShowModal = mrOK then
        Result := FormScroll.SelectedColor;
    on E: Exception do
      MessageDlg ('Error in library: ' + E.Message, mtError, [mbOK], 0);

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!

Part I: Foundations