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.