Delphi DLL 是否注定要加载程序锁?

Are Delphi DLLs predestined for loader locks?

有一个 DLL,由主(桌面)应用程序通过 Windows.LoadLibrary 动态加载。那是因为有很多类似的 DLL,并且只需要在运行时加载其中的几个或一个。所以静态链接不是一个选项。

问题是,主应用程序有时会在加载其中一个 DLL 时挂起。请注意,问题很可能发生在他们每个人身上。可能是因为他们有很多共同的代码库。

问题似乎是 加载程序锁 (see this SO answer on what it is)。我发现了一段通用代码,启动时所有 DLL 在 library 单元(即 project.dpr)的 begin...end 部分中使用,其中 GetModuleHandleGetProcAddress 被使用。

我发现,这与 DLL 完全不同,因为 DLL 项目文件的 begin...end 部分实际上是库的 DllMain 函数,调用此类函数会导致死锁(名为 loader lock)。我在这篇 Microsoft Best Practice Guide.

中读到

所以我重建了我的代码,这些调用将在 Windows.LoadLibrary 的调用完成后调用。

不幸的是,挂起的问题仍然存在。 :-(

然后我 运行 调试器,逐步完成每一个被调用的初始化,甚至在我的一行代码被执行之前。我确定,很多第三方代码违反了关于在 DLL 初始化代码中做什么和不做什么的指南:

以上所有动态加载 initialization 代码中的其他 DLL 或通过 GetProcAddress 请求过程指针。我认为这些调用会导致我的 DLL 加载时挂起。

是不是只有少数 Delphi 开发人员知道 initialization 的危险?我该怎么办?

这是一个常见问题,我认为这不是 Delphi 程序员特有的问题。如果您的代码在初始化部分调用 LoadLibrary,或者在完成部分调用 FreeLibrary,那么在库中使用该代码是不安全的。

请注意,我并不熟悉您提到的所有库,并且根本无法确认它们都有 initialization 节代码,这些代码在库中使用是不安全的。我认为这需要您确认——我想坚持这个答案中的概念,而不是对特定的 Delphi 库发表评论。

我想说的是,从 DllMain 调用 GetModuleHandleGetProcAddress 没问题。我这么说是因为你特别提到了 GetProcAddress。绝对可以通过调用GetModuleHandle获得模块句柄,然后通过调用GetProcAddress获得函数地址。因此,如果一些可疑的库这样做,并且不调用 LoadLibrary,那么它们可能没问题。

无论如何,根据上述附带条件,您需要安排在安全时间调用任何将从 DllMain 调用的违反 Microsoft 制定的规则的代码,而不是从 DllMain。不幸的是,这些规则充其量是模糊的。微软 say the following in the DllMain documentation:

The entry-point function should perform only simple initialization or termination tasks. It must not call the LoadLibrary or LoadLibraryEx function (or a function that calls these functions), because this may create dependency loops in the DLL load order. This can result in a DLL being used before the system has executed its initialization code. Similarly, the entry-point function must not call the FreeLibrary function (or a function that calls FreeLibrary) during process termination, because this can result in a DLL being used after the system has executed its termination code.

Because Kernel32.dll is guaranteed to be loaded in the process address space when the entry-point function is called, calling functions in Kernel32.dll does not result in the DLL being used before its initialization code has been executed. Therefore, the entry-point function can call functions in Kernel32.dll that do not load other DLLs. For example, DllMain can create synchronization objects such as critical sections and mutexes, and use TLS. Unfortunately, there is not a comprehensive list of safe functions in Kernel32.dll.

最后一段给您留下的指导很少。为确保您的库健壮,您需要按照以下几行做一些事情:

  1. 安排您控制其源代码的任何单元的每个初始化部分向中央注册表注册一个初始化和完成程序。
  2. 在可执行项目中,您在注册时调用初始化过程,并在程序终止时以相反的顺序调用结束过程。
  3. 在库项目中,您推迟调用这些初始化和完成过程。从 DLL 中导出一对函数,DLL 的消费者可以调用这些函数来请求调用这些初始化和完成过程。

这是我在我的图书馆中采用的方法,多年来它对我很有帮助。

这种方法涉及到相当多的工作,并且有一个缺点是您正在修改第三方库。但是,如果这些库在按交付使用时无法正常工作,您有什么选择?

也许在较慢的时间内,您可以联系您认为与在库中使用不兼容的任何库的开发人员。尝试说服他们更改代码,使其与在图书馆中使用兼容。正如您从雷米对您的问题的评论中看到的那样,库开发人员完全有可能没有意识到这个问题,并且非常愿意进行更改。

如果您在程序内部进行动态加载,那么 windows DLL 不会导致加载程序锁定,因为当程序中的第一个代码有机会执行时,它们已经加载。因此加载程序锁只能在您的库之间引起。在这种情况下,您将必须确定正确的加载顺序。如果您有一些文档,请在其中搜索。

如果您所有的库都是 Delphi/C++ Builder 库并且是在同一个 RAD Studio 版本中编译的,那么我建议您为所有这些库启用运行时包。这将减少代码重复和获得锁定的机会,因为像 Application 这样的 singelton 对象的两个实例试图同时工作。甚至更好——将您的库转换为包。这将消除任何锁定的机会。

来自俄罗斯博客的想法:http://www.gunsmoker.ru/

您可以在 Delphi 中创建在其 DllMain 中不执行任何操作的 Dll。为此,您应该创建如下新包:

package Plugin1;

//...
{$E dll}

contains
  InitHook,
  //...
  ;

end.

和 InitHook:

unit InitHook;

interface

implementation

function GetProcAddress(hModule: HMODULE; lpProcName: PAnsiChar): Pointer; stdcall; external 'kernel32.dll' name 'GetProcAddress';

procedure Done; stdcall;
type
  TPackageUnload = procedure;
var
  PackageUnload: TPackageUnload;
begin
  @PackageUnload := GetProcAddress(HInstance, 'Finalize'); //Do not localize
  PackageUnload;
end;

procedure Init; stdcall;
type
  TPackageLoad = procedure;
var
  PackageLoad: TPackageLoad;
begin
  @PackageLoad := GetProcAddress(HInstance, 'Initialize'); //Do not localize
  PackageLoad;
end;

exports
  Init,
  Done;
end.

现在您可以将您想要放入 Dll 中的任何代码放入此包中。但是您必须在从此 dll 调用任何其他函数之前调用 Init,并在卸载它之前调用 Done。

Initialize 和 Finalize 是编译器自动在包中创建的过程。这些过程执行包中所有单元的所有初始化和完成部分。