卸载使用GdiPlus的DLL时程序卡住了

5
我有一个应用程序,它加载使用Delphi GDI+ Library的DLL。当卸载该DLL(调用FreeLibrary)时,该应用程序会挂起。
我追踪了问题,发现是GdiPlus.pas单元的finalization部分调用了GdiPlusShutdown,但该函数从未返回。
如何避免这种死锁?
2个回答

4

GdiplusStartup函数的文档如下:

不要在DllMain或任何被DllMain调用的函数中调用GdiplusStartupGdiplusShutdown。如果您想创建一个使用GDI+的DLL,您应该使用以下技术之一来初始化GDI+:

  • 要求客户在调用您的DLL函数之前调用GdiplusStartup并在使用完毕后调用GdiplusShutdown
  • 导出自己的启动函数,该函数调用GdiplusStartup和自己的关闭函数,该函数调用GdiplusShutdown。要求客户在调用您的DLL中的其他函数之前调用您的启动函数,并在使用完毕后调用您的关闭函数。
  • 在每个调用GDI+的函数中调用GdiplusStartupGdiplusShutdown
通过将Delphi GdiPlus库编译为DLL,您违反了GdiplusStartupGdiplusShutdown的规则。这些函数在initializationfinalization部分中调用。对于库项目,单元的initializationfinalization部分中的代码从DllMain执行。
看起来,您使用的GdiPlus库从未打算用于库。但是,作为一般规则,在编写库代码时,您应该注意DllMain周围的限制,并确保您放置在initializationfinalization部分中的代码遵守这些限制。我认为这个GdiPlus库在这方面失败了。
相比之下,请查看Delphi RTL的WinApi.GDIPOBJ单元中的代码:
initialization
  if not IsLibrary then
  begin
    // Initialize StartupInput structure
    StartupInput.DebugEventCallback := nil;
    StartupInput.SuppressBackgroundThread := False;
    StartupInput.SuppressExternalCodecs   := False;
    StartupInput.GdiplusVersion := 1;

    GdiplusStartup(gdiplusToken, @StartupInput, nil);
  end;

finalization
  if not IsLibrary then
  begin
    if Assigned(GenericSansSerifFontFamily) then
      GenericSansSerifFontFamily.Free;
    if Assigned(GenericSerifFontFamily) then
      GenericSerifFontFamily.Free;
    if Assigned(GenericMonospaceFontFamily) then
      GenericMonospaceFontFamily.Free;
    if Assigned(GenericTypographicStringFormatBuffer) then
      GenericTypographicStringFormatBuffer.free;
    if Assigned(GenericDefaultStringFormatBuffer) then
      GenericDefaultStringFormatBuffer.Free;

    GdiplusShutdown(gdiplusToken);
  end;

这段代码通过确保不从DllMain中调用GdiplusStartup和GdiplusShutdown来遵守规则。相反,它将责任留给任何使用WinApi.GDIPOBJ库的库的作者,以确保在适当的时候调用GdiplusStartup和GdiplusShutdown。
如果我是你,我会选择上面列出的三个选项中的一个。其中第三个选项不太实际,但前两个选项是不错的选择。如果是我,我会选择第一个选项,并修改你的GdiPlus库中的初始化和终止代码,使其更像WinApi.GDIPOBJ中的代码。

使用您的建议更新我的答案。谢谢。 - fpiette
在这个答案中,我尝试提供一些具体的文档证据来支持任何更改。个人认为这比没有理由的代码摘录更重要。这也是我写这个答案的原因。添加文档链接并说明为什么需要进行更改。 - David Heffernan

3

GdiPlusShutdown(以及GdiPlusStartup)不能从DllMain中调用,但当调用FreeLibrary时,Windows和Delphi运行时会调用DllMain:Delphi调用DLL使用的所有单元的终止部分,而GdiPlus终止部分调用GdiPlusShutdown(在可执行文件中使用时完全可以)。初始化部分也有类似的行为。

我通过在初始化和终止部分中添加对IsLibrary的测试来避免调用有问题的函数,还添加了两个公共过程InitializeForDll和FinalizeForDll。通过这些小改动,DLL能够导出调用InitializeForDll和FinalizeForDll的函数。这些导出函数必须由托管应用程序在加载DLL后立即调用,并在卸载DLL之前调用。

以下是我对GdiPlus.pas所做的更改:

在接口部分:

var
  procedure InitializeForDll;
  procedure FinalizeForDll;

在实现部分:
procedure InitializeForDll;
begin
  Initialize;
end;

procedure FinalizeForDll;
begin
  Finalize;
end;

还更新了初始化和结束部分,如下所示:

Initialization
  if not IsLibrary then
    Initialize;

Finalization
  if not IsLibrary then
    Finalize;

在DLL中,我导出了这些函数:
procedure Initialize; stdcall;
begin
  GdiPlus.InitializeForDll;
end;

procedure Finalize; stdcall;
begin
  GdiPlus.FinalizeForDll;
end;

在调用LoadLibrary之后,初始化和完成由托管应用程序调用,就在调用FreeLibrary(或任何将加载/卸载DLL的内容)之前。

我希望这可以帮助其他人。 顺便说一句:感谢Eric Bilsen提供Delphi GdiPlus Library


2
这只是故事的一部分。即使您进行了此更改,仍然通过从初始化部分调用GdiplusStartup来违反规则。可能需要更深入地阅读文档。我已经在我的答案中尝试写出它。 - David Heffernan
我看到 GdiPlusStartup 被调用了,但没有产生不良影响。所以我只修复了失败的部分。 - fpiette
你还没有看到任何负面影响。文档在这一点上非常清楚。 - David Heffernan
更新了我的回答,修复了初始化部分。谢谢。 - fpiette

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接