Recibe advertencias del vinculador al compilar extensiones administradas para proyectos DLL de C++

En este artículo se proporciona información sobre cómo resolver las advertencias del enlazador al compilar extensiones administradas para proyectos DLL de C++.

Versión original del producto: Visual C++
Número de KB original: 814472

Síntomas

Recibirá uno de los siguientes mensajes de error en tiempo de compilación o en tiempo de vínculo:

Error de herramientas del enlazador LNK2001
'símbolo externo sin resolver "símbolo" '
Advertencia de herramientas del enlazador LNK4210
'. Existe la sección CRT; puede haber inicializaciones estáticas no controladas o teminadores'Recibe advertencias del enlazador al compilar Extensiones administradas para C++ proyectos DLL
Advertencia de herramientas del enlazador LNK4243
'DLL que contiene objetos compilados con /clr no está vinculado a /NOENTRY; es posible que la imagen no se ejecute correctamente'.

Estas advertencias pueden producirse en las siguientes circunstancias:

  • Al compilar objetos de vinculación con el modificador /clr .
  • Al compilar uno de los proyectos siguientes:
    • Plantilla de servicio web de ASP.NET
    • Plantilla de biblioteca de clases
    • Plantilla de biblioteca de controles de Windows
  • Cuando se ha agregado código que usa variables globales o clases nativas (es decir, no __gco __value) con miembros de datos estáticos. Por ejemplo, las clases Biblioteca de plantillas ActiveX (ATL), Microsoft Foundation Classes (MFC) y C Run-Time (CRT).

Nota:

Puede recibir los errores de LNK2001 y LNK4210 con proyectos que no se ven afectados por el problema descrito en este artículo. Sin embargo, el proyecto definitivamente se ve afectado por el problema descrito en este artículo si la resolución de una advertencia de LNK2001 o LNK4210 conduce a una advertencia de LNK4243, o si la vinculación del proyecto genera una advertencia de LNK4243.

Causa

Los proyectos siguientes se crean de forma predeterminada como una biblioteca de vínculos dinámicos (DLL) sin ningún vínculo a bibliotecas nativas (como CRT, ATL o MFC) y sin ninguna variable global ni clases nativas con miembros de datos estáticos:

  • Plantilla de servicio web de ASP.NET
  • Plantilla de biblioteca de clases
  • Plantilla de biblioteca de controles de Windows

Si agrega código que usa variables globales o clases nativas con miembros de datos estáticos (por ejemplo, las bibliotecas ATL, MFC y CRT usan variables globales), recibirá mensajes de error del enlazador en tiempo de compilación. Cuando esto ocurre, debe agregar código para inicializar manualmente las variables estáticas. Para obtener más información sobre cómo hacerlo, consulte la sección Resolución de este artículo.

Para mayor comodidad, en este artículo se hace referencia a variables globales y miembros de datos estáticos de clases nativas como variables estáticas o estáticas desde este punto hacia adelante.

Este problema se debe al problema de carga de ARCHIVOS DLL mixtos. Los archivos DLL mixtos (es decir, los archivos DLL que contienen código administrado y nativo) pueden encontrar escenarios de interbloqueo en algunas circunstancias cuando se cargan en el espacio de direcciones del proceso, especialmente cuando el sistema está bajo estrés. Los mensajes de error del enlazador mencionados anteriormente se habilitaron en el enlazador para asegurarse de que los clientes son conscientes de la posibilidad de interbloqueo y de las soluciones alternativas que se describen en este documento.

Solución

Las extensiones administradas para proyectos de C++ que se crean como archivos DLL de forma predeterminada no se vinculan a bibliotecas nativas de C/C++, como la biblioteca en tiempo de ejecución de C (CRT), ATL o MFC, y no usan ninguna variable estática. Además, la configuración del proyecto especifica que los archivos DLL deben vincularse con la opción /NOENTRY habilitada.

Esto se hace porque la vinculación con un punto de entrada hace que el código administrado se ejecute durante DllMain, lo que no es seguro (consulte DllMain el conjunto limitado de cosas que puede hacer durante su ámbito).

Un archivo DLL sin un punto de entrada no tiene ninguna manera de inicializar variables estáticas, excepto para tipos simples como enteros. Normalmente no tiene variables estáticas en un archivo DLL /NOENTRY .

Las bibliotecas ATL, MFC y CRT se basan en variables estáticas, por lo que tampoco puede usar estas bibliotecas desde dentro de estos archivos DLL sin realizar primero modificaciones.

Si el archivo DLL en modo mixto necesita usar estáticas o bibliotecas que dependan de estáticas (como ATL, MFC o CRT), debe modificar el archivo DLL para que se inicialice manualmente.

El primer paso para la inicialización manual es asegurarse de deshabilitar el código de inicialización automática, que no es seguro con archivos DLL mixtos y puede causar interbloqueo. Para deshabilitar el código de inicialización, siga estos pasos.

