Inicialización de ensamblados mixtos

Actualización: noviembre 2007

En Visual C++ .NET y Visual C++ 2003, los archivos DLL compilados con la opción del compilador /clr se podían interbloquear de manera no determinista al cargarse; este problema se conocía como carga de archivo DLL mixto o interbloqueo del cargador. En Visual C++ 2005, se ha eliminado prácticamente todo no determinismo del proceso de carga de archivos DLL mixtos. Sin embargo, quedan algunos casos en los que se puede producir (de manera determinista) un bloqueo del cargador. Para obtener más información sobre este problema, vea "Mixed DLL Loading Problem" en MSDN Library.

En Visual C++ 2005, todavía se mantiene una restricción consistente en que el código de DllMain no debe tener acceso al CLR. Es decir, DllMain no debe hacer llamadas a funciones administradas, directa o indirectamente; no se debe declarar ni implementar código administrado en DllMain, ni deben recolectarse elementos no utilizados ni cargarse bibliotecas automáticamente en DllMain.

Nota:

Visual C++ 2003 proporcionaba _vcclrit.h para facilitar la inicialización del archivo DLL minimizando la posibilidad de interbloqueo. Para Visual C++ 2005, el uso de _vcclrit.h ya no es necesario, y provoca la aparición de advertencias de desaprobación durante la compilación. La estrategia recomendada es quitar las dependencias de este archivo mediante los pasos descritos en Cómo: Quitar dependencias en _vcclrit.h. Algunas soluciones menos ideales son la supresión de las advertencias definiendo _CRT_VCCLRIT_NO_DEPRECATE antes de incluir _vcclrit.h, o simplemente, omitiendo las advertencias de desaprobación.

Causas de interbloqueo del cargador

Con la introducción de la plataforma .NET, hay dos mecanismos diferentes para cargar un módulo de ejecución (EXE o DLL): uno para Windows, que se utiliza para módulos no administrados, y otro para Common Language Runtime (CLR) de .NET, que carga ensamblados de .NET. El problema de carga de archivos DLL mixtos se centra en el cargador del sistema operativo Microsoft Windows.

Cuando un ensamblado que sólo contiene construcciones .NET se carga en un proceso, el propio cargador CLR puede llevar a cabo todas las tareas de carga e inicialización necesarias. Sin embargo, para los ensamblados mixtos, dado que pueden contener código nativo y datos, también se debe utilizar el cargador de Windows.

El cargador de Windows garantiza que ningún código tenga acceso a código o datos en esa DLL antes de que se haya inicializado, y que ningún código pueda cargar innecesariamente dicha DLL mientras se inicializa parcialmente. Para ello, el cargador de Windows utiliza una sección crítica global del proceso (que a menudo se denomina "bloqueo del cargador"), que evita el acceso no seguro durante la inicialización del módulo. Como resultado, el proceso de carga es vulnerable a muchos escenarios clásicos de interbloqueo. Si se trata de ensamblados mixtos, los dos escenarios siguientes aumentan el riesgo de interbloqueo:

  • En primer lugar, si los usuarios tratan de ejecutar funciones compiladas en lenguaje intermedio de Microsoft (MSIL) mientras se mantiene el bloqueo del cargador (desde DllMain o en inicializadores estáticos, por ejemplo), se puede producir un interbloqueo. Considere el caso en el que la función MSIL hace referencia a un tipo en un ensamblado que no se ha cargado. El CLR intentará cargar automáticamente ese ensamblado, lo que puede requerir que el cargador de Windows se bloquee en el bloqueo del cargador. Puesto que el código ya contiene el bloqueo del cargador previamente en la secuencia de llamada, se produce un interbloqueo. No obstante, ejecutar MSIL con bloqueo del cargador no garantiza que se vaya a producir un interbloqueo; por tanto, este escenario resulta difícil de diagnosticar y corregir. En algunos casos, como cuando la DLL del tipo de referencia no contiene construcciones nativas y ninguna de sus dependencias tampoco las contiene, no se requiere el cargador de Windows para cargar el ensamblado de .NET del tipo de referencia. Además, otro código puede haber cargado ya el ensamblado necesario o sus dependencias de .NET o nativas mixtas. En consecuencia, el interbloqueo puede ser difícil de predecir y puede variar dependiendo de la configuración del equipo de destino.

  • En segundo lugar, al cargar archivos DLL en las versiones 1.0 y 1.1 de .NET Framework, el CLR suponía que no se mantenía el bloqueo del cargador y llevaba a cabo varias acciones que no son válidas si existe bloqueo del cargador. Pensar que no se mantiene el bloqueo del cargador es una suposición válida para archivos DLL de .NET pero, dado que los archivos DLL mixtos ejecutan rutinas de inicialización nativas, requieren el cargador nativo de Windows y, por tanto, el bloqueo del cargador. En consecuencia, aunque el desarrollador no trataba de ejecutar funciones MSIL durante la inicialización del archivo DLL, aún existía una pequeña posibilidad de interbloqueo no determinista con las versiones 1.0 y 1.1 de .NET Framework.

