Dynamic-link libraries

Dynamic-link libraries (DLLs) permit several Windows applications to share code and resources. With Object Pascal, you can use DLLs as well as write your own DLLs to be used by other applications.

What is a DLL?

A DLL is an executable module containing code or resources for use by other applications or DLLs. Conceptually, a DLL is similar to a unit—both have the ability to provide services in the form of procedures and functions to a program. There are, however, many differences between DLLs and units. In particular, units are statically linked, whereas DLLs are dynamically linked.

When a program uses a procedure or function from a unit, a copy of that procedure or function’s code is statically linked into the program’s executable file. If two programs are running simultaneously and they use the same procedure or function from a unit, there will be two copies of that routine present in the system. It would be more efficient if the two programs could share a single copy of the routine. DLLs provide that ability.

In contrast to a unit, the code in a DLL isn’t linked into a program that uses the DLL. Instead, a DLL’s code and resources are in a separate executable file with a

.DLL extension. This file must be present when the client program runs. The procedure and function calls in the program are dynamically linked to their entry points in the DLLs used by the application.

Another difference between units and DLLs is that units can export types, constants, data, and objects whereas DLLs can export procedures and functions only.

Note A DLL doesn’t have to be written in Object Pascal for a Object Pascal application to be able to use it. Also, programs written in other languages can use DLLs written in Object Pascal. DLLs are therefore ideal for multi-language programming projects.

Using DLLs

For a module to use a procedure or function in a DLL, the module must import the procedure or function using an external declaration. For example, the following external declaration imports a function called GlobalAlloc from the DLL called KERNEL (the Windows kernel):

function GlobalAlloc(Flags: Word; Bytes: Longint): THandle; far; external 'KERNEL' index 15;

In imported procedures and functions, the external directive takes the place of the declaration and statement parts that would otherwise be present. Imported procedures and functions must use the far call model selected by using a far procedure directive or a {$F+} compiler directive, but otherwise they behave no differently than normal procedures and functions. See “External declarations” on page 72.

Object Pascal imports procedures and functions in three ways:

  • By name

  • By new name

  • By ordinal

The format of external directives for each of the three methods is demonstrated in the following examples.

When no index or name clause is specified, the procedure or function is imported by name. The name used is the same as the procedure or function’s identifier. In this example, the ImportByName procedure is imported from ‘TESTLIB’ using the name ‘IMPORTBYNAME’.

procedure ImportByName; external 'TESTLIB';

When a name clause is specified, the procedure or function is imported by a different name than its identifier. Here the ImportByNewName procedure is imported from ‘TESTLIB’ using the name ‘REALNAME’:

procedure ImportByNewName; external 'TESTLIB' name 'REALNAME';

Finally, when an index clause is present, the procedure or function is imported by ordinal. Importing by ordinal reduces the module’s load time because the name doesn’t have to be looked up in the DLL’s name table. In the example, the ImportByOrdinal procedure is imported as the fifth entry in the ‘TESTLIB’ DLL:

procedure ImportByOrdinal; external 'TESTLIB' index 5;

The DLL name specified after the external keyword and the new name specified in a name clause don’t have to be string literals. Any constant-string expression is allowed. Likewise, the ordinal number specified in an index clause can be any constant-integer expression.

const

TestLib = 'TESTLIB';

Ordinal = 5;

procedure ImportByName; external TestLib;

procedure ImportByNewName; external TestLib name 'REALNAME';

procedure ImportByOrdinal; external TestLib index Ordinal;

Although a DLL can have variables, it’s not possible to import them in other modules. Any access to a DLL’s variables must take place through a procedural interface.

Import units

Declarations of imported procedures and functions can be placed directly in the program that imports them. Usually, though, they are grouped together in an import unit that contains declarations for all procedures and functions in a DLL, along with any constants and types required to interface with the DLL. The WinTypes and WinProcs units supplied with Delphi are examples of such import units. Import units aren’t a requirement of the DLL interface, but they do simplify maintenance of projects that use multiple DLLs.

