APIs para hospedaje del CLR

 

Alessandro Catorcini

Traducción de

Piotr Puszkiewicz

RFOG

 

El documento original se puede obtener de:

http://msdn.microsoft.com/msdnmag/issues/06/08/CLRInsideOut/default.aspx

 

 

Imagine que está desarrollando una aplicación de gran tamaño con C++ nativo, y le gustaría que sus clientes pudieran extender esa aplicación para que se amoldara a sus necesidades. Permitir que sus clientes escribieran las extensiones con código manejado dentro del Microsoft® .NET Framework podría hacer la experiencia de esos programadores mucho más suave que si tuvieran que trabajar con código nativo, pero cargar el motor del lenguaje común (CLR) dentro del espacio de proceso de su aplicación podría resultar preocupante. ¿Qué pasaría si una excepción CLR no manejada terminara matando a la aplicación? ¿Qué pasaría si las extensiones manejadas utilizaran más memoria de la que su aplicación estuviera dispuesta a suministrar? Podría resolver estos problemas lanzando el motor en otro proceso, pero el tener que mover grandes bloques de memoria entre los límites del proceso pronto se convertiría en algo muy caro y prohibitivo.  ¿Existe alguna forma de tomar las ventajas del código manejado sin sus costes?

Estas cuestiones frecuentemente se enfrentan al desarrollo de aplicaciones extensibles. El CLR proporciona una variedad de funcionalidades, recursos y bibliotecas para reducir el coste de construir extensiones. Sin embargo, los desarrolladores de este tipo de aplicaciones son cautelosos a la hora de permitir que código de terceros potencialmente hambriento de recursos o sin verificar se pueda ejecutar dentro de sus aplicaciones. Afortunadamente, las APIs de hospedaje del CLR 2.0 permiten controlar a los programadores de aplicaciones el consumo de recursos y la garantía de ejecución de código dentro del CLR. Mediante el uso de estas APIs, los desarrolladores de anfitriones nativos pueden ejecutar código manejado dentro de su proceso con el completo control y el conocimiento sobre cómo va a ser el comportamiento del CLR y cómo puede este afectar a la aplicación.

El CLR siempre ha permitido cierta integración entre él mismo y un anfitrión. En el .NET Framework 1.x, las aplicaciones nativas podían cargar una versión específica del CLR, iniciar y detener su ejecución, y configurar un número limitado de opciones. En la versión 2.0, el CLR permite una integración mucho más profunda. Las APIs de hospedaje actualizadas proporcionan capas de abstracción que permiten al anfitrión manejar muchos de los recursos actualmente proporcionados por Win32. Además, las APIs actualizadas extienden el conjunto de funcionalidad del CLI que puede ser configurado por el anfitrión.

 

Manejadores del anfitrión

Las APIs del CLR 2.0 se encuentras divididas en dos grupos primarios: manejadores del anfitrión y manejadores del CLR.  Los primeros poseen el nombre con el prefijo IHost, y continúa con la descripción de la función (como en IHostMemoryManager e IHostSecurityManager). Las implementaciones de los interfaces de los manejadores del anfitrión son proporcionados por el programador del anfitrión y son registrados con el CLR, que usará esos interfaces para realizar retrollamadas hacia el anfitrión durante todo el tiempo de vida del proceso.

Los interfaces del manejador del CLR vienen prefijados con ICLR, y lo que sigue a ICLR en el nombre describe la función del mismo (como en ICLRTaskManager e ICLRSyncManager). Los manejadores del CLR son mantenidos por el CRL hacia el anfitrión y son implementados por el CLR. El anfitrión usa estos interfaces para solicitar al CLR que realice cierta acción específica (por ejemplo, un anfitrión puede usar ICLRGCManager para forzar recolección de basura).

Para el propósito de este artículo, hemos agrupado ambos manejadores de acuerdo con su intención de uso, lo que aclarará los beneficios clave de utilizar las APIs de hospedaje. Incluiremos una lista de los interfaces ICLR* e IHost* asociados y su descripción en cada área de funcionalidad.

 

