Escritura de un host personalizado de .NET para controlar el entorno de ejecución de .NET desde el código nativo

Como sucede con todo el código administrado, las aplicaciones de .NET las ejecuta un host. El host es responsable de iniciar el entorno de ejecución (incluidos componentes como el JIT y el recolector de elementos no utilizados) y de invocar puntos de entrada administrados.

El hospedaje del entorno de ejecución de .NET es un escenario avanzado y, en la mayoría de los casos, los desarrolladores de .NET no deben preocuparse del hospedaje porque los procesos de compilación de .NET proporcionan un host predeterminado para ejecutar aplicaciones de .NET. Pero en algunas circunstancias especializadas, puede ser útil hospedar de forma explícita el entorno de ejecución de .NET, como un medio para invocar código administrado en un proceso nativo o para obtener más control sobre el funcionamiento del entorno de ejecución.

En este artículo se proporciona información general sobre los pasos necesarios para iniciar el entorno de ejecución de .NET desde código nativo y ejecutar código administrado en él.

Prerrequisitos

Como los hosts son aplicaciones nativas, en este tutorial se describe la creación de una aplicación de C++ para hospedar .NET. Necesitará un entorno de desarrollo de C++ (como el que se proporciona mediante Visual Studio).

También tendrá que compilar un componente de .NET con el que probar el host, por lo que debe instalar el SDK de .NET.

API de hospedaje

El hospedaje del entorno de ejecución de .NET en .NET Core 3.0 y versiones posteriores se realiza por medio de las API de las bibliotecas nethost y hostfxr. Estos puntos de entrada tratan la complejidad de buscar y configurar el entorno de ejecución para la inicialización y permiten tanto el inicio de una aplicación administrada como la llamada a un método administrado estático.

Antes de .NET Core 3.0, la única opción para hospedar el entorno de ejecución era la API coreclrhost.h. Ahora esta API de hospedaje ha quedado obsoleta y no se debe usar para hospedar entornos de ejecución de NET Core 3.0 y versiones posteriores.

Creación de un host mediante nethost.h y hostfxr.h

En el repositorio dotnet/samples de GitHub hay disponible un host de ejemplo que muestra los pasos descritos en el siguiente tutorial. Los comentarios del ejemplo asocian claramente los pasos numerados de este tutorial con el lugar en el que se realizan en el ejemplo. Para obtener instrucciones de descarga, vea Ejemplos y tutoriales.

Tenga en cuenta que el host de ejemplo está diseñado para usarse con fines de aprendizaje, por lo que la comprobación de errores es flexible y está diseñado para priorizar la legibilidad antes que la eficacia.

En los pasos siguientes se explica cómo usar las bibliotecas nethost y hostfxr para iniciar el entorno de ejecución de .NET en una aplicación nativa y llamar a un método estático administrado. En el ejemplo se usa el encabezado nethost y la biblioteca instalados con el SDK de .NET así como copias de los archivos coreclr_delegates.h y hostfxr.h del repositorio dotnet/runtime.

Paso 1: Carga de hostfxr y obtención de funciones de hospedaje exportadas

La biblioteca nethost proporciona la función get_hostfxr_path para buscar la biblioteca hostfxr. La biblioteca hostfxr expone funciones para hospedar el entorno de ejecución de .NET. Encontrará la lista completa de funciones en hostfxr.h y en el documento de diseño de hospedaje nativo. El ejemplo y este tutorial usan lo siguiente:

  • hostfxr_initialize_for_runtime_config: inicializa un contexto de host y se prepara para la inicialización del entorno de ejecución de .NET mediante la configuración del entorno de ejecución especificado.
  • hostfxr_get_runtime_delegate: Obtiene un delegado para la funcionalidad del entorno de ejecución.
  • hostfxr_close: Cierra un contexto del host.

La biblioteca hostfxr se encuentra mediante la API get_hostfxr_path de la biblioteca nethost. Después, se carga y se recuperan las exportaciones.