Quitar el punto de entrada del archivo DLL administrado

  1. Vínculo con /NOENTRY. En Explorador de soluciones, haga clic con el botón derecho en el nodo del proyecto y haga clic en Propiedades. En el cuadro de diálogo Páginas de propiedades , haga clic en Enlazador, en Línea de comandos y, a continuación, agregue este modificador al campo Opciones adicionales .

  2. Vincule msvcrt.lib. En el cuadro de diálogo Páginas de propiedades, haga clic en Enlazador, haga clic en Entraday, a continuación, agregue msvcrt.lib a la propiedad Dependencias adicionales .

  3. Quite nochkclr.obj. En la página Entrada (misma página que en el paso anterior), quite nochkclr.obj de la propiedad Dependencias adicionales .

  4. Vínculo en CRT. En la página Entrada (misma página que en el paso anterior), agregue __DllMainCRTStartup@12 a la propiedad Forzar referencias de símbolos .

    Si usa el símbolo del sistema, especifique la configuración del proyecto anterior con lo siguiente:

    LINK /NOENTRY msvcrt.lib /NODEFAULTLIB:nochkclr.obj /INCLUDE:__DllMainCRTStartup@12
    

Modificación de componentes que consumen el archivo DLL para la inicialización manual

Después de quitar el punto de entrada explícito, debe modificar los componentes que consumen el archivo DLL para la inicialización manual, en función de la forma en que se implemente el archivo DLL:

  • El archivo DLL se escribe mediante exportaciones DLL (__declspec(dllexport)) y los consumidores no pueden usar código administrado si están vinculados estática o dinámicamente al archivo DLL.
  • El archivo DLL es un archivo DLL basado en COM.
  • Los consumidores del archivo DLL pueden usar código administrado y el archivo DLL contiene exportaciones DLL o puntos de entrada administrados.

Modificación de archivos DLL especificados mediante exportaciones de DLL y consumidores que no pueden usar código administrado

Para modificar los archivos DLL especificados mediante exportaciones dll (__declspec(dllexport)) y consumidores que no pueden usar código administrado, siga estos pasos:

  1. Agregue dos nuevas exportaciones a la DLL, como se muestra en el código siguiente:

    // init.cpp
    #include <windows.h>
    #include <_vcclrit.h>
    // Call this function before you call anything in this DLL.
    // It is safe to call from multiple threads; it is not reference
    // counted; and is reentrancy safe.
    __declspec(dllexport) void __cdecl DllEnsureInit(void)
    {
        // Do nothing else here. If you need extra initialization steps,
        // create static objects with constructors that perform initialization.
        __crt_dll_initialize();
        // Do nothing else here.
    }
    // Call this function after this whole process is totally done
    // calling anything in this DLL. It is safe to call from multiple
    // threads; is not reference counted; and is reentrancy safe.
    // First call will terminate.
    __declspec(dllexport) void __cdecl DllForceTerm(void)
    {
        // Do nothing else here. If you need extra terminate steps, 
        // use atexit.
        __crt_dll_terminate();
        // Do nothing else here.
    }
    

    Para agregar la opción del compilador de compatibilidad con Common Language Runtime, siga estos pasos:

    1. Haga clic en Proyectoy, a continuación, haga clic en Propiedades de NombreDeProyecto.

      Nota:

      ProjectName es un marcador de posición para el nombre del proyecto.

    2. Expanda Propiedades de configuracióny, a continuación, haga clic en General.

    3. En el panel derecho, haga clic para seleccionar Compatibilidad con Common Language Runtime, Sintaxis antigua (/clr:oldSyntax) en la configuración del proyecto de soporte técnico de Common Language Runtime .

    4. En el cuadro Plan de marcado (contexto telefónico), haga clic en Examinar para buscar el plan de marcado del usuario.

    Para obtener más información sobre las opciones del compilador de compatibilidad con Common Language Runtime, vea /clr (Compilación de Common Language Runtime).

    Estos pasos se aplican a todo el artículo.

  2. El archivo DLL puede tener varios consumidores. Si tiene varios consumidores, agregue el código siguiente al archivo DLL .def en la sección exports:

    DllEnsureInitPRIVATE
    DllForceTermPRIVATE
    

    Si no agrega estas líneas y tiene dos archivos DLL que exportan funciones, la aplicación que se vincula a la DLL tendrá errores de vínculo. Normalmente, las funciones exportadas tienen los mismos nombres. En un caso multiconsumer, cada consumidor se puede vincular estática o dinámicamente al archivo DLL.

  3. Si el consumidor está vinculado estáticamente al archivo DLL, antes de usar el archivo DLL la primera vez o antes de usar cualquier cosa que dependa de él en la aplicación, agregue la siguiente llamada:

    // Snippet 1
    typedef void (__stdcall *pfnEnsureInit)(void);
    typedef void (__stdcall *pfnForceTerm)(void);
    {
        // ... initialization code
        HANDLE hDll=::GetModuleHandle("mydll.dll");
        If(!hDll)
        {
            // Exit, return; there is nothing else to do.
        }
        pfnEnsureInit pfnDll=::( pfnEnsureInit) GetProcAddress(hDll,
         "DllEnsureInit");
        if(!pfnDll)
        {
            // Exit, return; there is nothing else to do.
        }
        pfnDll();
        // ... more initialization code
    }
    
  4. Después del último uso del archivo DLL en la aplicación, agregue el código siguiente:

    // Snippet 2
    {
        // ... termination code
        HANDLE hDll=::GetModuleHandle("mydll.dll");
        If(!hDll)
        {
            // exit, return; there is nothing else to do
        }
        pfnForceTerm pfnDll=::( pfnForceTerm) GetProcAddress(hDll,
         "DllForceTerm");
        if(!pfnDll)
        {
            // exit, return; there is nothing else to do
        }
        pfnDll();
        // ... more termination code
    }
    
  5. Si el consumidor está vinculado dinámicamente al archivo DLL, inserte código como se indica a continuación:

    • Inserte el fragmento de código 1 (vea el paso 3) inmediatamente después de la primera LoadLibrary para el archivo DLL.
    • Inserte el fragmento de código 2 (vea el paso 4) inmediatamente antes de la última FreeLibrary para el archivo DLL.