Manejador de memoria

El gestor de memoria proporciona al anfitrión un interfaz a través del cual el CLR solicitará las asignaciones de memoria, y que reemplaza tanto a las APIs de Windows como a las rutinas estándar de asignación del CLR. Además, el interfaz permite que el CLR informe al anfitrión de las consecuencias del fallo de una asignación particular (por ejemplo, el fallo de una asignación de memoria de un hilo que mantenga un bloqueo podría tener consecuencias serias). También permite al anfitrión personalizar la respuesta del CLR ante una asignación fallida, desde el lanzamiento de una excepción del tipo OutOfMemoryException hasta la finalización de todo el proceso. El anfitrión puede asimismo utilizar este manejador para recapturar memoria del CLR mediante la descarga de dominios de aplicación no utilizados y forzando la recolección de basura. El interfaz de este controlador de memoria se lista en la tabla inferior.

SQL Server 2005 demuestra la utilizad de las APIs de manejo de memoria. SQL Server 2005 trabaja con una cantidad de memoria configurable que generalmente se acerca a toda la memoria física disponible en la máquina. Para maximizar el rendimiento, SQL Server 2005 rastrea todas las asignaciones de memoria para asegurarse de que nunca haya paginación. El servidor podría más bien fallar ante una asignación de memoria que paginar a disco. Para seguir la pista acertadamente a todas las asignaciones, SQL Server utiliza las APIs de hospedaje de memoria de forma que cualquier petición de asignación de memoria realizada por el CLR se realiza a través del SQL Server en lugar de llamar directamente a Windows. Esto da la facultad al SQL Server de fallar ante una petición de memoria antes de que permitir la paginación.[1]

 

Interfaz

Descripción

ICLRGCManager

Proporciona métodos que permiten a un anfitrión interactuar con el sistema de recolección de basura del CLR.

ICLRMemoryNotificationCallback

Notifica al CLR de la carga de memoria del ordenador.

IHostGCManager

Notifica al anfitrión que el hilo (thread) desde el cual se ha llamado al método pertañe a un bloque para recolección de basura.

IHostMAlloc

Proporciona métodos que permiten al CLR peticionar asignaciones de grano fino al montículo a través del anfitrión.

IHostMemoryManager

Proporciona métodos que permiten al CLR realizar peticiones de memoria virtual a través del anfitrión en lugar de utilizar las funciones sobre memoria virtual estándar de Windows.

 

Manejador de hilos

Las nuevas APIs de hospedaje abstraen la noción de hilo del Win32 y esencialmente permiten al anfitrión definir la unidad de planificación y ejecución, acuñando el término “Tarea” para definir esta abstracción. Como parte de la abstracción de los hilos Win32, las tareas suministran varios métodos para modificar el CLR.

El Manejador de hilos puede:

·        Permitir al anfitrión suministrar un interfaz para que sea utilizado por el CLR para crear e iniciar nuevas tareas.

·        Proveer al anfitrión de un mecanismo para “reusar” o combinar (pool) la porción de una tarea implementada con el CLR.

·        Soportar operaciones estándar como iniciar, abortar, unir (join) y avisar (alert).

·        Implementar una retrollamada para notificar al CLR cuando una tarea ha sido movida de o a un estado ejecutable. Cuando una llamada es movida desde un estado ejecutable, el CLR debe ser capaz de indicar que dicha tarea debería ser reprogramada (rescheduled) tan pronto como sea posible.

·        Proporcionar una manera para que el CLR notifique al anfitrión de que una tarea dada no puede ser movida a otro hilo de otro Sistema Operativo físico y que su ejecución no pueda ser bloqueada durante una ventana de tiempo.

·        Permitir que el anfitrión proporcione una implementación del tread pool. El CLR ha de ser capaz de encolar elementos de trabajo, establecer y consultar el tamaño del thread pool, y demás.

