Scrivere un host .NET personalizzato per controllare il runtime .NET dal codice nativo

Come tutto il codice gestito, le applicazioni .NET sono eseguite da un host. L'host è responsabile dell'avvio del runtime, inclusi i componenti come JIT e Garbage Collector, nonché della chiamata dei punti di ingresso gestiti.

L'hosting del runtime .NET è uno scenario avanzato e, nella maggior parte dei casi, gli sviluppatori .NET non devono preoccuparsi dell'hosting perché i processi di compilazione .NET forniscono un host predefinito per l'esecuzione di applicazioni .NET. Tuttavia, in alcune circostanze particolari può essere utile ospitare in modo esplicito il runtime di .NET per richiamare il codice gestito in un processo nativo o per avere maggior controllo sul funzionamento del runtime.

Questo articolo offre una panoramica dei passaggi necessari per avviare il runtime di .NET dal codice nativo ed eseguire al suo interno il codice gestito.

Prerequisiti

Poiché gli host sono applicazioni native, in questa esercitazione verrà descritta la costruzione di un'applicazione C++ per l'hosting di .NET. Sarà necessario un ambiente di sviluppo C++, come quello incluso in Visual Studio.

È anche necessario compilare un componente .NET per testare l'host con , quindi è necessario installare .NET SDK. Include le intestazioni e librerie necessarie con cui collegarsi. Ad esempio, in Windows con .NET 8 SDK i file sono disponibili in C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Host.win-x64\8.0.4\runtimes\win-x64\native.

API di hosting

L'hosting del runtime .NET in .NET Core 3.0 e versioni successive viene eseguito con le API librerie nethost e hostfxr. Questi punti di ingresso gestiscono la complessità legata alla ricerca e alla configurazione del runtime per l'inizializzazione e consentono sia l'avvio di un'applicazione gestita sia la chiamata a un metodo gestito statico.

Prima di .NET Core 3.0, l'unica opzione per ospitare il runtime era tramite l'API coreclrhost.h. Questa API di hosting è obsoleta e non deve essere usata per l'hosting di runtime .NET Core 3.0 e versioni successive.

Creare un host usando nethost.h e hostfxr.h

Un host di esempio che illustra i passaggi descritti nell'esercitazione seguente è disponibile nel repository GitHub dotnet/samples. I commenti nell'esempio associano chiaramente i passaggi numerati di questa esercitazione alla posizione in cui vengono eseguiti nell'esempio. Per istruzioni sul download, vedere Esempi ed esercitazioni.

Tenere presente che poiché l'host di esempio è destinato a essere usato ai fini dell'apprendimento, il controllo degli errori non è prioritario e l'host è stato progettato per evidenziare la leggibilità e non l'efficienza.

La procedura seguente illustra come usare le librerie nethost e hostfxr per avviare il runtime di .NET in un'applicazione nativa ed eseguire una chiamata a un metodo statico gestito. L'esempio usa le intestazioni e la libreria nethost e le intestazioni coreclr_delegates.h e hostfxr.h installate con .NET SDK.

Passaggio 1: caricare hostfxr e ottenere funzioni di hosting esportate

La libreria nethost fornisce la funzione get_hostfxr_path per l'individuazione della libreria hostfxr. La libreria hostfxr espone le funzioni per l'hosting del runtime .NET. L'elenco completo delle funzioni è riportato in hostfxr.h e nel documento per la progettazione dell'hosting nativo. L'esempio e questa esercitazione usano le funzioni seguenti:

  • hostfxr_initialize_for_runtime_config: inizializza un contesto host e lo prepara per l'inizializzazione del runtime di .NET usando la configurazione di runtime specificata.
  • hostfxr_get_runtime_delegate: ottiene un delegato per la funzionalità di runtime.
  • hostfxr_close: chiude un contesto host.

La libreria di hostfxr si trova usando get_hostfxr_path API dalla libreria di nethost. La libreria viene quindi caricata e vengono recuperate le relative funzioni esportate.

// 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);
}

L'esempio usa quanto segue:

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

Questi file sono disponibili nei percorsi seguenti:

Oppure, se si ha installato .NET 8 SDK in Windows:

  • C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Host.win-x64\8.0.4\runtimes\win-x64\native

Passaggio 2: inizializzare e avviare il runtime .NET

Le funzioni hostfxr_initialize_for_runtime_config e hostfxr_get_runtime_delegate inizializzano e avviano il runtime di .NET usando la configurazione di runtime per il componente gestito che verrà caricato. La funzione hostfxr_get_runtime_delegate viene usata per ottenere un delegato di runtime che consente il caricamento di un assembly gestito e il recupero di un puntatore di funzione a un metodo statico in tale assembly.

// 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;
}

Passaggio 3: Caricare l'assembly gestito e ottenere un puntatore di funzione a un metodo gestito

Il delegato di runtime viene chiamato per caricare l'assembly gestito e ottenere un puntatore di funzione a un metodo gestito. Il delegato richiede il percorso dell'assembly, il nome del tipo e il nome del metodo come input e restituisce un puntatore di funzione che può essere usato per richiamare il metodo gestito.

// 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);

Passando nullptr come nome del tipo delegato quando viene eseguita la chiamata al delegato di runtime, l'esempio usa una firma predefinita per il metodo gestito:

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

È possibile usare una firma diversa specificando il nome del tipo delegato quando viene eseguita la chiamata al delegato di runtime.

Passaggio 4: Eseguire il codice gestito

L'host nativo può ora chiamare il metodo gestito e passare i parametri desiderati.

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

hello(&args, sizeof(args));