一 Dll的制作一般分为以下几步:
1 在一个DLL工程里写一个过程或函数
2 写一个Exports关键字,在其下写过程的名称。不用写参数和调用后缀。
二 参数传递
1 参数类型最好与window C++的参数类型一致。不要用DELPHI的数据类型。
2 最好有返回值[即使是一个过程],来报出调用成功或失败,或状态。成功或失败的返回值最好为1[成功]或0[失败].一句话,与windows c++兼容。
3 用stdcall声明后缀。
4 最好大小写敏感。
5 无须用far调用后缀,那只是为了与windows 16位程序兼容。
三 DLL的初始化和退出清理[如果需要初始化和退出清理]
1 DLLProc[SysUtils单元的一个Pointer]是DLL的入口。在此你可用你的函数替换了它的入口。但你的函数必须符合以下要求[其实就是一个回调函数]。如下:
procedure DllEnterPoint(dwReason: DWORD);far;stdcall;
dwReason参数有四种类型:
DLL_PROCESS_ATTACH:进程进入时
DLL_PROCESS_DETACH进程退出时
DLL_THREAD_ATTACH 线程进入时
DLL_THREAD_DETACH 线程退出时
在初始化部分写:
DLLProc := @DLLEnterPoint;
DllEnterPoint(DLL_PROCESS_ATTACH);
2 如Form上有TdcomConnection组件,就Uses Activex,在初始化时写一句CoInitialize (nil);
3 在退出时一定保证DcomConnection.Connected := False,并且数据集已关闭。否则报地址错。
四 全局变量的使用
在widnows 32位程序中,两个应用程序的地址空间是相互没有联系的。虽然DLL在内存中是一份,但变量是在各进程的地址空间中,因此你不能借助dll的全局变量来达到两个应用程序间的数据传递,除非你用内存映像文件。
五 调用静态载入
1 客户端函数声名:
1)大小写敏感。
2)与DLL中的声明一样。
如: showform(form:Tform);Far;external'yproject_dll.dll';
3)调用时传过去的参数类型最好也与windows c++一样。
4)调用时DLL必须在windows搜索路径中,顺序是:当前目录;Path路径;windows;widows\system;windows\ssystem32;
六 调用动态载入
1 建立一种过程类型[如果你对过程类型的变量只是一个指针的本质清楚的话,你就知道是怎么回事了]。如:
type
mypointer=procedure(form:Tform);Far;external;
var
Hinst:Thandle;
showform:mypointer;
begin
Hinst:=loadlibrary('yproject_dll');//Load一个Dll,按文件名找。
showform:=getprocaddress(Hinst,'showform');//按函数名找,大小写敏感。如果你知道自动化对象的本质就清楚了。
showform(application.mainform);//找到函数入口指针就调用。
Freelibrary(Hinst);
end;
七 在DLL建立一个TForM
1 把你的Form Uses到Dll中,你的Form用到的关联的单元也要Uses进来[这是最麻烦的一点,因为你的Form或许Uses了许多特殊的单元或函数]
2 传递一个Application参数,用它建立Form.
八 在DLL中建立一个TMDIChildForM
1 Dll中的MDIForm.FormStyle不用为fmMDIChild.
2 在CreateForm后写以下两句:
function ShowForm(mainForm:TForm):integer;stdcall
var
Form1: TForm1;
ptr:PLongInt;
begin
ptr:=@(Application.MainForm);//先把dll的MainForm句柄保存起来,也无须释放,只不过是替换一下
ptr^:=LongInt(mainForm);//用主调程序的mainForm替换DLL的MainForm。MainForm是特殊的WINDOW,它专门管理Application中的Forms资源.
//为什么不直接Application.MainForm := mainForm,因为Application.MainForm是只读属性
Form1:=TForm1.Create(mainForm);//用参数建立
end;
备注:参数是主调程序的Application.MainForm
九 示例:
DLL源代码:
library Project2; uses SysUtils, Classes, Dialogs, Forms, Unit2 in 'Unit2.pas' {Form2}; {$R *.RES} var ccc: Pchar; procedure OpenForm(mainForm:TForm);stdcall; var Form1: TForm1; ptr:PLongInt; begin ptr:=@(Application.MainForm); ptr^:=LongInt(mainForm); Form1:=TForm1.Create(mainForm); end; procedure InputCCC(Text: Pchar);stdcall; begin ccc := Text; end; procedure ShowCCC;stdcall; begin ShowMessage(String(ccc)); end; exports OpenForm; InputCCC, ShowCCC; begin end.
调用方源代码:
unit Unit1; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls; type TForm1 = class(TForm) Button1: TButton; Button2: TButton; Edit1: TEdit; procedure Button1Click(Sender: TObject); procedure Button2Click(Sender: TObject); private { Private declarations } public { Public declarations } end; var Form1: TForm1; implementation {$R *.DFM} procedure OpenForm(mainForm:TForm);stdcall;External'project2.dll'; procedure ShowCCC;stdcall;External'project2.dll'; procedure InputCCC(Text: Pchar);stdcall;External'project2.dll'; procedure TForm1.Button1Click(Sender: TObject); var Text: Pchar; begin Text := Pchar(Edit1.Text); // OpenForm(Application.MainForm);//为了调MDICHILD InputCCC(Text);//为了实验DLL中的全局变量是否在各个应用程序间共享 end; procedure TForm1.Button2Click(Sender: TObject); begin ShowCCC;//这里表明WINDOWS 32位应用程序DLL中的全局变量也是在应用程序地址空间中,16位应用程序或许不同,没有做实验。 end;
Introduction
DLL means Dynamic Link Libraries.
As the name implies , dll is a collection of functions and procedures in a place which can be used in other applications.
Basic idea of creating DLLs is to share functions and procedures across languages.
DLL files will have an extension of .dll.
How to create a dll using Delphi
It's not so hard to create a dll as you heard.
Using Delphi it's very easy.
Select the menu File -> New -> Other... from Delphi IDE.
This will open a dialog box.
From there select the "DLL Wizard" icon .
A new project will be opened with a new unit.
This unit file will be little bit different from the normal unit files.
In the normal units , the first line will contain a keyword unit and the respective unitname.
But the unit created in dll , the first keyword will be library instead of unit as given below.
library DLLProject; uses SysUtils, Classes; {$R *.RES} begin end.
Now you can add as many functions and procedures under the uses clause.
The functions and procedures that are required to be called from other applications
should be exported using the clause Exports.
The name used along with the library clause will become the name of the dll. In this case our dll will be DLLProject.dll
Suppose you have 3 functions as shown below.
Function SubFunc():Integer; begin ... end; Procedure SubProc(); begin ... end; Function MainFunc():Integer; begin ... SubFunc; SubProc; ... end; Exports MainFunc; begin end;
Here we have 2 functions and one procedure .
The MainFunc is the main function which calls other 2 functions and here we are exporting MainFunc only.
That means MainFunc is the only function which can be called from outside.
If you need to export any other function or procedure , you have to include it under the Exports clause.
Dll loading
Now we can see how these dll [exported] functions can be called from other applications.
The dll function/procedure has to be declared in the global section [before implementation section]
of the unit file where the it has to be called. The syntax should be
var procedure MainFunc ():Integer; external 'DLLProject.dll'; Implementation. end;
This kind of loading a dll is called static loading.
Because compiler try to load the dll when it loads the application itself.
So if the dll is missing in the search path , you'll get an erroro message while starting your application itself.
Now we'll see another kind of dll loading named Dynamic loading.
This kind of loading require extra commands to load dll , call your dll function and release the dll from memory.
The dll will be loaded in the memory whenever you call the LoadLibrary function.
It won't be loaded when your application starts.
So if the dll is missing in your search path, you won't get any error message during the start up of your application.
The dll should be loaded using the command LoadLibrary which returns the handle to the dll once it's loaded to the memory.
If there is any problem in loading, the handle will return 0.
Once dll is loaded properly in to the memory, we have to gather the address of the function\procedure to be called.
This can be done using the function GetProcAddress which returns the address of the dll function.
To receive the return value , you have to have a variable of type function or procedure [TDllAddr = function : Integer; ].
Have a look at the example shown below.
type TDllAddr = function : Integer; var DllAddr : TDllAddr; begin DllHandle := LoadLibrary("DLLProject.dll"); if DllHandle <> 0 then begin @DllAddr := GetProcAddress(DllHandle , 'MainProc'); DllAddr(); // Calling dll function end; end;
Finally, the memory allocated for the dll has to be explicitly released using the FreeLibrary function.
Note :
The dll function parameters should be windows types.
If you have any String type parameter in dll function/procedure ,
then you have include ShareMem unit in the dll.
Not only that, Sharemem unit should be the first unit in the uses clause.
Note: Libraries are significantly more limited than packages in what they can export.
Libraries cannot export constants, types, and normal variables.
That is, class types defined in a library will not be seen in a program using that library.
To export items other than simple procedures and functions,
packages are the recommended alternative.
Libraries should only be considered when interoperability with other programming is a requirement.
The following topics describe elements of writing dynamically loadable libraries, including
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.
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 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.
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.
A DLL or a dynamic link library acts as a shared library of function, that can be called by applications and by other DLLs.
Using Delphi, you can create and use our own DLLs, you can call functions in DLLs developed with other programming languages / by other developers.
If you are new to working with DLLs, make sure you read Introduction to DLLs.
When you want to call a function exported by a DLL, one question comes up: should you use static or dynamic DLL loading? .
Before you can call routines defined in DLL, you must import them. Functions exported from a DLL can be imported in two ways:
by declaring an external procedure or function (static),
or by direct calls to DLL specific API functions (dynamic).
Let's create a simple DLL. Here's the code to the "circle.dll"
exporting one function "CircleArea" which calculates the area of a circle using the given radius:
library circle;
uses SysUtils, Classes, Math;
{$R *.res}
function CircleArea(const radius : double) : double; stdcall;
begin
result := radius * radius * PI;
end;
exports CircleArea;
begin
end.
Note: if you need help with creating a DLL using Delphi, read the How to create (and use) a DLL in Delphi .
Once you have the circle.dll you can use the exported "CircleArea" function from your application.
The simplest way to import a procedure or function is to declare it using the external directive:
function CircleArea(const radius : double) : double; external 'circle.dll';
If you include this declaration in the interface part of a unit,
circle.dll is loaded once, when the program starts.
Throughout execution of the program, the function CircleArea is available
to all units that use the unit where the above declaration is.
You can access routines in a library through direct calls to Win32 APIs,
including LoadLibrary , FreeLibrary , andGetProcAddress .
These functions are declared in Windows.pas.
Here's how to call the CircleArea function using dynamic loading:
type TCircleAreaFunc = function (const radius: double) : double; stdcall; var dllHandle : cardinal; circleAreaFunc : TCircleAreaFunc; begin dllHandle := LoadLibrary('circle.dll') ; if dllHandle <> 0 then begin @circleAreaFunc := GetProcAddress(dllHandle, 'CircleArea') ; if Assigned (circleAreaFunc) then circleAreaFunc(15) //call the function else ShowMessage('"CircleArea" function not found') ; FreeLibrary(dllHandle) ; end else begin ShowMessage('circle.dll not found / not loaded') ; end; end;
When you import using dynamic loading, the DLL is not loaded until the call to LoadLibrary.
The library is unloaded by the call to FreeLibrary.
With static loading the DLL will be loaded and its initialization sections will execute
before the calling application's initialization sections are executed.
With dynamic loading, this is reversed.
Let's now compare static and dynamic DLL loading to see what are advantages and disadvantages of both.
Static loading PROS:
Static loading CONS:
Dynamic loading PROS:
Dynamic loading CONS:
I hope differences are clear and that you will know what type of DLL loading to use for your next project ;)
If you think some PROS or CONS are missing, feel free to let me know - I'll add it to the list.
A dynamically loadable library is a dynamic-link library (DLL) on Windows, or a DYLIB on Mac.
It is a collection of routines that can be called by applications and by other DLLs or shared objects.
Like units, dynamically loadable libraries contain sharable code or resources.
But this type of library is a separately compiled executable that is linked, at run time, to the programs that use it.
Delphi programs can call DLLs and assemblies written in other languages,
and applications written in other languages can call DLLs or assemblies written in Delphi.
You can call operating system routines directly, but they are not linked to your application until run time.
This means that the library need not be present when you compile your program.
Also, there is no compile-time validation of attempts to import a routine.
Before you can call routines defined in DLL or assembly, you must import them.
This can be done in two ways:
by declaring an external procedure or function, or by direct calls to the operating system.
Whichever method you use, the routines are not linked to your application until run time.
Delphi does not support importing variables from DLLs or assemblies.
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 the execution of the program, the identifier DoSomething always refers to the same entry point in the same shared library.
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 library.
Other modules that use the import unit can call any routines declared in it.
You can access routines in a library through direct calls to Windows APIs,
including LoadLibrary, FreeLibrary, and GetProcAddress.
These functions are declared in Windows.pas.
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('libraryname'); 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 library is not loaded
until the code containing the call to LoadLibrary executes.
The library is later unloaded by the call to FreeLibrary.
This allows you to conserve memory and to run your program
even when some of the libraries it uses are not present.
The delayed directive can be used to decorate an external routine to delay the loading of the library containing the routine.
The actual loading happens when the routine is called for the first time.
The following example demonstrates the use of the delayed directive:
function GetSomething: Integer; external 'somelibrary.dll' delayed;
In the example above, the GetSomething routine is imported from the somelibrary.dll library.
The delayed directive ensures thatsomelibrary.dll is not statically linked to the application, but rather dynamically.
The delayed directive is useful in the case where the imported routines do not exist on the target operating system on which the application is run.
Statically imported routines require that the operating system find and load the library when the application is started.
If the routine is not found in the loaded library, or the library does not exist, the Operating System halts the execution of the application.
Using the delayed directive enables you to check, at run time, whether the Operating System supports the required APIs;
only then you can call the imported routines.
Another potential use for the delayed directive is related to the memory footprint of the application:
decorating the less probably to be used routines, as delayed may decrease the memory footprint of the application,
because the libraries are loaded only when required.
The abusive use of delayed can damage the speed performance of the program (as perceived by the end user).
Note:
Trying to call a delayed routine that cannot be resolved results in a run-time error (or an exception, if the SysUtils unit is loaded).
In order to fine-tune the delay-loading process used by the Delphi Run-time Library,
you can register hook procedures to oversee and change its behavior.
To accomplish this, use SetDliNotifyHook2 and SetDliFailureHook2, declared in the SysInit unit.
This example demonstrates the fine tuning of the delay loading mechanism.
Using the provided functionality, you can hook-up various steps in the delay loading process.
This example defines three cases, one of which is correct, and two incorrect.
Note:
At the XE2 release, the delayed loading mechanism for Delphi was
refactored and moved From the System unit into the SysInit unit.
For example, System.dliNotification became SysInit.dliNotification,
and System.DliNotifyHook became SysInit.DliNotifyHook2.
This code example has not yet been revised to use the new delayed loading.
program TestDelayLoad; {$APPTYPE CONSOLE} uses Winapi.Windows, System.SysUtils; function GetDesktopWindow: HWND; stdcall; external user32 name 'GetDesktopWindow' delayed; function GetFooBar: Integer; stdcall; external kernel32 name 'GetFooBar' delayed; function GetFooBarBar: Integer; stdcall; external 'kernel33' name 'GetFooBarBar' delayed; var LOldNotifyHook, LOldFailureHook: TDelayedLoadHook; { for storing the old hook pointers } { Utility function to retrieve the name of the imported routine or its ordinal } function ImportName(const AProc: TDelayLoadProc): String; inline; begin if AProc.fImportByName then Result := AProc.szProcName else Result := '#' + IntToStr(AProc.dwOrdinal); end; function MyDelayedLoadHook(dliNotify: dliNotification; pdli: PDelayLoadInfo): Pointer; stdcall; begin { Write a message for each dli notification } case dliNotify of dliNoteStartProcessing: WriteLn('Started the delayed load session for "', pdli.szDll, '" DLL'); dliNotePreLoadLibrary: WriteLn('Starting to load "', pdli.szDll, '" DLL'); dliNotePreGetProcAddress: WriteLn('Want to get address of "', ImportName(pdli.dlp), '" in "', pdli.szDll, '" DLL'); dliNoteEndProcessing: WriteLn('Ended the delaay load session for "', pdli.szDll, '" DLL'); dliFailLoadLibrary: WriteLn('Failed to load "', pdli.szDll, '" DLL'); dliFailGetProcAddress: WriteLn('Failed to get proc address for "', ImportName(pdli.dlp), '" in "', pdli.szDll, '" DLL'); end; { Call the old hooks if they are not nil } { This is recommended to do in case the old hook do further processing } if dliNotify in [dliFailLoadLibrary, dliFailGetProcAddress] then begin if Assigned(LOldNotifyHook) then LOldFailureHook(dliNotify, pdli); end else begin if Assigned(LOldNotifyHook) then LOldNotifyHook(dliNotify, pdli); end; Result := nil; end; begin { Install new delayed loading hooks } LOldNotifyHook := SetDliNotifyHook2(MyDelayedLoadHook); LOldFailureHook := SetDliFailureHook2(MyDelayedLoadHook); { Calling an existing delayed external routine } GetDesktopWindow; try { Calling an unexisting delayed external routine in an existing library } GetFooBar; except end; try { Calling an unexisting delayed external routine in an unexisting library } GetFooBarBar; except end; { Reset the hooks } SetDliNotifyHook2(LOldNotifyHook); SetDliFailureHook2(LOldFailureHook); Readln; end.
The following table lists the cases in which the registered hook is called by the delay load helper.
Value of dliNotifyparameter | Description |
---|---|
dliNoteStartProcessing |
Sent to a delayed-load notification hook when a delayed-load session is starting. Called before the library containing the delay loaded external procedure is processed. Used to bypass or notify helper only. |
dliNotePreLoadLibrary |
Sent before LoadLibrary is called, allowing a new HMODULE to be returned. Called before the library containing the delay loaded external procedure is loaded. Can override with the new HMODULE return value. |
dliNotePreGetProcAddress |
Sent before GetProcAddress, allowing for a new procedure address to be returned if desired. Called before the address of the delay loaded procedure is found. Can override with new HMODULE return value. |
dliNoteEndProcessing |
Sent to a delayed-load notification hook when all delayed-load processing completes. Cannot be bypassed except for raise or RaiseException. |
dliFailLoadLibrary |
Sent to a delayed-load failure hook when LoadLibrary fails; allows you to specify a different valid HMODULE handle. |
dliFailGetProcAddress |
Sent to a delay-load failure hook when GetProcAddress fails; allows you to replace the procedure address with the address of a valid procedure. |
DelayLoadProc = record fImportByName: LongBool; case Byte of 0: (szProcName: _PAnsiChr); 1: (dwOrdinal: LongWord); end;
Record that holds information for a procedure that is delay loaded.
The meaning of each field in the DelayLoadProc record is given in the following table.
Field | Meaning |
---|---|
szProcName |
The name of the procedure. |
dwOrdinal |
The ordinal of the procedure. |
Some knowledgeable people posted the following wisdom about delay load. Archived here to share.
The advantages are that dlls get loaded only when they are used,
and you can “statically” bind to an import that may not exist at runtime,
and as long as you are careful not to call it, the program will still work downlevel.
The disadvantage is:
1) Some DLLs don’t work DelayLoaded (as mentioned in the limitations of LoadLibrary).
In particular, any dll that uses __declspec(thread), or any dll that you want to import data from.
2) You can’t call any API that might be delay loaded in your DllMain (since you can’t call LoadLibrary during DllMain).
Probably not a big deal since you’re generally not supposed to call any APIs in DllMain.
3) The DLLs in your process are now initialized in random order.
There may be some orders that cause bizarre bugs.
4) In particular, if you “statically” bind to a dll, then your dll is uninitialized before that other dll.
If you delayload, then you will be uninitialized after your delayloaded dlls.
5) Since DLLs may be loaded “late”, their preferred base address may be used already by stack or heap data,
resulting in a rebasing performance penalty that wouldn’t happen if the Dll was loaded at boot.
6) If the LoadLibrary fails for some reason (at the delay loading site), then your program just crashes.
You can catch this exception, but it is difficult to recover from and definitely the API callsite that was expecting
to make a call into this delayed dll will have no direct way of detecting failures.
The last one is the biggest burden.
It can mean random crashes when your app is under stress (because you’re out of memory to load that DLL).
Unless your feature is specifically designed to deal with that DLL not loading
(and you’d have to put this code around every place where you call a delayed API), you run this random crash risk.
Delay Loading definitely has its uses and its benefits, but the limitations means it is not correct to blindly delayload everything.
Only 1 is a real disadvantage. 2,3,4,5 in most cases causes no problems. 6 is a real advantage, because instead of receiving error in the loader you can get the error in the runtime and fix it.
On the contrary, 6 is no advantage at all. A hard program crash is never a good situation to be in if it can be avoided. If you want the same behaviour, then throw your own exception or abort by some other easy to trace & debug mechanism when LoadLibrary fails. At least with that approach you can choose to fail gracefully. 1 is a problem, but if you're doing that sort of thing you probably shouldn't be delay loading anyway. 3/4 is also a big problem when you have an API which then has an API built on top of it, the latter being delay loaded because it may or may not be there.
In 6 you have the same code for both. LL+GetProcAddress: HMODULE h = LoadLibrary(...); if(!h) throw Exception; W/ DL: CallFunction() // If it fails you get exception Regular call: CallFuUnction() // If it fails in the loader your program cannot run at all !! 3. You must not rely on dll load order. It is not good. 4. Yes, this can cause problem in some times, I hope it won't cause problems for me :)
Delphi 2010 Delayed Dynamic Link Libraries
Traditionally, Dynamic Link Libraries (DLLs) can be loaded in two different ways:
implicit or explicit.
In this article, I’ll create a simple DLL, and show how this DLL can be loaded implicitly as well as explicitly.
I’ll then move to a new feature introduced with Delphi 2010:
the delayed loading of DLLs, which offers the best of both worlds and more, as we’ll see.
Example DLL
The example DLL should be as simple as possible,
yet introducing another nice feature that perhaps not all of the readers may know:
the concept of function overloading in DLLs, and how to export overloaded functions correctly.
The example I’m going to use her is a DLL with two “Add” methods,
one for adding integers, and one for adding doubles.
Since the function names are the same, we must decorate them with the overload keyword.
In order to export them, we must make sure each function is exported with a unique name,
so in case of the Add for doubles, I’ll export it by name “AddFloat”.
The source code for the library eBob42 is as follows:
library eBob42; function Add(X,Y: Integer): Integer; overload; stdcall; begin Result := X + Y end; function Add(X,Y: Double): Double; overload; stdcall; begin Result := X + Y end; exports Add(X,Y: Integer) name 'Add', Add (X,Y: Double) name 'AddFloat'; end.
When compiling this source code, we’ll end up with a eBob42.DLL that we can import and use in different ways:
implicit, explicit or with the new delayed technique, offered by Delphi 2010.
The implicit import of functions from DLLs is the easiest way to use a DLL.
All you have to do is repeat the function definition, including the calling convention (plus overload when needed),
and add the “external” keyword plus the name of the DLL where the function can be found.
For an overloaded function, we should also include the name keyword again,
followed by the correct name under which the function was exported.
All in all, not very complex, and for the eBob42.DLL, the Implicit import unit could be implemented as follows:
unit eBob42Implicit; interface const DLL = 'eBob42.DLL'; function Add(X,Y: Integer): Integer; overload; stdcall external DLL; function Add(X,Y: Double): Double; overload; stdcall external DLL name 'AddFloat'; implementation end.
The biggest disadvantage of using the implicit import of DLLs technique is the fact that you’ll get an error message when trying to load an application that requires a DLL, and that DLL cannot be found. In that situation, the application will be unable to start, so the error message is a fatal one, and without the DLL, the application itself is useless.
Explicit
The main alternative for implicit import is the explicit loading and import of functions from a DLL.
This takes more code, but it allows us to give a nice error message
when the DLL cannot be loaded and/or when a function from the DLL cannot be found,
without keeping the application itself from running.
So even without the DLL being present, the application can be used (albeit without the functionality from the DLL).
As an example of an explicit import unit, where we explicitly need to load the DLL using
LoadLibrary and get a handle to the functions using GetProcAddress, is as follows:
unit eBob42Explicit; interface const DLL = 'eBob42.DLL'; var Add: function(X,Y: Integer): Integer; stdcall = nil; AddFloat: function(X,Y: Double): Double; stdcall = nil; implementation uses Windows; var eBob42Handle: Cardinal; initialization eBob42Handle := LoadLibrary(DLL); if eBob42Handle <= 32 then MessageBox(0,Error: could not load ' + DLL, 'Oops!', MB_OK) else begin Add := GetProcAddress(eBob42Handle, 'Add'); AddFloat := GetProcAddress(eBob42Handle, 'AddFloat') end finalization FreeLibrary(eBob42Handle) end.
Obviously, the unit eBob42Explicit is a lot bigger and complex than the simple unit eBob42Implicit.
And each additional function from the DLL will make this difference bigger,
because eBob42Implicit only needs to list the function (with the external keyword),
while eBob42Explicit needs to declare a function pointer and assign a value to that pointer using GetProcAddress.
The biggest advantage of explicit importing is the fact that the application
will be able to load and start even if the DLL that we’re trying to use cannot be found (or loaded).
We’ll see an error message when the LoadLibrary or GetProcAddress fails, but the application itself will still run.
The disadvantage is that the code for the explicit import unit is a lot more complex,
and when we call the imported functions through the function pointers,
we should check if the function pointers are actually assigned
(otherwise we might still get a run-time error or access violation).
Although neither of these approaches appears perfect,
Delphi 2010 now supports a third method which combines the strength and best of both worlds, and then some.
The technique is known as delayed loading.
Delay Load
Delphi 2010 introduces a new keyword:
delayed.
In fact, it’s so new that the online help, the wiki and even the syntax highlighter don’t know about it, yet.
The only source of information that I could find was the blog post of Allen Bauer of the Delphi R&D Team itself.
The basic idea of the solution is the fact that the DLL will not be loaded right away (which is the case for implicit linking),
but only when needed.
So potentially “delayed”, and hence the name “delay loading”.
The syntax of using the delayed approach is actually quite similar to the implicit import unit,
with the exception that we now add the delayed keyword to the function definition
(and since neither code insight and syntax highlighting seem to know about this new keyword,
you’ll have to trust on the compiler to tell you when it’s right:
after the name of the DLL, without semi-colon between the name of the DLL and the delayed keyword itself).
unit eBob42Delayed; interface const DLL = 'eBob42.DLL'; function Add(X,Y: Integer): Integer; overload; stdcall external DLL delayed; function Add(X,Y: Double): Double; overload; stdcall external DLL name 'AddFloat' delayed; implementation end.
When compiling unit eBob42Delayed, you get two warnings about the delayed keyword,
telling you that the symbol DELAYED is specific to a platform.
Yeah right, that doesn’t matter to much to me to be honest.
What matters is that we now have the ease of implicit importing with the robustness of explicit importing.
The best of both worlds:
unit eBob42Delayed is as short as unit eBob42Implicit,
and yet the application will start normally even if the DLL cannot be found.
There is one thing left to test:
imagine what would happen if we use the eBob42Delayed unit, and start the application
without the DLL being present (or found), and then call the function Add?
The good news is that the application can be started just fine, and will remain up-and-running.
The bad news is that the user will see a not very user-friendly error message, namely:
I can imagine that for the average user this error message will not be fully clear,
so the user may not know what the actual problem is.
Of course, we can catch this EExternalException in a try-except block,
but the problem is that we do not know if the error is caused by a missing DLL,
or perhaps by the function which was not found in the DLL
(for example if an incorrect version of the DLL was loaded with the correct name, but without the required function exported).
DelayedLoadHook
Based on a blog post from – again – Allen Bauer,
we could read that there is actually a way to handle the specific errors that can occur
when (delay) loading a DLL or obtaining a function pointer using GetProcAddress.
The delay loading itself is done in an old (but well-tested) delayhpl.c file from the C++RTL,
which offers the option to “hook” to the notification messages from this process
by defining a DelayedLoadHook function that we can install using the SetDliNotifyHook function.
The DelayedLoadHook function should be defined as follows (according to line 2392 of system.pas):
DelayedLoadHook = function (dliNotify: dliNotification; pdli: PDelayLoadInfo): Pointer; stdcall;
The records dliNotification and PDelayLoadInfo are also interesting,
and contain the information we need to determine the nature of the notification (and possibly error).
Again a little snippet from system.pas:
dliNotification = ( dliNoteStartProcessing, { used to bypass or note helper only } dliNotePreLoadLibrary, { called just before LoadLibrary, can } { override w/ new HMODULE return val } dliNotePreGetProcAddress, { called just before GetProcAddress, can } { override w/ new Proc address return } { value } dliFailLoadLibrary, { failed to load library, fix it by } { returning a valid HMODULE } dliFailGetProcAddress, { failed to get proc address, fix it by } { returning a valid Proc address } dliNoteEndProcessing { called after all processing is done, } { no bypass possible at this point } { except by raise, or RaiseException } );
Based on the value of dliFailLoadLibrary, we can raise an exception to explain to the user in detail that a DLL could not be loaded.
And based on the value dliFailGetPRocAddress, we can tell the user that the DLL could be loaded,
but the specific function could not be found in this DLL.
In order to determine the name of the DLL and when needed the name of the function,
we should examine the DelayLoadInfo record, which is defined as follows:
DelayLoadInfo = record cb: LongWord; { size of structure } pidd: PImgDelayDescr; { raw form of data (everything is there) } ppfn: Pointer; { points to address of function to load } szDll: PAnsiChar; { name of dll } dlp: TDelayLoadProc; { name or ordinal of procedure } hmodCur: HMODULE; { the hInstance of the library we have loaded } pfnCur: Pointer; { the actual function that will be called } dwLastError: LongWord; { error received (if an error notification) } end;
For the name of the DLL itself, we can use the field szDll,
and for the name of the function (or the value of the export index from the function in the DLL)
we have to look a little bit further (or deeper) to the dlp structure of type TDelayLoadProc.
The type DelayLoadProc in turn is defined in a variant record as follows:
DelayLoadProc = record fImportByName: LongBool; case Byte of 0: (szProcName: PAnsiChar); 1: (dwOrdinal: LongWord); end;
If the variant record field fImportByName is true, then we should look at the szProcName field,
otherwise the value of the dwOrdinal field must be used
(in case the function was imported by index number instead of by name).
With this information at our disposal, we can write a procedure DelayedHandlerHook (see listing 5)
with arguments dliNotification and PDelayLoadInfo,
and inside this procedure we can raise an exception with detailed information if the specific error situations have occurred.
DelayedHandler
For my own convenience, I’ve placed the procedure DelayedHandlerHook inside its own unit DelayedHandler,
making sure that the DelayedHandlerHook is installed by calling the SetDliFailureHook function
in the initialization section of the unit, and uninstalling it again in the finalization section again.
As a result, you only need to add this unit to the uses clause of any project that uses the “delayed” external references,
and needs to be able to raise specific exceptions for the situations
where the DLL could not be found (or loaded) or when a specific function (or index) could not be found in the DLL.
The two specific exceptions are of type ELoadLibrary and EGetProcAddress and also defined in Listing 5:
unit DelayedHandler; interface uses SysUtils; type ELoadLibrary = class(Exception); EGetProcAddress = class(Exception); implementation function DelayedHandlerHook(dliNotify: dliNotification; pdli: PDelayLoadInfo): Pointer; stdcall; begin if dliNotify = dliFailLoadLibrary then raise ELoadLibrary.Create('Could not load ' + pdli.szDll) else if dliNotify = dliFailGetProcAddress then if pdli.dlp.fImportByName then raise EGetProcAddress.Create('Could not load ' + pdli.dlp.szProcName + ' from ' + pdli.szDll) else raise EGetProcAddress.Create('Could not load index ' + IntToStr(pdli.dlp.dwOrdinal) + ' from ' + pdli.szDll) end; initialization SetDliFailureHook(DelayedHandlerHook); finalization SetDliFailureHook(nil); end.
This time, when the DLL could not be found or loaded,
we’ll get a far more descriptive exception which can be displayed in a ShowMessage box as follows:
My final tests show that each time we call the (delay loaded) Add function,
the application tries to load the DLL (if not already loaded).
This means that if the DLL could not be found the first time this function was called,
then during the second attempt, it will again try to load the DLL.
And if we’ve found and for example installed the DLL in the meantime, then this means the second call will succeed!
This is another, perhaps unexpected, advantage of the delayed loading approach compared to the explicit importing approach.
Unless we extend the explicit importing approach to also try to load the DLL
when we make a function call, the delay loading will be more robust and able to connect to a DLL even after the application has started.
As final test, let’s see what happens if the DLL is present but we want to import and call a function with an incorrect name.
As a test, we should modify the eBob42Delayed unit for the Add Float function as follows:
function Add(X,Y: Double): Double; overload; stdcall external DLL name 'AddDouble' delayed; // was ‘AddFloat’
This will lead to a nice exception to inform us that the function AddDouble could not be found in the eBob42.DLL.
Note that the exception is displayed using a call to ShowMessage, by our client code, inside a try-except block.
But you can use this technique also in web server application
(where you don’t want to use a ShowMessage) by handling the exception inside a try-except block in another way.
Summary
Where the implict import of DLLs is easy but not always very convenient or robust (when the DLL is not present, the application won’t start),
and the explicit import of DLLs is robust but more complex (where you should also always check before calling a function pointer that this function pointer is actually assigned),
there the delayed loading technique of DLLs offers the best of both worlds.
The easy of declaration (with the additional delayed keyword) with the easy and robustness of use,
plus the ability to “connect” to the DLL at a later time, even after the application was started and the DLL wasn’t found the first time.
In combination with the unit DelayedHandler, for specific exception raising in case the DLL could not be found or loaded,
or a specific function was not found, we now have the ultimate way of importing and using DLLs from now on
(although this functionality is only supported by Delphi 2010).
References
Allen Bauer, Procrastinators Unite… Eventually!
Allen Bauer, Exceptional Procrastination
We’re all taught at an early age to “Never put off until tomorrow that which can be done today.”
In general, that is wise advice. However there are some cases where you do want to wait
until the last possible moment to do (or not do) something.
In fact, that is one of the overall tenets of Agile Programming;
delay decisions until the last possible moment
because you always know more about a problem tomorrow
than you do today and can make a better, more informed decision.
But, I digress. I’m not here to talk about philosophies of life,
or to introduce another “agile methodology” or even about a new weight loss plan based on bacon, lard and cheese puffs.
How many of you have written this same bit of boilerplate code or something similar over and over again?
if OSVersion >= 5.0 then begin Module = LoadLibrary('kernel32.dll'); if Module <> 0 then begin Proc := GetProcAddress(Module, 'APICall'); if Assigned(Proc) then Proc(Param1, Param2); end; end else { gracefully fallback } What if you could just do this? if OSVersion >= 5.0 then APICall(Param1, Param2); else { gracefully fallback }
The astute among you would immediately see that with previous Delphi releases,
the second form, while certainly preferable, at some point the call to “APICall”
would eventually have to effectively go through some bit of code similar to the first bit of code.
Normally, you would declare an external API reference like this:
procedure APICall(Param1, Param2: Integer); stdcall; external 'kernel32.dll' name 'APICall';
That will cause the linker to emit a external reference into the executable binary that is resolved at load time.
Therein lies the problem.
That is the situation that the first bit of code above was designed to avoid.
If APICall didn’t existing in ‘kernel32.dll’ at load time, the whole application would fail to load.
Game over.
Thanks for playing.
Now, what if you could write code similar the second block of code above and still declare your external API calls in a manner similar to above?
Starting with Delphi 2010, you can do exactly the scenario I describe.
To make the second code block work even if “APICall” isn’t available on the version of the OS on which your application is currently running,
we’ve introduced a new directive to be used only in the above context, delayed;
procedure APICall(Param1, Param2: Integer); stdcall; external 'kernel32.dll' name 'APICall' delayed;
Simply put, by adding the delayed directive, that instructs the linker to generate the external API reference differently in the executable binary.
Now Delphi’s RTL will take care of all that ugly “late-binding” boilerplate code for you.
Rather than generating the import in the normal “Imports” section of the executable’s PE file,
it is generated into the “Delayed Imports” section following the published PE spec.
This also requires that the RTL now has a generic function that does the proper lookups
and binds the API whenever it is called the first time.
Subsequent calls are just as fast as a normal import.
This is different than similar functionality available in ILink32 from C++Builder
wherein you can only specify an entire dll in which all references are delay loaded.
Also, in C++ you cannot specify kernel32.dll or even ntdll.dll to be delay loaded,
since the very startup of any application or dll requires them to already be loaded.
In Delphi you can, since it is on an API-by-API basis.
The intent of this feature was to make managing all the new APIs
from Windows Vista and now Windows 7 much easier without having to continuously
and manually write all the delay loaded boilerplate code.
Throughout VCL, we can simply make runtime decisions on
which APIs to call without always going through those manually coded “thunks.”
They are now simply handled by the compiler/linker
and the source code barely reveals the fact that something is late-bound.
In a future release, we are considering adding
both the API-by-API capability to C++Builder and
a way to specify an entire dll to be late-bound in Delphi.
I kept putting this post off… ok, ok… that was a really bad pun…
Seems there was a little bit of concern about my
last post regarding the new ‘delayed’ directive.
Christian Wimmer had a few critiques
regarding the implementation and how the exception
that was raised was an EExternalException with an obscure exception code.
There is a simple explanation for this.
The delay load helper functions were taken directly from the C++Builder RTL.
By that I mean, the delayhlp.c file gets built to a .obj file
by the C++ compiler and then directly linked into the Delphi System unit.
There were several key reasons for this.
The first of which was the code was already written, has been in many versions of C++Builder RTL
(including back in the old Borland C++ days) and has been extensively tested.
Another reason is that in order for Delphi and C++Builder to “play-nice,”
when a Delphi unit is linked in C++Builder that contains a reference to a delay load import,
ILink32 takes over the duties of generating the proper delay load functionality into the PE file.
So the C++RTL version of delayhlp.c is what is used.
In order to things to remain consistent, this is the route taken.
Had there been two delay-load helper functions in the case of a C++Builder application,
then any existing C++ code that used the delay load hooks would only work on other C++ code and vice-versa.
Fear not, all is not lost.
To satisfy our good friend, Christian’s request, here is a unit that you can use to generate nice,
unique Delphi specific exceptions.
This is accomplished by leveraging the ability for the delayhlp.c code to be “hooked.”
This code also demonstrates another new Delphi language feature,
class constructors and destructors.
I will describe them in better detail in a subsequent post.
If you never reference any of the declared exception types in this unit,
the hook is not installed and the normal EExternalException is raised.
Presumably you would use this unit for the purpose of actually catching the exceptions
and doing something interesting with them.
Another thing to note is that you should also be able to add this unit to a C++Builder application
and it work for any delay-load functions done in the existing way for C++
and any Delphi units that reference Delphi imports with the delayed directive.
unit DelayExcept; interface uses SysUtils; type EDliException = class(Exception) private class constructor Create; class destructor Destroy; end; EDliLoadLibraryExeception = class(EDliException) private FDllName: string; public constructor Create(const ADllName: string); overload; property DllName: string read FDllName; end; EDliGetProcAddressException = class(EDliException) private FDllName: string; FExportName: string; public constructor Create(const ADllName, AExportName: string); overload; constructor Create(const ADllName: string; AOrdinal: LongWord); overload; property DllName: string read FDllName; property ExportName: string read FExportName; end; implementation { EDliLoadLibraryExeception } constructor EDliLoadLibraryExeception.Create(const ADllName: string); begin inherited Create(Format('Unable to load ''%s''', [ADllName])); FDllName := ADllName; end; { EDliGetProcAddressException } constructor EDliGetProcAddressException.Create(const ADllName, AExportName: string); begin inherited Create(Format('Unable to locate export name ''%s'' in ''%s''', [AExportName, ADllName])); FDllName := ADllName; FExportName := AExportName; end; constructor EDliGetProcAddressException.Create(const ADllName: string; AOrdinal: LongWord); begin inherited Create(Format('Unable to locate export ordinal ''%d'' in ''%s''', [AOrdinal, ADllName])); FDllName := ADllName; FExportName := IntToStr(AOrdinal); end; function DelayLoadFailureHook(dliNotify: dliNotification; pdli: PDelayLoadInfo): Pointer; stdcall; begin Result := nil; if dliNotify = dliFailLoadLibrary then raise EDliLoadLibraryExeception.Create(pdli.szDll); if dliNotify = dliFailGetProcAddress then if pdli.dlp.fImportByName then raise EDliGetProcAddressException.Create(pdli.szDll, pdli.dlp.szProcName) else raise EDliGetProcAddressException.Create(pdli.szDll, pdli.dlp.dwOrdinal); end; { EDliException } class constructor EDliException.Create; begin SetDliFailureHook(DelayLoadFailureHook); end; class destructor EDliException.Destroy; begin SetDliFailureHook(nil); end; end.