·        Proporcionar notificaciones tanto al anfitrión como al CLR de que las localizaciones han cambiado para una tarea dada.

·        Proporcionar un medio para el CLR (y el código de usuario) para ajustar la prioridad de una tarea.

La Figura 2 muestra los interfaces de la gestión de hilos.

 

Interfaz

Descripción

ICLRTask

Proporciona métodos que permiten al anfitrión realizar preguntas al CLR o proporcionar notificaciones al motor en tiempo de ejecución acerca de la tarea asociada.

ICLRTaskManager

Proporciona métodos que permiten al anfitrión solicitar explícitamente que el CLR cree una nueva tarea, obtener la tarea que se esté ejecutando ahora, y establecer el idioma geográfico y la cultura para la tarea.

IHostSecurityContext

Permite al CLR mantener la información de contexto de seguridad implementada por el anfitrión.

IHostSecurityManager

Proporciona métodos que permiten acceder y controlar el contexto de seguridad del hilo que se esté ejecutando en ese momento.

IHostTask

Proporciona métodos que permiten al CLR iniciar, despertar o abortar tareas, y asociar una instancia de ICLRTask con su correspondiente instancia de IHostTask.

IHostTaskManager

Proporciona métodos que permiten al CLT trabajar con tareas a través del anfitrión en lugar de utilizar las APIs estándar sobre hilos o fibras del sistema operativo.

IHostThreadPoolManager

Proporciona métodos para permitir al CLR configurar el thread pool y encolar elementos de trabajo en él.

 

Manejador de sincronización.

Mientras que permitir al anfitrión la creación de tareas es suficiente para muchas aplicaciones multihilo, algunos anfitriones también necesitan la facultad de sobrescribir las primitivas de sincronización del CLR. Esto asegura que los bloqueos no se producen en un hilo del sistema operativo sin el conocimiento del anfitrión, y permite a las tareas CLR integrarse con el mecanismo de temporización (scheduling) del anfitrión, así como facultar al anfitrión la detección de bloqueos.

El uso del manejador de sincronización permite que el anfitrión proporcione al CLR implementaciones para varias primitivas de sincronización, incluyendo secciones críticas, eventos (tanto manuales como auto reiniciados), semáforos, monitores, y bloqueos de lectura/escritura. Vea los interfaces disponibles tabla siguiente.

 

Interfaz

Descripción

ICLRSyncManager

Define métodos que facultan al anfitrión para obtener información sobre las tareas solicitadas y para detectar puntos muertos en su implementación de la sincronización.

IHostCrst

Sirve como representación en el anfitrión de una sección crítica para multihilo.

IHostManualEvent

Proporciona la implementación al anfitrión de una representación de un evento iniciado manualmente.

IHostAutoEvent

Proporciona la implementación al anfitrión de una representación de un evento auto iniciado.

IHostSemaphore

Representa la implementación del anfitrión de un semáforo.

IHostSyncManager

Proporciona métodos y permite al CLR crear primitivas de sincronización para llamar al anfitrión en lugar de las APIs estándar de Windows.

 

Manejador de terminación de E/S

Permite al anfitrión una implementación personalizada para que el CLR la utilice en lugar de usar la terminación de E/S por defecto proporcionada por Windows. El anfitrión proporciona un camino para que el CLR enlace un manejador a un puerto de finalización de E/S. El CLR proporciona una retrollamada para que sea llamada por el anfitrión cuando la operación de E/S asíncrona se haya completado. Adicionalmente, este manejador también permite que el anfitrión inserte datos personalizados al final de la estructura OVERLAPPED pasada a las rutinas de E/S. La tabla siguiente muestra los interfaces disponibles.

 

Interfaz

Descripción

ICLRIoCompletionManager

Implementa un método de retrollamada que permite al anfitrión notificar al CLR el estado de las peticiones de E/S especificadas.

IHostIoCompletionManager

Proporciona métodos que permiten al CLR interactuar con la E/S proporcionada por el anfitrión.

 

Manejador de carga de ensamblados