En Visual C++ 2005, se ha eliminado todo no determinismo del proceso de carga de archivos DLL mixtos. Se ha conseguido con estos cambios:

  • El CLR ya no hace suposiciones falsas al cargar archivos DLL mixtos.

  • La inicialización no administrada y administrada se realiza en dos fases separadas y distintas. La inicialización no administrada tiene lugar primero (a través de DllMain) y la inicialización administrada se produce después, a través de una construcción admitida por .NET denominada .cctor. El último es completamente transparente al usuario a menos que se utilice /Zl o /NODEFAULTLIB. Para obtener más información, vea /NODEFAULTLIB (Omitir bibliotecas) y /Zl (Omitir nombres de biblioteca predeterminada).

Todavía puede producirse un bloqueo del cargador, pero ahora tiene lugar de forma reproducible y se detecta. Si DllMain contiene instrucciones MSIL, el compilador generará la advertencia Advertencia del compilador (nivel 1) C4747. Además, el CRT o el CLR intentará detectar y comunicar los intentos de ejecutar MSIL con bloqueo del cargador. La detección del CRT produce el error en tiempo de ejecución de C R6033 del diagnóstico en tiempo de ejecución.

El resto de este documento describe los otros escenarios para los que se puede ejecutar MSIL con bloqueo del cargador, soluciones para el problema en cada uno de ellos y técnicas de depuración.

Escenarios y soluciones

Hay varias situaciones diferentes en las cuales el código de usuario puede ejecutar MSIL si existe bloqueo del cargador. El desarrollador debe garantizar que la implementación del código de usuario no intenta ejecutar las instrucciones MSIL bajo ninguna de estas circunstancias. Las subsecciones siguientes detallan todas las posibilidades con una descripción de cómo resolver los problemas en los casos más comunes.

  • DllMain

  • Inicializadores estáticos

  • Funciones proporcionadas por el usuario que influyen en el inicio

  • Configuraciones regionales personalizadas

DllMain

La función DllMain es un punto de entrada definido por el usuario para un archivo DLL. A menos que el usuario especifique lo contrario, se invoca DllMain cada vez que un proceso o subproceso se asocie al archivo DLL que lo contiene o se desasocie de él. Puesto que esta invocación se puede producir mientras se mantiene el bloqueo del cargador, ninguna función DllMain proporcionada por el usuario se debería compilar en MSIL. Además, ninguna función del árbol de llamadas con raíz en DllMain se puede compilar en MSIL. Para resolver estos problemas, el bloque de código que define DllMain debe modificarse con una directiva #pragma unmanaged. Lo mismo se debe hacer con todas las funciones que a las que llame DllMain.

En los casos en que estas funciones deban llamar a una función que requiera una implementación MSIL para otros contextos de llamada, se puede aplicar una estrategia de duplicación donde se cree una versión .NET y una nativa de la misma función.

Otra posibilidad, si no se requiere DllMain o si no es necesaria su ejecución con bloqueo del cargador, es suprimir la implementación de DllMain proporcionada por el usuario, lo que eliminará el problema.

Si DllMain intenta ejecutar MSIL directamente, se provocará Advertencia del compilador (nivel 1) C4747. Sin embargo, el compilador no puede detectar los casos en los que DllMain llame a una función en otro módulo que a su vez intenta ejecutar MSIL.

Consulte "Impedimentos de diagnóstico" para obtener más información sobre este escenario.

Inicializar objetos estáticos

Al inicializar objetos estáticos, se puede producir interbloqueo si se requiere un inicializador dinámico. En los casos sencillos, como cuando una variable estática se asigna a un valor conocido en tiempo de compilación, no se requiere inicialización dinámica; por tanto, no hay riesgo de interbloqueo. No obstante, las variables estáticas inicializadas mediante llamadas a funciones, las invocaciones a constructores o las expresiones que no se pueden evaluar en tiempo de compilación requieren la ejecución de código durante la inicialización del módulo.

