You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多
文档控制台
免费开始使用

Embarcadero C++ DLL自卸载问题:第三方EXE中VCL窗体关闭后卸载失败

解决VCL子窗体DLL自卸载的Access Violation问题

这个问题我碰到过好几次了——VCL DLL的自卸载确实容易踩坑,尤其是涉及窗体生命周期的时候。你的Access Violation本质上是因为卸载时机不对:当你调用FreeLibraryAndExitThread时,宿主的主线程可能还在执行DLL里的VCL代码(比如窗体的销毁逻辑),这时候卸载DLL会导致主线程访问已经被释放的内存,直接触发违规。

核心问题分析

VCL窗体的销毁是一个异步过程:当你关闭窗体并设置Action=caFree后,VCL内部还会执行一系列清理操作(比如释放控件、处理消息队列),这段时间内宿主主线程仍然在调用DLL中的代码。如果此时在另一个线程里强行卸载DLL,主线程后续的代码执行就会访问无效内存,引发Access Violation。

可靠的解决方案

要安全实现自卸载,必须满足两个前提:

  1. 所有VCL窗体已完全销毁,相关资源清理完毕
  2. 宿主主线程已经退出DLL的所有代码执行路径

下面是具体的实现步骤和代码示例:

1. 全局状态管理与DLL初始化

首先在DLL中维护线程安全的窗体计数,并保存DLL实例句柄:

#include <vcl.h>
#include <windows.h>
#include <process.h>
#include <intrin.h>

#pragma hdrstop

HINSTANCE g_hDllInstance = nullptr;
volatile LONG g_FormCount = 0;

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
    switch (fdwReason)
    {
        case DLL_PROCESS_ATTACH:
            g_hDllInstance = hinstDLL;
            // 禁用不必要的线程通知,减少开销和潜在问题
            DisableThreadLibraryCalls(hinstDLL);
            break;
        case DLL_PROCESS_DETACH:
            // 这里不要操作VCL资源,此时VCL环境可能已销毁
            break;
    }
    return TRUE;
}

2. 窗体创建与生命周期绑定

暴露创建窗体的导出函数,并在窗体的OnCloseOnDestroy事件中处理计数和卸载触发:

extern "C" __declspec(dllexport) void CreateVCLChildForm(HWND hParentWnd)
{
    TForm* pChildForm = new TForm(nullptr);
    pChildForm->ParentWindow = hParentWnd;
    pChildForm->Caption = "VCL子窗体";
    // 绑定关闭和销毁事件
    pChildForm->OnClose = &FormCloseHandler;
    pChildForm->OnDestroy = &FormDestroyHandler;
    pChildForm->Show();
    // 线程安全地增加窗体计数
    _InterlockedIncrement(&g_FormCount);
}

void __fastcall FormCloseHandler(TObject *Sender, TCloseAction &Action)
{
    // 确保窗体被彻底销毁,而不是隐藏
    Action = caFree;
}

void __fastcall FormDestroyHandler(TObject *Sender)
{
    // 线程安全地减少计数,当最后一个窗体销毁时触发卸载
    if (_InterlockedDecrement(&g_FormCount) == 0)
    {
        // 启动独立线程执行卸载,避免在主线程的DLL代码路径中直接操作
        _beginthreadex(nullptr, 0, EjectDllThread, nullptr, 0, nullptr);
    }
}

3. 延迟卸载线程

在独立线程中等待片刻,确保主线程完全退出DLL代码路径后,再调用FreeLibraryAndExitThread

unsigned int __stdcall EjectDllThread(void* pParam)
{
    // 等待100ms,给主线程足够时间完成VCL清理并退出DLL代码
    Sleep(100);
    // 调用FreeLibraryAndExitThread自动卸载DLL并终止当前线程
    FreeLibraryAndExitThread(g_hDllInstance, 0);
    return 0;
}

额外的稳妥方案:让宿主控制卸载

如果自卸载的稳定性要求极高,建议让宿主EXE负责卸载逻辑:

  1. 在DLL中暴露一个导出函数,用于检查是否所有窗体已关闭:
extern "C" __declspec(dllexport) bool IsAllFormsClosed()
{
    return g_FormCount == 0;
}
  1. 宿主EXE在主窗体的OnIdle事件中定期检查该函数,返回true时调用FreeLibrary卸载DLL。这种方式完全由宿主控制时机,避免了DLL自卸载的线程安全风险。

关键注意事项

  • 必须使用线程安全的计数方法(_InterlockedIncrement/_InterlockedDecrement),防止多线程创建/销毁窗体时计数出错
  • 绝对不能在窗体的事件处理函数中直接调用卸载函数,必须等到窗体完全销毁后再触发
  • Sleep的时间可以根据实际情况调整,确保主线程有足够时间退出DLL代码路径

内容的提问来源于stack exchange,提问作者Ivan Ivanov

火山引擎 最新活动