Los anfitriones se basan en las APIs antiguas con comportamiento personalizado de carga mediante la captura de los eventos de System.AppDomain que son lanzados mediante un ensamblado, un recurso, o un tipo que no pueda ser encontrado (AssemblyResolveEvent, ResourceResolveEvent, y TypeResolveEvent).  El ensamblado solicitado es entonces cargado por el anfitrión utilizando la sobrecarga de Assembly.Load, que acepta un array de bytes que son pasados de vuelta al evento. Este enfoque se ha revelado como insuficiente por unas pocas razones, incluyendo el rendimiento. Cargar un ensamblado a través de la especificación de un array manejado de bytes involucra la copia de los bits múltiples veces entre la memoria manejada y la no manejada antes de que el ensamblado pueda ser ejecutado por el anfitrión.

En la versión 2.0, el anfitrión especifica qué ensamblados van a ser cargados desde la Caché Global de Ensamblados (GAC) y cumple con cualesquiera otros requisitos directamente. Cuando se está construyendo el ensamblado, el anfitrión devuelve uno como un puntero a memoria no manejada (un IStream * no manejado). En efecto, esto permite que el anfitrión implemente un ensamblado personalizado. De hecho, esta es la principal razón por la cual SQL Server 2005 implementa su propio gestor de carga de ensamblados. Los ensamblados que constituyen una aplicación de bases de datos en SQL Server 2005 se encuentran físicamente almacenados en una base de datos SQL Server en lugar de en ficheros independientes en disco. SQL Server 2005 utiliza estas APIs para cargar los ensamblados de las aplicaciones de forma eficiente desde las bases de datos mientras que continua dependiendo de los mecanismos usuales del .NET Framework para la carga de ensamblados. La tabla muestra estos interfaces.

 

Interfaz

Descripción

ICLRAssemblyIdentityManager

Proporciona métodos que soportan la comunicación entre el anfitrión y los ensamblados.

ICLRAssemblyReferenceList

Maneja una lista de ensamblados que son cargados por el CLR y no por el anfitrión.

ICLRHostBindingPolicyManager

Proporciona métodos al anfitrión para evaluar la política de enlace actual y comunicar cambios en ella a un ensamblado específico.

ICLRProbingAssemblyEnum

Proporciona métodos que permiten al anfitrión obtener las identidades de un ensamblado utilizando la información de identidad de dicho ensamblado que es interna al CLR, sin la necesidad de crear o entender dicha identidad.

ICLRReferenceAssemblyEnum

Proporciona métodos que permiten al anfitrión manipular el conjunto de ensamblados referenciados por un fichero o flujo utilizando la información de identidad interna del CLR, sin la necesidad de crear o entender dichas identidades.

ICLRValidator

Proporciona métodos para la validación de imágenes de ejecutables portátiles y detallar informes de los errores de validación.

IHostAssemblyManager

Proporciona métodos que permiten al anfitrión especificar conjuntos de ensamblados que deberán ser cargados por el CLR o por el anfitrión.

IHostAssemblyStore

Proporciona métodos que permiten al host enlazar enamblados y módulos independientemente del CLR.

 

Manejador de Configuraciones del CLR

Proporciona acceso a interfaces que permiten al anfitrión configurar varios aspectos del CLR, incluyendo la agrupación lógica de tareas para simplificar el depurado, el envío de un volcado personalizado del montículo, el registro de retrollamadas para eventos importantes del CLR, el bloqueo de funcionalidades para prevenir que sean cargadas en el CLR, y el establecimiento de una política de escalada ante fallos.

El concepto de una política de escalada requiere algunas explicaciones. Los fallos críticos incluyen los fallos de asignación de recursos (como la condición de sin memoria) y fallos síncronos (como desbordamiento de pila), y cada tipo de fallo tiene un comportamiento asociado a él, desde el lanzamiento de una excepción hasta la finalización del proceso. En la versión 2.0, el CLR habilita al anfitrión para que sobrescriba el comportamiento por defecto presente ante un fallo crítico. Un anfitrión avanzado podría definir una cadena de eventos que deberán ser intentados en una secuencia determinada por el anfitrión al responder ante fallos. Por ejemplo, un anfitrión podría decidir que ante cada fallo de no hay memoria se abortara el hilo o se descargara el AppDomain.

