Writing Dynamically Loaded Libraries
Go Up to Libraries and Packages Index
The following topics describe elements of writing dynamically loadable libraries, including
- The exports clause.
- Library initialization code.
- Global variables.
- Libraries and system variables.
Using Export Clause in Libraries
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.
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.
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.
Library Initialization Code
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 in a Library
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.
Libraries and System Variables
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 used in multithreading applications. 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.
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.
Indicates that the library is attaching to the address space of the calling process as the result of a call to LoadLibrary.
Indicates that the current process is creating a new thread.
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.
Exceptions and Runtime Errors in Libraries
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.