As an example, consider a DLL called DATETIME.DLL that has four routines to get and set the date and time, using a record type that contains the day, month, and year, and another record type that contains the second, minute, and hour. Instead of specifying the associated procedure, function, and type declarations in every program that uses the DLL, you can construct an import unit to go along with the DLL. This code creates a .DCU file, but it doesn’t contribute code or data to the programs that use it:

unit DateTime;

interface type

TTimeRec = record

Second: Integer; Minute: Integer; Hour: Integer;

end;

type

TDateRec = record Day: Integer; Month: Integer; Year: Integer;

end;

procedure SetTime(var Time: TTimeRec); procedure GetTime(var Time: TTimeRec); procedure SetDate(var Date: TDateRec); procedure GetDate(var Date: TDateRec);

implementation

procedure SetTime; external 'DATETIME' index 1; procedure GetTime; external 'DATETIME' index 2; procedure SetDate; external 'DATETIME' index 3; procedure GetDate; external 'DATETIME' index 4;

end.

Any program that uses DATETIME.DLL can now simply specify DateTime in its

uses clause. Here is a Windows program example:

program ShowTime;

uses WinCrt, DateTime;

var

Time: TTimeRec;

begin

GetTime(Time);

with Time do

WriteLn('The time is ', Hour, ':', Minute, ':', Second);

end.

Another advantage of an import unit such as DateTime is that when the associated DATETIME.DLL is modified, only one unit, the DateTime import unit, needs updating to reflect the changes.

When you compile a program that uses a DLL, the compiler doesn’t look for the DLL so it need not be present. The DLL must be present when you run the program, however.

Note If you write your own DLLs, they aren’t automatically compiled when you compile a program that uses the DLL. Instead, DLLs must be compiled separately.

Static and dynamic imports

The external directive provides the ability to statically import procedures and functions from a DLL. A statically-imported procedure or function always refers to the same entry point in the same DLL. Windows also supports dynamic imports, whereby the DLL name and the name or ordinal number of the imported procedure or function is specified at run time. The ShowTime program shown here uses dynamic importing to call the GetTime procedure in DATETIME.DLL. Note the use of a procedural-type variable to represent the address of the GetTime procedure.

program ShowTime;

uses WinProcs, WinTypes, WinCrt;

type

TTimeRec = record

Second: Integer;

Minute: Integer;

Hour: Integer;

end;

TGetTime = procedure(var Time: TTimeRec);

var

Time: TTimeRec;

Handle: THandle;

GetTime: TGetTime;

begin

Handle := LoadLibrary('DATETIME.DLL');

if Handle >= 32 then begin

@GetTime := GetProcAddress(Handle, 'GETTIME');

if @GetTime <> nil then begin

GetTime(Time);

with Time do

WriteLn('The time is ', Hour, ':', Minute, ':', Second);

end; FreeLibrary(Handle);

end; end;

Writing DLLs

The structure of a Object Pascal DLL is identical to that of a program, except a DLL starts with a library header instead of a program header. The library header tells Object Pascal to produce an executable file with the extension .DLL instead of .EXE, and also ensures that the executable file is marked as being a DLL.

library

Dynamic-link libraries - 图1Dynamic-link libraries - 图2

library heading

The example here implements a very simple DLL with two exported functions, Min

and Max, that calculate the smaller and larger of two integer values.

library MinMax;

function Min(X, Y: Integer): Integer; export; begin

if X < Y then Min := X else Min := Y;

end;

function Max(X, Y: Integer): Integer; export; begin

if X > Y then Max := X else Max := Y;

end;

exports

Min index 1,

Max index 2;

begin end.

Note the use of the export procedure directive to prepare Min and Max for exporting, and the exports clause to actually export the two routines, supplying an optional ordinal number for each of them.

Although the preceding example doesn’t demonstrate it, libraries can and often do consist of several units. In such cases, the library source file itself is frequently reduced to a uses clause, an exports clause, and the library’s initialization code. For example,

library Editors;

uses EdInit, EdInOut, EdFormat, EdPrint;

