RAD Studio (Common)
ContentsIndex
PreviousUpNext
Writing Dynamically Loaded Libraries

The following topics describe elements of writing dynamically loadable libraries, including

  • The exports clause.
  • Library initialization code.
  • Global variables.
  • Libraries and system variables.

The main source for a dynamically loadable library is identical to that of a program, except that it begins with the reserved word library (instead of program). 

Only routines that a library explicitly exports are available for importing by other libraries or programs. The following example shows a library with two exported functions, Min and Max.

library MinMax;
    function Min(X, Y: Integer): Integer; stdcall;
 begin
   if X < Y then Min := X else Min := Y;
 end;
 function Max(X, Y: Integer): Integer; stdcall;
 begin
   if X > Y then Max := X else Max := Y;
 end;
   exports
     Min,
   Max;
 begin
 end.

If you want your library to be available to applications written in other languages, it's safest to specify stdcall in the declarations of exported functions. Other languages may not support Delphi's default register calling convention. 

Libraries can be built from multiple units. In this case, the library source file is frequently reduced to a uses clause, an exports clause, and the initialization code. For example,

library Editors;
  uses EdInit, EdInOut, EdFormat, EdPrint;
  exports
    InitEditors,
    DoneEditors name Done,
    InsertText name Insert,
    DeleteSelection name Delete,
    FormatSelection,
    PrintSelection name Print,
    .
                .
                .
    SetErrorHandler;
  begin
    InitLibrary;
  end.

You can put exports clauses in the interface or implementation section of a unit. Any library that includes such a unit in its uses clause automatically exports the routines listed the unit's exports clauses without the need for an exports clause of its own. 

The directive local, which marks routines as unavailable for export, is platform-specific and has no effect in Windows programming. 

On Linux, the local directive provides a slight performance optimization for routines that are compiled into a library but are not exported. This directive can be specified for stand-alone procedures and functions, but not for methods. A routine declared with localfor example,  

function Contraband(I: Integer): Integer; local;

does not refresh the EBX register and hence

  • cannot be exported from a library.
  • cannot be declared in the interface section of a unit.
  • cannot have its address taken or be assigned to a procedural-type variable.
  • if it is a pure assembler routine, cannot be called from another unit unless the caller sets up EBX.
A routine is exported when it is listed in an exports clause, which has the form

exports entry1, ..., entryn;

where each entry consists of the name of a procedure, function, or variable (which must be declared prior to the exports clause), followed by a parameter list (only if exporting a routine that is overloaded), and an optional name specifier. You can qualify the procedure or function name with the name of a unit. 

(Entries can also include the directive resident, which is maintained for backward compatibility and is ignored by the compiler.) 

On the Win32 platform, an index specifier consists of the directive index followed by a numeric constant between 1 and 2,147,483,647. (For more efficient programs, use low index values.) If an entry has no index specifier, the routine is automatically assigned a number in the export table.

Note: Use of index
specifiers, which are supported for backward compatibility only, is discouraged and may cause problems for other development tools. A name specifier consists of the directive name followed by a string constant. If an entry has no name specifier, the routine is exported under its original declared name, with the same spelling and case. Use a name clause when you want to export a routine under a different name. For example,

exports
DoSomethingABC name 'DoSomething';

When you export an overloaded function or procedure from a dynamically loadable library, you must specify its parameter list in the exports clause. For example,

exports
Divide(X, Y: Integer) name 'Divide_Ints',
Divide(X, Y: Real) name 'Divide_Reals';

On Win32, do not include index specifiers in entries for overloaded routines. 

An exports clause can appear anywhere and any number of times in the declaration part of a program or library, or in the interface or implementation section of a unit. Programs seldom contain an exports clause.

The statements in a library's block constitute the library's initialization code. These statements are executed once every time the library is loaded. They typically perform tasks like registering window classes and initializing variables. Library initialization code can also install an entry point procedure using the DllProc variable. The DllProc variable is similar to an exit procedure, which is described in Exit procedures; the entry point procedure executes when the library is loaded or unloaded. 

Library initialization code can signal an error by setting the ExitCode variable to a nonzero value. ExitCode is declared in the System unit and defaults to zero, indicating successful initialization. If a library's initialization code sets ExitCode to another value, the library is unloaded and the calling application is notified of the failure. Similarly, if an unhandled exception occurs during execution of the initialization code, the calling application is notified of a failure to load the library. 

