Поведение библиотек DLL и библиотеки среды выполнения Visual C++

При создании динамической библиотеки связей (DLL) в Visual Studio компоновщик по умолчанию включает в себя библиотеку среды выполнения Visual C++ (VCRuntime). VCRuntime содержит код, необходимый для инициализации и завершения исполняемого файла C/C++. При связывании с библиотекой DLL код VCRuntime предоставляет внутреннюю функцию точки входа DLL, называемую _DllMainCRTStartup, которая обрабатывает сообщения ОС Windows для библиотеки DLL по присоединению к процессу или потоку или отсоединению от процесса или потока. Функция _DllMainCRTStartup выполняет основные задачи, такие как настройка безопасности буфера стека, инициализация и завершение библиотеки среды выполнения C (CRT), а также вызовы конструкторов и деструкторов для статических и глобальных объектов. _DllMainCRTStartup также вызывает функции-ловушки для других библиотек, таких как WinRT, MFC и ATL, для выполнения их собственных операций инициализации и завершения. Без этой инициализации CRT и другие библиотеки, а также статические переменные останутся в неинициализированном состоянии. Внутренние процедуры инициализации и завершения VCRuntime вызываются одинаково, независимо от того, использует ли ваша DLL статически связанную библиотеку CRT или динамически связанную библиотеку CRT DLL.

Точка входа DLL по умолчанию — _DllMainCRTStartup

В Windows все библиотеки DLL могут содержать необязательную функцию точки входа, обычно называемую DllMain, которая вызывается как для инициализации, так и для завершения. Это дает возможность выделить или освободить другие ресурсы по мере необходимости. Windows вызывает функцию точки входа в четырех ситуациях: присоединение процесса, отсоединение процесса, присоединение потока и отсоединение потока. При загрузке библиотеки DLL в адресное пространство процесса либо при загрузке приложения, использующего его, либо при запросе библиотеки DLL во время выполнения операционная система создает отдельную копию данных DLL. Это называется присоединением процесса. Присоединение потока происходит, когда процесс, в который загружена библиотека DLL, создает новый поток. Отсоединение потока происходит при завершении потока. Отсоединение процесса происходит, когда библиотека DLL больше не нужна и освобождается приложением. Операционная система выполняет отдельный вызов точки входа библиотеки DLL для каждого из этих событий, передавая аргумент reason для каждого типа события. Например, операционная система отправляет DLL_PROCESS_ATTACH в качестве аргумента reason для сигнализирования присоединения процесса.

Библиотека VCRuntime предоставляет функцию точки входа с именем _DllMainCRTStartup для обработки операций инициализации и завершения по умолчанию. При присоединении процесса _DllMainCRTStartup функция настраивает проверки безопасности буфера, инициализирует CRT и другие библиотеки, инициализирует сведения о типах среды выполнения, инициализирует и вызывает конструкторы для статических и нелокальных данных, инициализирует локальное хранилище потока, увеличивает внутренний статический счетчик для каждого присоединения, а затем вызывает DllMain, предоставленные пользователем или библиотекой. При отсоединении процесса функция выполняет эти действия в обратном порядке. Она вызывает DllMain, уменьшает показатель внутреннего счетчика, вызывает деструкторы, вызывает функции завершения CRT и зарегистрированные функции atexit и уведомляет о завершении все другие библиотеки. Когда показатель счетчика присоединений достигает нуля, функция возвращает FALSE, чтобы указать Windows на возможность выгрузки DLL. Функция _DllMainCRTStartup также вызывается при присоединении и отсоединении потока. В таких случаях код VCRuntime не выполняет никакой иной инициализации или завершения самостоятельно, а просто вызывает DllMain, чтобы передать сообщение дальше. Если DllMain возвращает FALSE из присоединения процесса, что говорит о сбое, _DllMainCRTStartup вызывает DllMain снова и передает DLL_PROCESS_DETACH в качестве аргумента reason, а затем выполняет оставшуюся часть процесса завершения.

При создании библиотек DLL в Visual Studio точка _DllMainCRTStartup входа по умолчанию, предоставляемая VCRuntime, автоматически связана. Не нужно указывать функцию точки входа вашей библиотеки DLL с помощью параметра компоновщика /ENTRY (символ точки входа)

Примечание.

Хотя возможно указать другую функцию точки входа для библиотеки DLL, используя /ENTRY параметр компоновщика, мы не рекомендуем этого делать, так как функция точки входа должна будет точно повторить все, что делает _DllMainCRTStartup, в том же порядке. VCRuntime предоставляет функции, позволяющие повторять его поведение. Например, можно вызвать __security_init_cookie сразу же при присоединении процесса для поддержки параметра проверки буфера /GS (проверка безопасности буфера). Можно вызвать функцию _CRT_INIT, передавая те же параметры, что и функция точки входа, для выполнения остальных функций инициализации или завершения DLL.