exports

InitEditors index 1,

DoneEditors index 2,

InsertText index 3,

DeleteSelection index 4,

FormatSelection index 5,

PrintSelection index 6, ƒ

SetErrorHandler index 53;

begin

InitLibrary;

end.

The export procedure directive

If procedures and functions are to be exported by a DLL, they must be compiled with the export procedure directive. The export directive belongs to the same family of procedure directives as the near, far, and inline directives. This means that an export directive, if present, must be specified upon the first introduction of procedure or function—it can’t be supplied in the defining declaration of a forward declaration.

The export directive makes a procedure or function exportable. It forces the routine to use the far call model and prepares the routine for export by generating special procedure entry and exit code. Note, however, that the actual exporting of the procedure or function doesn’t occur until the routine is listed in a library’s exports clause. See “Export declarations” on page 71.

The exports clause

A procedure or function is exported by a DLL when it’s listed in the library’s

exports clause.

exports clause

Dynamic-link libraries - 图3

Dynamic-link libraries - 图4exports list

An exports clause can appear anywhere and any number of times in a program or library’s declaration part. Each entry in an exports clause specifies the identifier of a procedure or function to be exported. That procedure or function must be declared before the exports clause appears, however, and its declaration must contain the export directive. You can precede the identifier in the exports clause with a unit identifier and a period; this is known as a fully qualified identifier.

An exports entry can also include an index clause, which consists of the word index followed by an integer constant between 1 and 32,767. When an index clause is specified, the procedure or function to be exported uses the specified ordinal number. If no index clause is present in an exports entry, an ordinal number is automatically assigned. The quickest way to look up a DLL entry is by index.

An entry can also have a name clause, which consists of the word name followed by a string constant. When there is a name clause, the procedure or function to be exported uses the name specified by the string constant. If no name clause is present in an exports entry, the procedure or function is exported by its identifier and converted to all uppercase.

Finally, an exports entry can include the resident keyword. When resident is specified, the export information stays in memory while the DLL is loaded. The resident option significantly reduces the time it takes to look up a DLL entry by name, so if client programs that use the DLL are likely to import certain entries by name, those entries should be exported using the resident keyword.

A program can contain an exports clause, but it seldom does because Windows doesn’t allow application modules to export functions for use by other applications.

Library initialization code

The statement part of a library constitutes the library’s initialization code. The initialization code is executed once, when the library is initially loaded. When subsequent applications that use the library are loaded, the initialization code isn’t executed again, but the DLL’s use count is incremented.

A DLL is kept in memory as long as its use count is greater than zero. When the use count becomes zero, indicating that all applications that used the DLL have terminated, the DLL is removed from memory. At that point, the library’s exit procedures are executed. Exit procedures are registered using the ExitProc variable, as described in Chapter 17, “Control issues.”

A DLL’s initialization code typically performs tasks like registering window classes for window procedures contained in the DLL and setting initial values for the DLL’s global variables. The initialization code of a library can signal an error condition by setting the ExitCode variable to zero. (ExitCode is declared by the System unit.) ExitCode defaults to 1, indicating initialization was successful. If the initialization code sets ExitCode to zero, the DLL is unloaded from system memory and the calling application is notified of the failure to load the DLL.

When a library’s exit procedures are executed, the ExitCode variable doesn’t contain a process-termination code, as is the case with a program. Instead, ExitCode contains one of the values wep_System_Exit or wep_Free_DLL, which are defined in the WinTypes unit. wep_System_Exit indicates that Windows is shutting down, whereas wep_Free_DLL indicates that just this single DLL is being unloaded.

Here is an example of a library with initialization code and an exit procedure:

library Test;{$S-}

uses WinTypes, WinProcs;

var

SaveExit: Pointer;

procedure LibExit; far; begin

if ExitCode = wep_System_Exit then begin

ƒ

{ System shutdown in progress } ƒ

end else begin

ƒ

{ DLL is being unloaded } ƒ

end;

ExitProc := SaveExit;

end;

