Écrire un hôte .NET personnalisé pour contrôler le runtime .NET à partir de votre code natif

Comme tout le code managé, les applications .NET sont exécutées par un hôte. L’hôte est chargé du démarrage du runtime (notamment des composants comme le JIT et le récupérateur de mémoire) et de l’appel des points d’entrée managés.

L’hébergement du runtime .NET est un scénario avancé et, dans la plupart des cas, les développeurs .NET n’ont pas à se soucier de l’hébergement, car les processus de génération .NET fournissent un hôte par défaut pour exécuter les applications .NET. Toutefois, dans certains cas spécifiques, il peut être utile d’héberger explicitement le runtime .NET, pour appeler le code managé dans un processus natif ou pour mieux contrôler le fonctionnement du runtime.

Cet article donne une vue d’ensemble des étapes nécessaires pour démarrer le runtime .NET à partir du code natif et y exécuter du code managé.

Prérequis

Comme les hôtes sont des applications natives, ce didacticiel aborde la construction d’une application C++ pour héberger .NET. Vous avez besoin d’un environnement de développement C++ (comme celui fourni par Visual Studio).

Vous devez également créer un composant .NET avec lequel tester l’hôte, aussi devez-vous installer le SDK .NET.

API d’hébergement

L’hébergement du runtime .NET dans .NET Core 3.0 et les versions ultérieures est effectué avec les API des bibliothèques nethost et hostfxr. Ces points d’entrée gèrent la complexité de la recherche et de la configuration du runtime pour l’initialisation, et permettent à la fois de lancer une application managée et d’appeler une méthode managée statique.

Avant .NET Core 3.0, la seule option d’hébergement du runtime était via l’API coreclrhost.h. Désormais obsolète, cette API d’hébergement ne doit pas être utilisée pour héberger des runtimes .NET Core 3.0 et versions ultérieures.

Créer un hôte à l’aide de nethost.h et hostfxr.h

Un exemple d’hôte illustrant les étapes décrites dans le tutoriel suivant est disponible dans le référentiel GitHub dotnet/samples. Les commentaires de l’exemple associent clairement les étapes numérotées de ce tutoriel à l’endroit où elles sont exécutées dans l’exemple. Pour obtenir des instructions de téléchargement, consultez Exemples et didacticiels.

N’oubliez pas que l’exemple d’hôte est destiné à être utilisé dans un contexte d’apprentissage, il ne s’attarde donc pas sur la vérification des erreurs et privilégie la lisibilité par rapport à l’efficacité.

Les étapes suivantes décrivent comment utiliser les bibliothèques nethost et hostfxr pour démarrer le runtime .NET dans une application native et appeler une méthode statique managée. L’exemple utilise l’en-tête nethost et la bibliothèque installée avec le SDK .NET et des copies des fichiers coreclr_delegates.h et hostfxr.h issus du référentiel dotnet/runtime.

Étape 1 : charger hostfxr et obtenir les fonctions d’hébergement exportées

La bibliothèque nethost fournit la fonction get_hostfxr_path pour localiser la bibliothèque hostfxr. La bibliothèque hostfxr expose des fonctions pour l’hébergement du runtime .NET. Vous trouverez la liste complète des fonctions dans hostfxr.h et le document de conception d’hébergement natif. L’exemple et ce didacticiel utilisent les éléments suivants :

  • hostfxr_initialize_for_runtime_config : initialise un contexte d’hôte et le prépare pour l’initialisation du runtime .NET à l’aide de la configuration de runtime spécifiée.
  • hostfxr_get_runtime_delegate : obtient un délégué pour la fonctionnalité de runtime.
  • hostfxr_close : ferme un contexte d’hôte.

La bibliothèque hostfxr est trouvée à l’aide de l’API get_hostfxr_path à partir de la bibliothèque nethost. Elle est ensuite chargée, et ses exportations sont récupérées.

// 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’exemple utilise les éléments include suivants :

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

Ces fichiers se trouvent aux emplacements suivants :

Étape 2 : initialiser et démarrer le runtime .NET

Les fonctions hostfxr_initialize_for_runtime_config et hostfxr_get_runtime_delegate initialisent et démarrent le runtime .NET à l’aide de la configuration du runtime pour le composant managé qui est chargé. La fonction hostfxr_get_runtime_delegate est utilisée pour obtenir un délégué de runtime qui permet le chargement d’un assembly managé et l’obtention d’un pointeur de fonction dans une méthode statique de cet 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;
}

Étape 3 : Charger l’assembly managé et obtenir le pointeur de fonction sur une méthode managée

Le délégué de runtime est appelé pour charger l’assembly managé et obtenir un pointeur de fonction vers une méthode managée. Le délégué nécessite le chemin d’accès de l’assembly, le nom de type et le nom de méthode en entrée, et retourne un pointeur de fonction qui peut être utilisé pour appeler la méthode managée.

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

En passant nullptr comme nom de type de délégué lors de l’appel du délégué du runtime, l’exemple utilise une signature par défaut pour la méthode managée :

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

Une signature différente peut être utilisée en spécifiant le nom du type de délégué lors de l’appel du délégué de runtime.

Étape 4 : Exécuter le code managé !

L’hôte natif peut maintenant appeler la méthode managée et lui passer les paramètres souhaités.

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

hello(&args, sizeof(args));