Инициализация библиотеки DLL

Ваша DLL может содержать код инициализации, который должен выполняться при её загрузке. Для выполнения ваших собственных функций инициализации и завершения библиотеки DLL _DllMainCRTStartup вызывает функцию с именем DllMain, которую вы сами можете указать. У DllMain должна быть сигнатура, необходимая для точки входа динамической библиотеки (DLL). Функция точки входа по умолчанию _DllMainCRTStartup вызывает DllMain с использованием тех же параметров, передаваемых Windows. По умолчанию, если вы не предоставляете DllMain функцию, Visual Studio предоставляет её и подключает так, чтобы у _DllMainCRTStartup всегда была вызываемая функция. Это означает, что если вам не нужно инициализировать библиотеку DLL, при сборке библиотеки DLL вам ничего не нужно делать.

Следующая сигнатура используется для DllMain:

#include <windows.h>

extern "C" BOOL WINAPI DllMain (
    HINSTANCE const instance,  // handle to DLL module
    DWORD     const reason,    // reason for calling function
    LPVOID    const reserved); // reserved

Некоторые библиотеки оборачивают функцию DllMain для вас. Например, в обычной библиотеке DLL MFC реализуйте функции-члены CWinApp и InitInstance объекта ExitInstance для выполнения операций инициализации и завершения, требуемых библиотекой DLL. Дополнительные сведения см. в разделе "Инициализация обычных библиотек DLL MFC ".

Предупреждение

В отношении операций, выполняемых в точке входа DLL, действуют серьезные ограничения. Дополнительные сведения о конкретных API Windows, которые небезопасны для вызова, см. в DllMainразделе "Общие рекомендации". Если вам нужно что-либо, кроме простейшей инициализации, выполните это в функции инициализации библиотеки DLL. Можно потребовать, чтобы приложения вызывали функцию инициализации после выполнения DllMain и перед вызовом любых других функций в библиотеке DLL.

Инициализация обычных (не MFC) библиотек DLL

Чтобы выполнить собственную инициализацию в обычных (не MFC) библиотеках DLL, использующих предоставленную VCRuntime точку входа _DllMainCRTStartup, исходный код библиотеки DLL должен содержать функцию с именем DllMain. Следующий код представляет собой базовую схему, показывающую, как может выглядеть определение DllMain:

#include <windows.h>

extern "C" BOOL WINAPI DllMain (
    HINSTANCE const instance,  // handle to DLL module
    DWORD     const reason,    // reason for calling function
    LPVOID    const reserved)  // reserved
{
    // Perform actions based on the reason for calling.
    switch (reason)
    {
    case DLL_PROCESS_ATTACH:
        // Initialize once for each new process.
        // Return FALSE to fail DLL load.
        break;

    case DLL_THREAD_ATTACH:
        // Do thread-specific initialization.
        break;

    case DLL_THREAD_DETACH:
        // Do thread-specific cleanup.
        break;

    case DLL_PROCESS_DETACH:
        // Perform any necessary cleanup.
        break;
    }
    return TRUE;  // Successful DLL_PROCESS_ATTACH.
}

Примечание.

Старая документация SDK для Windows говорит, что фактическое имя функции точки входа DLL должно быть указано в командной строке компоновщика с параметром /ENTRY. В Visual Studio не нужно использовать /ENTRY параметр, если имя функции точки входа имеет значение DllMain. На самом деле, если вы используете параметр /ENTRY и назовёте вашу функцию точки входа чем-то отличным от DllMain, CRT не будет правильно инициализирован, если ваша функция точки входа не выполнит те же вызовы инициализации, которые делает _DllMainCRTStartup.

Инициализация CRT вручную с помощью _CRT_INIT

При создании DLL, которая использует любую из библиотек среды выполнения C, чтобы убедиться, что CRT правильно инициализирован, следует:

  • Функция инициализации должна быть названа DllMain(), а точка входа должна быть указана с параметром -entry:_DllMainCRTStartup@12 компоновщика. Это поведение по умолчанию при создании библиотеки DLL (@12 предназначен только для платформы x86, так как эта платформа использует stdcall), или
  • Точка входа библиотеки DLL должна явно вызывать _CRT_INIT() при подключении к процессу и при отсоединении от процесса. Это важно только в том случае, если вы используете /NOENTRY или имеете пользовательскую точку входа. По возможности не рекомендуется использовать /NOENTRY или пользовательскую точку входа, так как необходимо дублировать весь код инициализации и завершения в том же порядке, в котором он предоставляется _DllMainCRTStartup.

Это позволяет библиотекам среды выполнения C правильно выделять и инициализировать данные среды выполнения C при подключении процесса или потока к библиотеке DLL, правильно очищать данные среды выполнения C, когда процесс отсоединяется от библиотеки DLL, а также для глобальных объектов C++ в библиотеке DLL для правильного создания и деструктирования.