// Using the nethost library, discover the location of hostfxr and get exports
bool load_hostfxr()
{
    // Pre-allocate a large buffer for the path to hostfxr
    char_t buffer[MAX_PATH];
    size_t buffer_size = sizeof(buffer) / sizeof(char_t);
    int rc = get_hostfxr_path(buffer, &buffer_size, nullptr);
    if (rc != 0)
        return false;

    // Load hostfxr and get desired exports
    void *lib = load_library(buffer);
    init_fptr = (hostfxr_initialize_for_runtime_config_fn)get_export(lib, "hostfxr_initialize_for_runtime_config");
    get_delegate_fptr = (hostfxr_get_runtime_delegate_fn)get_export(lib, "hostfxr_get_runtime_delegate");
    close_fptr = (hostfxr_close_fn)get_export(lib, "hostfxr_close");

    return (init_fptr && get_delegate_fptr && close_fptr);
}

El ejemplo utiliza lo que se incluye a continuación:

#include <nethost.h>
#include <coreclr_delegates.h>
#include <hostfxr.h>

Estos archivos se encuentran en las siguientes ubicaciones:

Paso 2: Inicialización e inicio del entorno de ejecución de .NET

Las funciones hostfxr_initialize_for_runtime_config y hostfxr_get_runtime_delegate inicializan e inician el entorno de ejecución de .NET con la configuración del entorno de ejecución para el componente administrado que se va a cargar. La función hostfxr_get_runtime_delegate se utiliza para obtener un delegado del entorno de ejecución que permite cargar un ensamblado administrado y obtener un puntero de función para un método estático en ese ensamblado.

// Load and initialize .NET Core and get desired function pointer for scenario
load_assembly_and_get_function_pointer_fn get_dotnet_load_assembly(const char_t *config_path)
{
    // Load .NET Core
    void *load_assembly_and_get_function_pointer = nullptr;
    hostfxr_handle cxt = nullptr;
    int rc = init_fptr(config_path, nullptr, &cxt);
    if (rc != 0 || cxt == nullptr)
    {
        std::cerr << "Init failed: " << std::hex << std::showbase << rc << std::endl;
        close_fptr(cxt);
        return nullptr;
    }

    // Get the load assembly function pointer
    rc = get_delegate_fptr(
        cxt,
        hdt_load_assembly_and_get_function_pointer,
        &load_assembly_and_get_function_pointer);
    if (rc != 0 || load_assembly_and_get_function_pointer == nullptr)
        std::cerr << "Get delegate failed: " << std::hex << std::showbase << rc << std::endl;

    close_fptr(cxt);
    return (load_assembly_and_get_function_pointer_fn)load_assembly_and_get_function_pointer;
}

Paso 3: Carga del ensamblado administrado y obtención un puntero de función a un método administrado

Se llama al delegado del entorno de ejecución para cargar el ensamblado administrado y obtener un puntero de función a un método administrado. El delegado necesita la ruta de acceso de ensamblado, el nombre del tipo y el nombre del método como entradas y devuelve un puntero de función que se puede utilizar para llamar al método administrado.

// Function pointer to managed delegate
component_entry_point_fn hello = nullptr;
int rc = load_assembly_and_get_function_pointer(
    dotnetlib_path.c_str(),
    dotnet_type,
    dotnet_type_method,
    nullptr /*delegate_type_name*/,
    nullptr,
    (void**)&hello);

Al pasar nullptr como nombre de tipo de delegado al llamar al delegado del entorno de ejecución, el ejemplo utiliza una firma predeterminada para el método administrado:

public delegate int ComponentEntryPoint(IntPtr args, int sizeBytes);

Se puede utilizar una firma diferente si se especifica el nombre del tipo de delegado al llamar al delegado en entorno de ejecución.

Paso 4: Ejecutar el código administrado

El host nativo ahora puede llamar al método administrado y pasarle los parámetros deseados.

lib_args args
{
    STR("from host!"),
    i
};

hello(&args, sizeof(args));