begin

ƒ

{ Perform DLL initialization } ƒ

SaveExit := ExitProc; { Save old exit procedure pointer } ExitProc := @LibExit; { Install LibExit exit procedure }

end.

When a DLL is unloaded, an exported function called WEP in the DLL is called, if it’s present. A Object Pascal library automatically exports a WEP function, which continues to call the address stored in the ExitProc variable until ExitProc becomes nil. Because this works the same way exit procedures are handled in Object Pascal programs, you can use the same exit procedure logic in both programs and libraries.

Note Exit procedures in a DLL must be compiled with stack-checking disabled (the {$S-} state) because the operating system switches to an internal stack when terminating a DLL. Also, the operating system crashes if a run-time error occurs in a DLL exit procedure, so you must include sufficient checks in your code to prevent run-time errors.

Library programming notes

The following sections note important points you should keep in mind while working with DLLs.

Global variables in a DLL

A DLL has its own data segment and any variables declared in a DLL are private to that DLL. A DLL can’t access variables declared by modules that call the DLL, and it’s not possible for a DLL to export its variables for use by other modules. Such access must take place through a procedural interface.

Global memory and files in a DLL

As a rule, a DLL doesn’t “own” any files that it opens or any global memory blocks that it allocates from the system. Such objects are owned by the application that (directly or indirectly) called the DLL.

When an application terminates, any open files owned by it are automatically closed, and any global memory blocks owned by it are automatically deallocated. This means that file and global memory-block handles stored in global variables in a DLL can become invalid at any time without the DLL being notified. For that reason, DLLs should refrain from making assumptions about the validity of file and global memory-block handles stored in global variables across calls to the DLL. Instead, such handles should be made parameters of the procedures and functions of the DLL, and the calling application should be responsible for maintaining them.

Note Global memory blocks allocated with the gmem_DDEShare attribute (defined in the WinTypes unit) are owned by the DLL, not by the calling applications. Such memory blocks remain allocated until they are explicitly deallocated by the DLL, or until the DLL is unloaded.

DLLs and the System unit

During a DLL’s lifetime, the HInstance variable contains the instance handle of the DLL. The HPrevInst and CmdShow variables are always zero in a DLL, as is the

PrefixSeg variable, because a DLL doesn’t have a Program Segment Prefix (PSP). PrefixSeg is never zero in an application, so the test PrefixSeg <> 0 returns True if the current module is an application, and False if the current module is a DLL.

To ensure proper operation of the heap manager contained in the System unit, the start-up code of a library sets the HeapAllocFlags variable to gmem_Moveable + gmem_DDEShare. Under Windows, this causes all memory blocks allocated via New and GetMem to be owned by the DLL instead of the applications that call the DLL.

For details about the heap manager, see Chapter 16.

Run-time errors in DLLs

If a run-time error occurs in a DLL, the application that called the DLL terminates. The DLL itself isn’t necessarily removed from memory at that time because other applications might still be using it.

Because a DLL has no way of knowing whether it was called from a Object Pascal application or an application written in another programming language, it’s not possible for the DLL to invoke the application’s exit procedures before the application is terminated. The application is simply aborted and removed from memory. For this reason, make sure there are sufficient checks in any DLL code so such errors don’t occur.

If a run-time error does occur in a DLL, the safest thing to do is to exit Windows entirely. If you simply try to modify and rebuild the faulty DLL code, when you run your program again, Windows won’t load the new version if the buggy one is still in memory. Exiting Windows and then restarting Windows and Object Pascal ensures that your corrected version of the DLL is loaded.

DLLs and stack segments

Unlike an application, a DLL doesn’t have its own stack segment. Instead, it uses the stack segment of the application that called the DLL. This can create problems in DLL routines that assume that the DS and SS registers refer to the same segment, which is the case in a Windows application module.

The Object Pascal compiler never generates code that assumes DS = SS, and none of the Object Pascal run-time library routines make this assumption. If you write assembly language code, don’t assume that SS and DS registers contain the same value.

C h a p t e r