Примеры пакета SDK Win32 используют первый метод. Дополнительные сведения см. в Руководстве программиста Win32 для DllEntryPoint() и документации по Visual C++ для DllMain(). DllMainCRTStartup() вызывает _CRT_INIT(), а _CRT_INIT() вызывает компонент вашего приложения DllMain(), если он существует.

Если вы хотите использовать второй метод и вызвать код инициализации CRT самостоятельно, вместо использования DllMainCRTStartup() и DllMain()есть два метода:

  1. Если у вас есть собственная точка входа DLL, выполните указанные ниже действия в точке входа:

    a. Используйте этот прототип (определенный в process.h при определении _DECL_DLLMAIN) для _CRT_INIT(): BOOL WINAPI _CRT_INIT(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved);

    Сведения о _CRT_INIT() возвращаемых значениях см. в документации по DllEntryPoint; возвращаются те же значения.

    б. DLL_PROCESS_ATTACH и DLL_THREAD_ATTACH вызовите _CRT_INIT() сначала, прежде чем будут вызваны какие-либо функции среды выполнения C или выполнены операции с плавающей запятой.

    с. Вызовите код инициализации/завершения собственных процессов и потоков.

    d. После DLL_PROCESS_DETACH и DLL_THREAD_DETACH вызовите _CRT_INIT() в конце, после вызова всех функций среды выполнения C и завершения всех операций с плавающей запятой.

Обязательно передайте _CRT_INIT() все параметры точки входа; _CRT_INIT() ожидает эти параметры, поэтому вещи могут не работать надежно, если они опущены (в частности, fdwReason необходимо определить, требуется ли инициализация процесса или завершение).

Ниже приведена схема функции точки входа, показывающая, когда и как выполнять эти вызовы _CRT_INIT() в точке входа DLL:

BOOL WINAPI DllEntryPoint(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved)
{
    if (fdwReason == DLL_PROCESS_ATTACH || fdwReason == DLL_THREAD_ATTACH)
        if (!_CRT_INIT(hinstDLL, fdwReason, lpReserved))
            return(FALSE);

    if (fdwReason == DLL_PROCESS_DETACH || fdwReason == DLL_THREAD_DETACH)
        if (!_CRT_INIT(hinstDLL, fdwReason, lpReserved))
            return(FALSE);

    return(TRUE);
}

Примечание.

Это не обязательно, если вы используете DllMain() и -entry:_DllMainCRTStartup@12.

Инициализация обычных библиотек DLL MFC

Так как обычные библиотеки DLL MFC содержат объект CWinApp, они должны выполнять задачи инициализации и завершения в том же расположении, что и приложение MFC: в функциях-членах InitInstance и ExitInstance класса, производного от CWinApp. Так как MFC предоставляет DllMain функцию, которая вызывается _DllMainCRTStartup, DLL_PROCESS_ATTACH и DLL_PROCESS_DETACH, вам не следует писать собственную DllMain функцию. Предоставляемая MFC функция DllMain вызывает InitInstance при загрузке библиотеки DLL и ExitInstance перед выгрузкой библиотеки DLL.

Обычная библиотека DLL MFC может отслеживать нескольких потоков, вызывая TlsAlloc и TlsGetValue в своей функции InitInstance. Эти функции позволяют библиотеке DLL отслеживать данные конкретного потока.

В обычной библиотеке MFC DLL, которая динамически связывается с MFC, если вы используете поддержку MFC OLE, MFC Database (или DAO) или MFC Sockets, отладочные DLL расширения MFC, такие как MFCOверсияD.dll, MFCDверсияD.dll и MFCNверсияD.dll (где версия – номер версии), подключаются автоматически. Необходимо вызвать одну из следующих предопределенных функций инициализации для каждой из этих библиотек DLL, которые вы используете в вашей обычной библиотеке DLL MFC CWinApp::InitInstance.

Тип поддержки MFC Функция инициализации для вызова
MFC OLE (MFCOверсияD.dll) AfxOleInitModule
База данных MFC (MFCDверсияD.dll) AfxDbInitModule
Сокеты MFC (MFCNверсияD.dll) AfxNetInitModule

Инициализация библиотек DLL расширения MFC

Так как расширяемые библиотеки DLL MFC не имеют производных объектов CWinApp (в отличие от обычных библиотек DLL MFC), необходимо добавить код инициализации и завершения в функцию DllMain, которую создает мастер MFC DLL.

Мастер предоставляет следующий код для библиотек DLL расширения MFC. В коде PROJNAME является заполнителем для имени вашего проекта.

#include "pch.h" // For Visual Studio 2017 and earlier, use "stdafx.h"
#include <afxdllx.h>

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
static AFX_EXTENSION_MODULE PROJNAMEDLL;