Here is an example of a library with initialization code and an entry point procedure.

library Test;
var
 SaveDllProc: Pointer;
 procedure LibExit(Reason: Integer);
begin
 if Reason = DLL_PROCESS_DETACH then
  begin
    .
                .   // library exit code
                . 
  end;
   SaveDllProc(Reason);     // call saved entry point procedure
  end;
 begin
    .
                .             // library initialization code
                .
  SaveDllProc := DllProc;       // save exit procedure chain
  DllProc := @LibExit;      // install LibExit exit procedure
 end.

DllProc is called when the library is first loaded into memory, when a thread starts or stops, or when the library is unloaded. The initialization parts of all units used by a library are executed before the library's initialization code, and the finalization parts of those units are executed after the library's entry point procedure.

Global variables declared in a shared library cannot be imported by a Delphi application. 

A library can be used by several applications at once, but each application has a copy of the library in its own process space with its own set of global variables. For multiple libraries - or multiple instances of a library - to share memory, they must use memory-mapped files. Refer to the your system documentation for further information.

Several variables declared in the System unit are of special interest to those programming libraries. Use IsLibrary to determine whether code is executing in an application or in a library; IsLibrary is always False in an application and True in a library. During a library's lifetime, HInstance contains its instance handle. CmdLine is always nil in a library. 

The DLLProc variable allows a library to monitor calls that the operating system makes to the library entry point. This feature is normally used only by libraries that support multithreading. DLLProc is available on both Windows and Linux but its use differs on each. On Win32, DLLProc is used in multithreading applications.; on Linux, it is used to determine when your library is being unloaded. You should use finalization sections, rather than exit procedures, for all exit behavior. 

To monitor operating-system calls, create a callback procedure that takes a single integer parameter, for example,

procedure DLLHandler(Reason: Integer);

and assign the address of the procedure to the DLLProc variable. When the procedure is called, it passes to it one of the following values.

DLL_PROCESS_DETACH  
Indicates that the library is detaching from the address space of the calling process as a result of a clean exit or a call to FreeLibrary.  
DLL_PROCESS_ATTACH  
Indicates that the library is attaching to the address space of the calling process as the result of a call to LoadLibrary.  
DLL_THREAD_ATTACH  
Indicates that the current process is creating a new thread.  
DLL_THREAD_DETACH  
Indicates that a thread is exiting cleanly.  

In the body of the procedure, you can specify actions to take depending on which parameter is passed to the procedure.

When an exception is raised but not handled in a dynamically loadable library, it propagates out of the library to the caller. If the calling application or library is itself written in Delphi, the exception can be handled through a normal try...except statement. 

On Win32, if the calling application or library is written in another language, the exception can be handled as an operating-system exception with the exception code $0EEDFADE. The first entry in the ExceptionInformation array of the operating-system exception record contains the exception address, and the second entry contains a reference to the Delphi exception object. 

Generally, you should not let exceptions escape from your library. Delphi exceptions map to the OS exception model. 

If a library does not use the SysUtils unit, exception support is disabled. In this case, when a runtime error occurs in the library, the calling application terminates. Because the library has no way of knowing whether it was called from a Delphi program, it cannot invoke the application's exit procedures; the application is simply aborted and removed from memory.

On Win32, if a DLL exports routines that pass long strings or dynamic arrays as parameters or function results (whether directly or nested in records or objects), then the DLL and its client applications (or DLLs) must all use the ShareMem unit. The same is true if one application or DLL allocates memory with New or GetMem which is deallocated by a call to Dispose or FreeMem in another module. ShareMem should always be the first unit listed in any program or library uses clause where it occurs. 

ShareMem is the interface unit for the BORLANDMM.DLL memory manager, which allows modules to share dynamically allocated memory. BORLANDMM.DLL must be deployed with applications and DLLs that use ShareMem. When an application or DLL uses ShareMem, its memory manager is replaced by the memory manager in BORLANDMM.DLL

Copyright(C) 2009 Embarcadero Technologies, Inc. All Rights Reserved.
What do you think about this topic? Send feedback!