El código siguiente muestra ejemplos de inicializadores estáticos que requieren inicialización dinámica: la llamada a una función, la construcción de un objeto y la inicialización de un puntero. (Estos ejemplos no son estáticos, pero se supone que están definidos en el ámbito global, que tiene el mismo efecto.)

// dynamic initializer function generated
int a = init();
CObject o(arg1, arg2);  
CObject* op = new CObject(arg1, arg2);

Este riesgo de interbloqueo depende de si el módulo contenedor se compila con el modificador /clr y de si se va a ejecutar MSIL. Específicamente, si la variable estática se compila sin /clr (o reside en un bloque #pragma unmanaged) y el inicializador dinámico requerido para inicializarla provoca la ejecución de instrucciones MSIL, se puede producir un interbloqueo. Esto se debe a que, para los módulos compilados sin /clr, DllMain realiza la inicialización de las variables estáticas. Por el contrario, las variables estáticas compiladas con /clr se inicializan mediante .cctor, una vez completada la etapa de inicialización sin administrar y liberado el bloqueo del cargador.

Existen varias soluciones para el interbloqueo causado por la inicialización dinámica de variables estáticas (más o menos organizadas según el tiempo necesario para corregir el problema):

  • El archivo de código fuente que contiene la variable estática se puede compilar con /clr.

  • Todas las funciones a las que llama la variable estática pueden compilarse en código nativo utilizando la directiva #pragma unmanaged.

  • Clonar manualmente el código del que depende la variable estática, proporcionando una versión .NET y una nativa con nombres diferentes. Después, los desarrolladores pueden llamar a la versión nativa desde inicializadores estáticos nativos y llamar a la versión de .NET en otra ubicación.

Funciones proporcionadas por el usuario que influyen en el inicio

Hay varias funciones proporcionadas por el usuario de las que dependen las bibliotecas para la inicialización durante el inicio. Por ejemplo, cuando se sobrecargan globalmente operadores en C++ como new y delete, las versiones proporcionadas por el usuario se utilizan en todos los casos, incluidas la inicialización y la destrucción de STL. Por consiguiente, la STL y los inicializadores estáticos proporcionados por el usuario invocarán cualquier versión proporcionada por el usuario de estos operadores.

Si las versiones proporcionadas por el usuario se compilan en MSIL, estos inicializadores tratarán de ejecutar instrucciones MSIL mientras se mantiene el bloqueo del cargador. Un malloc proporcionado por el usuario tiene las mismas consecuencias. Para resolver este problema, cualquiera de estas sobrecargas o definiciones proporcionadas por el usuario debe implementarse como código nativo utilizando la directiva #pragma unmanaged.

Consulte "Impedimentos de diagnóstico" para obtener más información sobre este escenario.

Configuraciones regionales personalizadas

Si el usuario proporciona una configuración regional global personalizada, ésta se utilizará para inicializar todas las secuencias de E/S posteriores, incluidas las que se inicializan estáticamente. Si este objeto de configuración regional local se compila en MSIL, se podrán invocar funciones miembro de objetos de configuración regional compiladas en MSIL mientras se mantiene el bloqueo del cargador.

Hay tres opciones para resolver este problema:

Los archivos de código fuente que contienen todas las definiciones de las secuencias de E/S globales se pueden compilar utilizando la opción /clr. De esta forma, se evitará que sus inicializadores estáticos se ejecuten con bloqueo del cargador.

Las definiciones de funciones de configuración regional personalizada pueden compilarse en código nativo utilizando la directiva #pragma unmanaged.

Evite establecer la configuración regional personalizada como configuración regional global hasta que se haya liberado el bloqueo del cargador. Después, configure explícitamente las secuencias de E/S creadas durante la inicialización con la configuración regional personalizada.

Impedimentos de diagnóstico

En algunos casos, es difícil detectar el origen de los interbloqueos. Las subsecciones siguientes detallan estos escenarios y algunas maneras de resolver estos problemas.

Implementación en encabezados

Con Visual C++ .NET, Visual C++ .NET 2003 y, en determinados casos, con Visual C++ 2005, las implementaciones de funciones dentro de archivos de encabezado pueden complicar el diagnóstico. Tanto las funciones inline como el código de plantilla requieren que se especifiquen funciones en un archivo de encabezado. El lenguaje C++ especifica la regla de definición única, que exige que todas las implementaciones de funciones con el mismo nombre sean semánticamente equivalentes. En consecuencia, el vinculador de C++ no necesita tener un cuidado especial al combinar archivos objeto que tengan implementaciones duplicadas de una función determinada.

En Visual C++ -NET y Visual C++ .NET 2003, el vinculador se limita a elegir la mayor de estas definiciones equivalentes, desde el punto de vista semántico, para ajustar declaraciones y escenarios directos cuando se utilizan opciones de optimización diferentes para archivos de código fuente distintos; de tal forma que surge un problema con los archivos DLL de .NET o nativos mixtos.

Puesto que un mismo encabezado puede estar incluido en archivos CPP con /clr habilitado o deshabilitado, o que una directiva #include puede ajustarse dentro de un bloque #pragma unmanaged, es posible tener tanto versiones MSIL como versiones nativas de funciones que proporcionan implementaciones en encabezados. Las implementaciones MSIL y las nativas tienen semánticas diferentes respecto a la inicialización con bloqueo del cargador, lo que supone una verdadera infracción de la regla de definición única. Por consiguiente, cuando el vinculador elige la mayor implementación, puede elegir la versión MSIL de una función, aunque se haya compilado explícitamente en código nativo mediante la directiva no administrada #pragma. Para garantizar que nunca se llame a una versión MSIL de una función inline o de una plantilla con bloqueo del cargador, todas las definiciones de cada una de las funciones de este tipo a las que se haya llamado con bloqueo del cargador deben modificarse con la directiva #pragma unmanaged. Si el archivo de encabezado es de otro fabricante, la forma más sencilla de conseguirlo es insertar y extraer la directiva no administrada #pragma en torno a la directiva #include para el archivo de encabezado que produce el error. (Vea un ejemplo en managed, unmanaged.) Sin embargo, esta estrategia no funcionará con los encabezados que contengan otro código que deba llamar directamente a las API de .NET.

En Visual C++ 2005, para comodidad de los usuarios que tratan con el bloqueo del cargador, el vinculador elegirá la implementación nativa frente a la administrada cuando se presenten ambas. De esta forma, se evitan los problemas anteriores. Sin embargo, en esta versión existen dos excepciones a esta regla debido a dos problemas sin resolver del compilador:

  • La llamada se realiza a una función inline a través de un puntero a una función estática global. Este escenario es particularmente notable dado que se llama a las funciones virtuales a través de punteros a funciones globales. Por ejemplo,
#include "definesfoo.h"
#include "definesclassC.h"

typedef void (*function_pointer_t)();

function_pointer_t foo_p = &foo;

#pragma unmanaged
void DuringLoaderlock(C & c)
{
    // Either of these calls could resolve to a managed implementation, 
    // at link-time, even if a native implementation also exists.
    c.VirtualMember();
    foo_p();
}
  • Con la compilación orientada a Itanium, existe un error en la implementación de todos los punteros a funciones. En el fragmento de código anterior, si foo_p se definiera localmente dentro de during_loaderlock(), la llamada también podría resolverse en una implementación administrada.

Diagnosticar en modo de depuración

Todos los diagnósticos de problemas de bloqueo del cargador se deben efectuar con versiones de depuración. Las versiones de lanzamiento podrían no generar diagnósticos y las optimizaciones efectuadas en modo de lanzamiento podrían enmascarar parte de MSIL en los escenarios de bloqueo del cargador.

Depuración de problemas de bloqueo del cargador

El diagnóstico que genera el CLR cuando se invoca una función MSIL provoca la suspensión de la ejecución de CLR. A su vez, esto da lugar a la suspensión del depurador de modo mixto de Visual C++ 2005 al ejecutar el código que está siendo depurado en proceso. Sin embargo, al asociarse al proceso, no es posible obtener ninguna pila de llamadas administrada para el código objeto que está siendo depurado utilizando el depurador mixto.

Para identificar la función MSIL a la que se llamó durante el bloqueo del cargador, los desarrolladores deben seguir estos pasos:

  1. Asegúrese de que hay símbolos disponibles para mscoree.dll y mscorwks.dll.

    Esto se puede llevar a cabo de dos maneras. Primero, se pueden agregar PDB para mscoree.dll y mscorwks.dll a la ruta de búsqueda del símbolo. Para ello, abra el cuadro de diálogo de opciones de ruta de búsqueda del símbolo. (En el menú Herramientas, haga clic en Opciones. En el panel izquierdo del cuadro de diálogo Opciones, abra el nodo Depuración y haga clic en Símbolos.) Agregue la ruta de acceso a los archivos PDB mscoree.dll y mscorwks.dll a la lista de búsqueda. Los PDB se instalan en %VSINSTALLDIR%\SDK\v2.0\symbols. Haga clic en Aceptar.

    Segundo, se pueden descargar PDB para mscoree.dll y mscorwks.dll del servidor de símbolos de Microsoft. Para configurar el servidor de símbolos, abra el cuadro de diálogo de opciones de ruta de búsqueda de símbolos. (En el menú Herramientas, haga clic en Opciones. En el panel izquierdo del cuadro de diálogo Opciones, abra el nodo Depuración y haga clic en Símbolos.) Agregue la ruta de búsqueda siguiente a la lista de búsquedas: http://msdl.microsoft.com/download/symbols. Agregue un directorio de caché de símbolos al cuadro de texto de caché del servidor de símbolos. Haga clic en Aceptar.

  2. Establezca el modo del depurador como sólo nativo.

    Para ello, abra la cuadrícula de propiedades del proyecto de inicio en la solución. En el árbol secundario Propiedades de configuración, seleccione el nodo Depuración. Establezca el campo Tipo de depurador en Sólo nativo.

  3. Inicie el depurador (F5).

  4. Hacer clic en Reintentar y, a continuación, en Interrumpir, cuando se genere el diagnóstico de /clr.

  5. Abra la ventana de la pila de llamadas. (En el menú Depurar, haga clic en Ventanas y luego en Pila de llamadas.) Si el inicializador estático o el DllMain que provoca el error se identifica con una flecha verde. Si no se identifica la función que provoca el error, siga estos pasos para localizarla.

  6. Abra la ventana Inmediato. (En el menú Depurar, haga clic en Ventanas y luego en Inmediato.)

  7. Escriba .load sos.dll en la ventana Inmediato para cargar el servicio de depuración SOS.

  8. Escriba !dumpstack en la ventana Inmediato para obtener una lista completa de la pila /clr interna.

  9. Busque la primera instancia (la más cercana al final de la pila) de _CorDllMain (si DllMain provoca el problema) o de _VTableBootstrapThunkInitHelperStub o GetTargetForVTableEntry (si el inicializador estático provoca el problema). La entrada de la pila siguiente a esta llamada es la invocación de la función implementada de MSIL que se intentó ejecutar durante el bloqueo del cargador.

  10. Vaya al archivo de código fuente y al número de línea identificados en el paso 9 para corregir el problema utilizando los escenarios y las soluciones descritos en la sección Escenarios.

Ejemplo

Description

El ejemplo siguiente muestra cómo evitar el bloqueo del cargador pasando código de DllMain al constructor de un objeto global.

En este ejemplo, hay un objeto administrado global cuyo constructor contiene el objeto administrado que estaba originalmente en DllMain. La segunda parte de este ejemplo hace referencia al ensamblado, creando una instancia del objeto administrado para llamar al constructor del módulo que realiza la inicialización.

Código

// initializing_mixed_assemblies.cpp
// compile with: /clr /LD 
#pragma once
#include <stdio.h>
#include <windows.h>
struct __declspec(dllexport) A {
   A() {
      System::Console::WriteLine("Module ctor initializing based on global instance of class.\n");
   }

   void Test() {
      printf_s("Test called so linker does not throw away unused object.\n");
   }
};
 
#pragma unmanaged
// Global instance of object
A obj;
 
extern "C"
BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved) {
   // Remove all managed code from here and put it in constructor of A.
   return true;
}

Ejemplo

Código

// initializing_mixed_assemblies_2.cpp
// compile with: /clr initializing_mixed_assemblies.lib
#include <windows.h>
using namespace System;
#include <stdio.h>
#using "initializing_mixed_assemblies.dll"
struct __declspec(dllimport) A {
   void Test();
};

int main() {
   A obj;
   obj.Test();
}

Resultado

Module ctor initializing based on global instance of class.

Test called so linker does not throw away unused object.

Vea también

Conceptos

Ensamblados mixtos (nativos y administrados)