extern "C" int APIENTRY
DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved)
{
   if (dwReason == DLL_PROCESS_ATTACH)
   {
      TRACE0("PROJNAME.DLL Initializing!\n");

      // MFC extension DLL one-time initialization
      AfxInitExtensionModule(PROJNAMEDLL,
                                 hInstance);

      // Insert this DLL into the resource chain
      new CDynLinkLibrary(Dll3DLL);
   }
   else if (dwReason == DLL_PROCESS_DETACH)
   {
      TRACE0("PROJNAME.DLL Terminating!\n");
   }
   return 1;   // ok
}

При создании CDynLinkLibrary объекта во время инициализации библиотека DLL расширения MFC может экспортировать объекты CRuntimeClass или ресурсы в клиентское приложение.

Если вы собираетесь использовать вашу библиотеку DLL расширения MFC из одной или нескольких стандартных библиотек DLL MFC, необходимо экспортировать функцию инициализации, которая создает объект CDynLinkLibrary. Эту функцию необходимо вызывать из каждой стандартной библиотеки DLL MFC, которая использует библиотеку DLL расширения MFC. Подходящим местом для вызова этой функции инициализации является функция-член InitInstance объекта библиотеки DLL MFC, производного от CWinApp. Вызов следует выполнить до использования экспортированных классов или функций библиотеки DLL расширения MFC.

В DllMain, который генерирует мастер DLL MFC, вызов AfxInitExtensionModule фиксирует классы среды выполнения модуля (CRuntimeClass структуры) и его фабрики объектов (COleObjectFactory объекты) для использования при создании объекта CDynLinkLibrary. Проверьте возвращаемое значение AfxInitExtensionModule; если из AfxInitExtensionModule возвращается ноль, верните ноль из вашей функции DllMain.

Если библиотека DLL расширения MFC явно связана с исполняемым файлом (то есть исполняемый файл вызывает AfxLoadLibrary для связи с библиотекой DLL), необходимо добавить вызов AfxTermExtensionModule на DLL_PROCESS_DETACH. Эта функция позволяет MFC очищать библиотеку DLL расширения MFC при отсоединении каждого процесса от библиотеки DLL расширения MFC (что происходит при завершении процесса или при выгрузке библиотеки DLL в результате вызова AfxFreeLibrary). Если библиотека DLL расширения MFC связана неявно с приложением, вызов AfxTermExtensionModule не требуется.

Приложения, которые подключаются к расширяющим DLL-библиотекам MFC явно, должны вызывать AfxTermExtensionModule при освобождении этой библиотеки. Если приложения используют несколько потоков, они также должны использовать AfxLoadLibrary и AfxFreeLibrary (вместо функций Win32 LoadLibrary и FreeLibrary). Использование AfxLoadLibrary и AfxFreeLibrary обеспечивает, что код запуска и завершения, выполняемый при загрузке и выгрузке библиотеки DLL расширения MFC, не повреждает глобальное состояние MFC.

Так как MFCx0.dll полностью инициализируется к моменту вызова DllMain, можно выделить память и вызвать функции MFC в DllMain (в отличие от 16-разрядной версии MFC).

Дополнительные библиотеки DLL могут заниматься управлением многопоточностью, обрабатывая случаи DLL_THREAD_ATTACH и DLL_THREAD_DETACH в функции DllMain. Эти случаи передаются в DllMain при присоединении и отсоединении потоков от библиотеки DLL. Вызов TlsAlloc при присоединении DLL позволяет DLL поддерживать индексы локального хранилища потоков (TLS) для каждого потока, подключенного к DLL.

Файл Afxdllx.h заголовка содержит особые определения для структур, используемых в расширенных DLL MFC, например, определения AFX_EXTENSION_MODULE и CDynLinkLibrary. Включите этот заголовочный файл в вашу библиотеку DLL расширения MFC.

Примечание.

Важно не определять и не разопределять макросы _AFX_NO_XXX в pch.h (stdafx.h в Visual Studio 2017 и более ранних версиях). Эти макросы существуют только для проверки того, поддерживает ли конкретная целевая платформа эту функцию или нет. Можно написать собственную программу для проверки этих макросов (например, #ifndef _AFX_NO_OLE_SUPPORT), но она не должна определять и отменять определение этих макросов.

Пример функции инициализации, обрабатывающей многопоточность, включен в раздел Использование локального хранилища потока в библиотеке динамической компоновки в Windows SDK. Обратите внимание, что пример содержит функцию точки входа, называемую LibMain, но вы должны назвать эту функцию DllMain, чтобы она работала с библиотеками среды выполнения MFC и C.

См. также

Создание библиотек DLL на C и C++ в Visual Studio
Точка входа DllMain
Рекомендации по использованию динамических библиотек (DLL)