SQL Server 2005 utiliza la política de escalado en parte para asegurar la estabilidad de los procesos mediante el escalado de abortado de hilos o la descarga de los AppDomains cuando se produzcan fallos de recursos en areas del código que pudieran afectar a múltiples tareas. Por ejemplo, imagine que una tarea que mantiene un bloqueo recibe un fallo cuando intenta alojar memoria. En este escenario, abortar la tarea no es suficiente para mantener el dominio de la aplicación porque pudiera haber otras tareas en el mismo AppDomain esperando la liberación del bloqueo o que las datos que se están manejando estén en un estado consistente. Para más información sobre las políticas de escalado y de confiabilidad, vea el artículo de Stephen Tour "High Availability: Keep Your Code Running with the Reliability Features of the .NET Framework" en el número de octubre del 2005 del MSDN®Magazine.

 

Interfaz

Descripción

IActionOnCLREvent

Proporciona un método OnEvent, que realiza retrollamadas en eventos que han sido registrados utilizando una llamada a ICLROnEventManager::RegisterActionOnEvent.

ICLRControl

Proporciona métodos que permiten a un anfitrión obtener referencias del y configurar aspectos, CLR.

ICLRDebugManager

Proporciona métodos que permiten a un anfitrión asociar un conjunto de tareas con un nombre amigable.

ICLRErrorReportingManager

Proporciona métodos que permiten a un anfitrión configurar volcados del montículo personalizados para realizar informes de error.

ICLRHostProtectionManager

Proporciona al anfitrión una forma de bloquear clases manejadas, métodos, propiedades y campos para ofrecer capacidades específicas de un código en ejecución parcialmente seguro.

ICLROnEventManager

Proporciona métodos que permiten a un anfitrión registrar y desregistrar retrollamadas para eventos del CLR.

ICLRPolicyManager

Proporciona métodos que permiten a un anfitrión especificar políticas de actuación ante el caso de fallos o timeouts.

 

Usando las APIs de Hospedaje