Modificar archivos DLL basados en COM

Modifique las funciones DllCanUnloadNowde exportación de DLL , DllGetClassObject, DllRegisterServery DllUnregisterServer como se muestra en el código siguiente:

// Implementation of DLL Exports.
#include <_vcclrit.h>
STDAPI DllCanUnloadNow(void)
{
    if ( _Module.GetLockCount() == 0 )
    {
        __crt_dll_terminate();
        return S_OK;
    }
    else
    {
        return S_FALSE;
    }
}

STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv)
{
    if ( !( __crt_dll_initialize()) )
    {
        return E_FAIL;
    }
    else
    {
        return _Module.GetClassObject(rclsid, riid, ppv);
    }
}

STDAPI DllRegisterServer(void)
{
    if ( !( __crt_dll_initialize()) )
    {
        return E_FAIL;
    }
    // Call your registration code here
    HRESULT hr = _Module.RegisterServer(TRUE)
    return hr;
}

STDAPI DllUnregisterServer(void)
{
    HRESULT hr = S_OK;
    __crt_dll_terminate();
    // Call your unregistration code here
    hr = _Module.UnregisterServer(TRUE);
    return hr;
}

Modificación del archivo DLL que contiene consumidores que usan código administrado y exportaciones dll o puntos de entrada administrados

Para modificar el archivo DLL que contiene consumidores que usan código administrado y exportaciones dll o puntos de entrada administrados, siga estos pasos:

  1. Implemente una clase administrada con funciones miembro estáticas para la inicialización y terminación. Agregue un archivo .cpp al proyecto, implementando una clase administrada con miembros estáticos para la inicialización y terminación:

    // ManagedWrapper.cpp
    // This code verifies that DllMain is not automatically called
    // by the Loader when linked with /noentry. It also checks some
    // functions that the CRT initializes.
    
    #include <windows.h>
    #include <stdio.h>
    #include <string.h>
    #include <stdlib.h>
    #include <math.h>
    #include "_vcclrit.h"
    
    #using <mscorlib.dll>
    using namespace System;
    public __gc class ManagedWrapper
    {
        public:
        static BOOL minitialize()
        {
            BOOL retval = TRUE;
            try
            {
                retval = __crt_dll_initialize();
            } catch(System::Exception* e)
            {
                Console::WriteLine(e->Message);
                retval = FALSE;
            }
            return retval;
        }
        static BOOL mterminate()
        {
            BOOL retval = TRUE;
            try
            {
                retval = __crt_dll_terminate();
            } catch(System::Exception* e)
            {
                Console::WriteLine(e->Message);
                retval = FALSE;
            }
            return retval;
        }
    };
    
    BOOL WINAPI DllMain(HINSTANCE hModule, DWORD dwReason, LPVOID
    lpvReserved)
    {
        Console::WriteLine(S"DllMain is called...");
        return TRUE;
    } /* DllMain */
    
  2. Llame a estas funciones antes de hacer referencia al archivo DLL y una vez que haya terminado de usarlo. Llame a las funciones miembro de inicialización y terminación en main:

    // Main.cpp
    
    #using <mscorlib.dll>
    using namespace System;
    using namespace System::Reflection;
    #using "ijwdll.dll";
    
    int main()
    {
        int retval = ManagedWrapper::minitialize();
        ManagedWrapper::mterminate();
    }