A dynamic-link library (DLL) is a collection of routines that can be called by applications and by other DLLs. Like units, DLLs contain sharable code or resources. But a DLL is a separately compiled executable that is linked at runtime to the programs that use it.
To distinguish them from standalone executables, files containing compiled DLLs are named with the .DLL extension. Object Pascal programs can call DLLs written in other languages, and Windows applications written in other languages can call DLLs written in Object Pascal.
Before you can call routines defined in a DLL, you must import them. This can be done in two ways: by declaring an external procedure or function, or by calling the Windows API directly. Whichever method you use, the routines are not linked to your application until runtime. This means that the DLL need not be present when you compile your program. It also means that there is no compile-time validation of attempts to import a routine.
Object Pascal does not support importing of variables from DLLs.
The simplest way to import a procedure or function is to declare it using the external directive. For example,
procedure DoSomething; external 'MYLIB.DLL';
If you include this declaration in a program, MYLIB.DLL is loaded once, when the program starts. Throughout execution of the program, the identifier DoSomething always refers to the same entry point in the same DLL.
Declarations of imported routines can be placed directly in the program or unit where they are called. To simplify maintenance, however, you can collect external declarations into a separate "import unit" that also contains any constants and types required for interfacing with the DLL. (Delphi's Windows unit is a good example.) Other modules that use the import unit can call any routines declared in it.
For more information about external declarations, see "Importing functions from DLLs".
You can access routines in a DLL through direct calls to Windows library functions, including LoadLibrary, FreeLibrary, and GetProcAddress (all declared in Delphi's Windows unit). In this case, use procedural-type variables to reference the imported routines. For example,
uses Windows, ...;
type
TTimeRec = record
Second: Integer;
Minute: Integer;
Hour: Integer;
end;
TGetTime = procedure(var Time: TTimeRec);
THandle = Integer;
var
Time: TTimeRec;
Handle: THandle;
GetTime: TGetTime;
...
begin
Handle := LoadLibrary('DATETIME.DLL');
if Handle <> 0 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;
When you import routines this way, the DLL is not loaded until the code containing the call to LoadLibrary executes. The DLL is later unloaded by the call to FreeLibrary. This allows you to conserve memory, and to run your program even when some of the DLLs it uses are not present.
The structure of a DLL is identical to that of a program, except that a DLL begins with the reserved word library (instead of program).
The following example shows a DLL 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 DLL 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 Object Pascal's default register calling convention.
DLLs 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 DLL's initialization code. For example,
library Editors; uses EdInit, EdInOut, EdFormat, EdPrint; exports InitEditors, DoneEditors index 17 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.
Only routines that a library explicitly exports are available for importing by other libraries or programs.
A routine is exported when it is listed in an exports clause, which has the form
where each entry consists of the name of a procedure or function (which must be declared prior to the exports clause), followed by a parameter list (only if the routine is overloaded), followed by an optional index specifier 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.)
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 DLL's export table. Use of index specifiers, which are supported for backward compatibility, 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 DLL, 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';
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 DLL is loaded. They typically perform tasks like registering window classes and initializing variables. Library initialization code can also install an exit procedure using the ExitProc variable, as described in "Exit procedures"; the exit procedure executes when the DLL is 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 DLL 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 DLL.
Here is an example of a library with initialization code and an exit procedure.
library Test; var SaveExit: Pointer; procedure LibExit; begin ... // library exit code ExitProc := SaveExit; // restore exit procedure chain end; begin ... // library initialization code SaveExit := ExitProc; // save exit procedure chain ExitProc := @LibExit; // install LibExit exit procedure end.
When a DLL is unloaded, the library's exit procedures are executed by repeated calls to the address stored in ExitProc, until ExitProc becomes nil. 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 exit procedure.
Global variables declared in a DLL cannot be imported by an Object Pascal application.
A DLL can be used by several applications at once, but each application has a copy of the DLL in its own process space with its own set of global variables. For multiple DLLs--or multiple instances of a DLL--to share memory, they must use memory-mapped files. Refer to the Windows API documentation for further information.
Several variables declared in the System unit are of special interest to programmers of DLLs. Use IsLibrary to determine whether code is executing in an application or in a DLL; IsLibrary is always False in an application and True in a DLL. During a DLL's lifetime, HInstance contains its instance handle. CmdLine is always nil in a DLL.
The DLLProc variable allows a DLL to monitor calls that the operating system makes to the DLL's entry point. This feature is normally used only by DLLs that support multithreading. 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 Windows calls the procedure, it passes to it one of the following values (defined in the Windows unit).
Indicates that the DLL 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 current process is creating a new thread. | |
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 DLL, it propagates out of the DLL to the caller. If the calling application or DLL is itself written in Object Pascal, the exception can be handled through a normal try...except statement. If the calling application or DLL is written in another language, the exception can be handled as an operating-system exception with the exception code $0EEDFACE. 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 Object Pascal exception object.
If a DLL does not use the SysUtils unit, Delphi's exception support is disabled. In this case, when a runtime error occurs in the DLL, the calling application terminates. Because the DLL has no way of knowing whether it was called from an Object Pascal program, it cannot invoke the application's exit procedures; the application is simply aborted and removed from memory.
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.
A package is a specially compiled dynamic-link library used by Delphi applications, the Delphi IDE, or both. Runtime packages provide functionality when a user runs an application. Design-time packages are used to install components in Delphi's IDE and to create special property editors for custom components. A single package can function at both design time and runtime, and design-time packages frequently work by referencing runtime packages in their requires clauses.
To distinguish them from other DLLs, package libraries are stored in files that end with the .BPL (Borland package library) extension.
Ordinarily, packages are loaded statically when an applications starts. But you can use the LoadPackage and UnloadPackage routines (in the SysUtils unit) to load packages dynamically.
Note: When an application utilizes packages, the name of each packaged unit still must appear in the uses clause of any source file that references it. For more information about packages, see the online Help.
Each package is declared in a separate source file, which should be saved with the .DPK (Delphi package) extension to avoid confusion with other files containing Object Pascal code. A package source file does not contain type, data, procedure, or function declarations. Instead, it contains
A package declaration has the form
package packageName;
requiresClause;
containsClause;
end.
where packageName is any valid identifier. The requiresClause and containsClause are both optional. For example, the following code declares the VCLDB50 package.
package VCLDB50; requires VCL50; contains Db, Dbcgrids, Dbctrls, Dbgrids, ... ; end.
The requires clause lists other, external packages used by the package being declared. It consists of the directive requires, followed by a comma-delimited list of package names, followed by a semicolon. If a package does not reference other packages, it does not need a requires clause.
The contains clause identifies the unit files to be compiled and bound into the package. It consists of the directive contains, followed by a comma-delimited list of unit names, followed by a semicolon. Any unit name may be followed by the reserved word in and the name of a source file, with or without a directory path, in single quotation marks; directory paths can be absolute or relative. For example,
contains MyUnit in 'C:\MyProject\MyUnit.pas';
Note: Thread-local variables (declared with threadvar) in a packaged unit cannot be accessed from clients that use the package.
A compiled package involves several generated files. For example, the source file for the package called VCL50 is VCL50.DPK, from which the compiler generates an executable and a binary image called VCL50.BPL and VCL50.DCP, respectively. VCL50 is used to refer to the package in the requires clauses of other packages, or when using the package in an application. Package names must be unique within a project.
The requires clause lists other, external packages that are used by the current package. It functions like the uses clause in a unit file. An external package listed in the requires clause is automatically linked at compile time into any application that uses both the current package and one of the units contained in the external package.
If the unit files contained in a package make references to other packaged units, the other packages should be included in the first package's requires clause. If the other packages are omitted from the requires clause, the compiler loads the referenced units from their DCU files.
Packages cannot contain circular references in their requires clauses. This means that
The compiler ignores duplicate references in a package's requires clause. For programming clarity and readability, however, duplicate references should be removed.
The contains clause identifies the unit files to be bound into the package. Do not include file-name extensions in the contains clause.
A package cannot be listed in the contains clause of another package or the uses clause of a unit.
All units included directly in a package's contains clause, or indirectly in the uses clauses of those units, are bound into the package at compile time. The units contained (directly or indirectly) in a package cannot be contained in any other packages referenced in requires clause of that package.
A unit cannot be contained (directly or indirectly) in more than one package used by the same application.
Packages are ordinarily compiled from the Delphi IDE using .DPK files generated by the Package editor. You can also compile .DPK files directly from the command line. When you build a project that contains a package, the package is implicitly recompiled if necessary.
The following table lists the files produced by the successful compilation of a package.
Several compiler directives and command-line switches are available to support package compilation.
The following table lists package-specific compiler directives that can be inserted into source code. See the online Help for details.
Including {$DENYPACKAGEUNIT ON} in source code prevents the unit file from being packaged. Including {$G-} or {IMPORTEDDATA OFF} may prevent a package from being used in the same application with other packages.
Other compiler directives may be included, if appropriate, in package source code.
The following package-specific switches are available for the command-line compiler. See the online Help for details.
Using the -$G- switch may prevent a package from being used in the same application with other packages.
Other command-line options may be used, if appropriate, when compiling packages.
pubsweb@inprise.com
Copyright © 1999, Inprise Corporation. All rights reserved.