En el artículo "No More Hangs: Advanced Techniques to Avoid and Detect Deadlocks in .NET Apps" en el número de abril de 2006 del MSDN Magazine, Joe Duffy desarrolló un anfitrión delgado que interceptaba hilos y primitivas de sincronización para realizar detecciones de puntos muertos de bloqueos (el código fuente está disponible en http://msdn.microsoft.com/msdnmag/issues/06/04/Deadlocks). Ese código muestra cómo usar las APIs de Hospedaje.

 

El código de la siguiente figura es similar al que necesitará para cualquier anfitrión que escriba. Carga el CLR en el proceso, lo arranca y carga un ensamblado dentro del AppDomain invocando un método en él (por claridad se omiten las verificaciones ante errores).

 

int main(int argc, _TCHAR* argv[])

{

    // Enlace al sistema de tiempo de ejecución.

    ICLRRuntimeHost *pClrHost = NULL;

    HRESULT hrCorBind = CorBindToRuntimeEx(

        NULL,   // Carga la última version del CLR disponible.

        L"wks", // Recolector de basura tipo “Workstation” ("wks" o "svr")

        0,      // No se necesitan flags.

        CLSID_CLRRuntimeHost,

        IID_ICLRRuntimeHost,

        (PVOID*)&pClrHost);

 

    // Construcción del objeto del control del anfitrión.

    DHHostControl *pHostControl = new DHHostControl(pClrHost);

   

    // Informar al CLR que este anfitrión implementa manejadores.

    pClrHost->SetHostControl(pHostControl);

 

    // Ahora, lanzar el CLR.

    HRESULT hrStart = pClrHost->Start();

 

    // Cargar un ensamblado y ejecutar un método de él.

    HRESULT hrExecute = pClrHost->ExecuteInDefaultAppDomain(

        pwzAssemblyPath, pwzAssemblyName,

        pwzMethodName, pwzMethodArgs,

        &retVal);

}

 

La primera tarea a la hora de hospedar un CLR es obtener un puntero al interfaz ICLRRuntimeHost del CLR:

 

ICLRRuntimeHost *pClrHost = NULL;

HRESULT hrCorBind = CorBindToRuntimeEx(

    NULL, // Carga la última version del CLR disponible

    L"wks", // Recolector de basura tipo “Workstation” ("wks" o "svr")

    0, // No flags needed

    CLSID_CLRCLRHost,

    IID_ICLRRuntimeHost,

    (PVOID*)&pClrHost);

 

En el .NET Framework 1.x se utilizaba ICorCLRHost para comunicar entre el CLR y el anfitrión; en la versión 2.0, el interfaz recomendado es ICLRRuntimeHost, que permite acceder a la nueva funcionalidad descrita aquí. Aparte de otras funciones, ICLRRuntimeHost facilita el intercambio de implementaciones de interfaces entre el CLR y el anfitrión. ICLRRuntimeHost::SetCLRControl permite al anfitrión notificar al CLR que algunos servicios definidos por el anfitrión necesitan ser asignados para sobrescribir el comportamiento por defecto del CLR. Este ejemplo utiliza la clase DHHostControl, que implementa IHostControl, para sobreescribir algunos de los majeadores del CLR por defecto:

 

DHHostControl *pHostControl = new DHHostControl(pClrHost)

pClrHost->SetHostControl(pHostControl)

 

Una vez que el anfitrión llama de forma satisfactoria a SetHostControl, el CLR utilizará cualquier interfaz definida por la implementación de IHostControl en lugar de las acciones por defecto. Por ejemplo, DHHostControl sobreescribe a IHostTaskManager y a IHostSyncManager mediante el suministro de dos clases en DHHostControl que heredan de dichos interfaces.

ICLRRuntimeHost también permite al anfitrión el obtener punteros a la implementación del CLR de los interfaces ICRL*. Esto se realiza mediante la llamada al método ICLRRuntimeHost::GetCLRControl para obtener un punteoro q un objeto ICLRControl. ICLRControl permite que el anfitrión acceda a todos los otros interfaces ICLR*, a través de los cuales el anfitrión puede requerir acciones específicas del CLR. Desde que la aplicación de ejemplo está monitorizando el CLR, no usa los interfaces ICLR*.

Una vez que ha cargado e inicializado el CLR, el modo más sencillo de cargar y ejecutar su código manejado es el uso del método ExexuteInDefaultAppDomain de ICLRRuntimeHost. Esto simplemente cargará un ensamblado manejado y ejecutará un método de él:

 

HRESULT hrExecute = pClrHost->ExecuteInDefaultAppDomain(

    pwzAssemblyPath, pwzAssemblyName,

    pwzMethodName, pwzMethodArgs,

    &retVal);

 

Conclusión

Los anfitriones no manejados son capaces de controlar la forma de trabajo del CLR de forma enormemente detallada. Utilizando las nuevas APIs de hospedaje del .NET Framework, un anfitrión puede, de hecho, colocarse a sí mismo entre el CLR y el sistema operativo y operar como intermediario ante cualquier requerimiento desde el CLR. Puede encontrar más información en la documentación en línea sobre las APIs de hospedaje y en el libro “Customizing the Microsoft .NET Framework Common Lenguaje Runtime” de Atreven Pratscher (Microsoft Press, 2005).

 

Puede enviar sus preguntas y comentarios a clrinout@microsoft.com.



[1] Nota del Traductor: tenemos aquí uno de los motivos por los cuales el SQL Server es tan intrusivo con el sistema, consume esa gran cantidad de recursos y ralentiza aquellos equipos en los que no se esté utilizando.