Share via


Schreiben eines benutzerdefinierten .NET Core-Hosts, um die .NET-Laufzeit über nativen Code zu steuern

Wie aller verwalteter Code werden .NET Core-Anwendungen von einem Host ausgeführt. Der Host ist für das Starten der Runtime (einschließlich Komponenten wie die JIT und Garbage Collector) und das Aufrufen von verwalteten Einstiegspunkten verantwortlich.

Das Hosten der .NET-Laufzeit ist ein erweitertes Szenario, und in den meisten Fällen müssen sich .NET-Entwickler nicht darum kümmern, da die Buildprozesse von .NET einen Standardhost bereitstellen, der die .NET-Anwendungen ausführt. Unter speziellen Umständen kann es jedoch hilfreich sein, die .NET-Laufzeit explizit zu hosten, entweder als Mittel zum Aufrufen von verwaltetem Code in einem nativen Prozess oder um mehr Kontrolle über die Funktionsweise der Laufzeit zu erhalten.

Dieser Artikel gibt einen Überblick über die Schritte, die erforderlich sind, um die .NET-Laufzeitumgebung aus nativem Code zu starten und darin verwalteten Code auszuführen.

Voraussetzungen

Da Hosts native Anwendungen sind, wird in diesem Tutorial das Erstellen einer C++-Anwendung zum Hosten von .NET behandelt. Sie benötigen eine C++-Entwicklungsumgebung (z.B. von Visual Studio).

Außerdem müssen Sie eine .NET-Komponente erstellen, mit der Sie den Host testen können. Daher sollten Sie das .NET SDK installieren.

Hosting-APIs

Das Hosting der .NET-Laufzeit in .NET Core 3.0 und höher erfolgt mit den APIs der nethost- und hostfxr-Bibliotheken. Diese Einstiegspunkte bewältigen die Komplexität des Auffindens und Einrichtens der Runtime für die Initialisierung und ermöglichen sowohl das Starten einer verwalteten Anwendung als auch das Aufrufen einer statischen verwalteten Methode.

Vor .NET Core 3.0 war die einzige Option zum Hosten der Runtime über die coreclrhost.h-API. Diese Hosting-API ist jetzt veraltet und sollte nicht zum Hosten von .NET Core 3.0 und höheren Laufzeiten verwendet werden.

Erstellen eines Hosts mithilfe von nethost.h und hostfxr.h

Ein Beispielhost zur Veranschaulichung der Schritte im Tutorial unten finden Sie in unserem Repository „dotnet/samples“ auf GitHub. Kommentare im Beispiel ordnen die nummerierten Schritte aus diesem Tutorial ihrer Position im Beispiel deutlich zu. Anweisungen zum Herunterladen finden Sie unter Beispiele und Lernprogramme.

Bedenken Sie, dass der Beispielhost zu Lernzwecken gedacht und somit bei der Fehlerüberprüfung nachsichtig ist und bessere Lesbarkeit über Effizienz stellt.

In den folgenden Schritten wird detailliert beschrieben, wie Sie die Bibliotheken nethost und hostfxr verwenden können, um die .NET-Laufzeit in einer nativen Anwendung zu starten und eine verwaltete statische Methode aufzurufen. Im Beispiel werden der mit dem .NET SDK installierte Header nethost und die Bibliothek sowie Kopien der Dateien coreclr_delegates.h und hostfxr.h aus dem Repository dotnet/runtime verwendet.

Schritt 1: Laden von hostfxr und Abrufen der exportierten Hostingfunktionen

Die Bibliothek nethost bietet die Funktion get_hostfxr_path für das Auffinden der Bibliothek hostfxr. Die hostfxr-Bibliothek macht die Funktionen zum Hosten der .NET-Laufzeit verfügbar. Die vollständige Liste der Funktionen finden Sie in hostfxr.h und im Entwurfsdokument zum nativen Hosting. Im Beispiel und in diesem Tutorial wird Folgendes verwendet:

  • hostfxr_initialize_for_runtime_config: Initialisiert einen Hostkontext und bereitet die Initialisierung der .NET-Laufzeit mithilfe der angegebenen Laufzeitkonfiguration vor.
  • hostfxr_get_runtime_delegate: Ruft einen Delegaten für Runtimefunktionalität ab.
  • hostfxr_close: Schließt einen Hostkontext.

Die hostfxr-Bibliothek wird mithilfe der get_hostfxr_path-API aus der nethost-Bibliothek gefunden. Sie wird anschließend geladen, und ihre Exporte werden abgerufen.

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

Die Stichprobe verwendet die folgenden Entitäten:

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

Diese Dateien finden Sie an den folgenden Speicherorten:

Schritt 2: Initialisieren und Starten der .NET-Laufzeit

Die Funktionen hostfxr_initialize_for_runtime_config und hostfxr_get_runtime_delegate initialisieren und starten die .NET-Laufzeit unter Verwendung der Laufzeitkonfiguration für die verwaltete Komponente, die geladen wird. Die Funktion hostfxr_get_runtime_delegate wird verwendet, um einen Runtimedelegaten abzurufen, der das Laden einer verwalteten Assembly und das Abrufen eines Funktionszeigers auf eine statische Methode in dieser Assembly ermöglicht.

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

Schritt 3: Laden der verwalteten Assembly und Abrufen des Funktionszeigers auf eine verwaltete Methode

Der Runtimedelegat wird aufgerufen, um die verwaltete Assembly zu laden und einen Funktionszeiger auf eine verwaltete Methode abzurufen. Der Delegat benötigt den Assemblypfad, Typ- und Methodennamen als Eingaben und gibt einen Funktionszeiger zurück, mit dem die verwaltete Methode aufgerufen werden kann.

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

Durch die Übergabe von nullptr als Delegatentypname beim Aufruf des Runtimedelegaten verwendet das Beispiel eine Standardsignatur für die verwaltete Methode:

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

Eine andere Signatur kann verwendet werden, indem beim Aufruf des Runtimedelegaten der Name des Delegatentyps angegeben wird.

Schritt 4: Ausführen von verwaltetem Code

Der native Host kann nun die verwaltete Methode aufrufen und die gewünschten Parameter an sie übergeben.

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

hello(&args, sizeof(args));