【问题标题】:Program hangs when unloading a DLL making use of GdiPlus使用 GdiPlus 卸载 DLL 时程序挂起
【发布时间】:2016-01-19 08:00:56
【问题描述】:

我有一个应用程序加载一个使用Delphi GDI+ Library 的DLL。此应用程序在卸载 DLL 时挂起(调用 FreeLibrary)。

我将问题追踪到 GdiPlus.pas 单元完成部分,该部分调用 GdiPlusShutdown,但永远不会返回。

如何避免这种死锁?

【问题讨论】:

    标签: delphi dll gdi+


    【解决方案1】:

    GdiplusStartup 函数的文档是这样说的:

    请勿在DllMain 或任何电话中致电GdiplusStartupGdiplusShutdown DllMain 调用的函数。如果你想创建一个 DLL 使用 GDI+,您应该使用以下技术之一 初始化 GDI+:

    • 要求您的客户在调用 DLL 中的函数之前调用 GdiplusStartup,并在调用时调用 GdiplusShutdown 使用完您的 DLL。
    • 导出您自己的调用GdiplusStartup 的启动函数和您自己的调用GdiplusShutdown 的关闭函数。要求您的客户 在他们调用你的其他函数之前调用你的启动函数 DLL 并在它们完成使用后调用您的关闭函数 你的 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 调用GdiplusStartupGdiplusShutdown。相反,它让任何使用 WinApi.GDIPOBJ 的库的作者承担责任,以确保在适当的时间调用 GdiplusStartupGdiplusShutdown

    如果我是你,我会选择上面列出的三个要点选项之一。这些选项中的第三个不是很实用,但前两个是不错的选择。如果是我,我会选择第一个选项并修改GdiPlus 库中的initializationfinalization 代码,使其看起来更像WinApi.GDIPOBJ 中的代码。

    【讨论】:

    • 根据您的建议更新我的答案。谢谢。
    • 我已经尝试在这个答案中提供一些具体的文件证据来支持任何更改。我个人认为这比没有正当理由的代码摘录更重要。这就是我写这个答案的真正原因。添加文档链接并说明您需要进行更改的原因。
    【解决方案2】:

    GdiPlusShutdown(和 GdiPlusStartup btw)不能从 DllMain 调用,但是当调用 FreeLibrary 时,DllMain 由 Windows 和 Delphi 运行时调用: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;
    

    Initialize 和 Finalize 由宿主应用程序在调用 LoadLibrary 之后和调用 FreeLibrary(或任何将加载/卸载 DLL 的方法)之前调用。

    我希望这对其他人有所帮助。 btw:感谢Eric Bilsen提供Delphi GdiPlus Library

    【讨论】:

    • 这只是故事的一部分。即使您进行了此更改,您仍然违反了从初始化部分调用GdiplusStartup 的规则。可能需要更深入地阅读文档。我已尝试将其写在我的答案中。
    • 我已经看到调用了 GdiPlusStartup 但它没有任何不良影响。所以我只修复了失败的地方。
    • 您还没有看到不良影响。文档在这一点上非常清楚。
    • 更新了我的答案以修复初始化部分。谢谢。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-02-08
    • 1970-01-01
    • 1970-01-01
    • 2012-02-23
    • 2017-08-09
    • 1970-01-01
    • 2011-07-12
    相关资源
    最近更新 更多