ネイティブ コードから .NET ランタイムを制御するカスタム .NET ホストを作成する

あらゆるマネージド コードと同様に、.NET アプリケーションはホストにより実行されます。 ホストは、ランタイム (JIT やガベージ コレクターのようなコンポーネントを含む) の開始、マネージド エントリ ポイントの呼び出しを担当します。

.NET ランタイムのホスティングは高度なシナリオです。ほとんどの場合、.NET 開発者はホスティングについて心配する必要がありません。.NET ビルド プロセスによって、.NET アプリケーションを実行するための既定ホストが提供されるからです。 しかしながら、一部の特別な状況で、ネイティブ プロセスのマネージド コードを呼び出す手段として、あるいはランタイムの動作をさらに細かくコントロールする目的で .NET ランタイムを明示的にホスティングすると効果的な場合があります。

この記事では、ネイティブ コードから .NET ランタイムを開始し、その中でマネージド コードを実行するために必要な手順について説明します。

前提条件

ホストはネイティブ アプリケーションであるため、このチュートリアルでは、C++ アプリケーションを構築して .NET をホスティングする方法について説明します。 C++ 開発環境が必要になります (Visual Studio に付属のものなど)。

また、.NET コンポーネントをビルドして、それを使ってホストをテストする必要があるため、.NET SDK をインストールしてください。

ホスト API

.NET Core 3.0 以降の .NET ランタイムをホスティングするには、nethost および hostfxr ライブラリの API を使います。 これらのエントリ ポイントは、初期化のためにランタイムを検索して設定する複雑な処理が行われ、マネージド アプリケーションの起動と静的マネージド メソッドの呼び出しの両方が可能です。

.NET Core 3.0 より前のバージョンでは、ランタイムをホスティングする唯一のオプションは coreclrhost.h API を使用する方法でした。 現在、このホスティング API は古くなっています。.NET Core 3.0 以上のランタイムのホスティングには使用しないでください。

nethost.hhostfxr.h を使用してホストを作成する

dotnet/samples GitHub リポジトリには、以下のチュートリアルで説明する手順を示すサンプル ホストがあります。 サンプル内のコメントを見れば、このチュートリアルの番号付きの手順がサンプルのどこで実行されるかがわかります。 ダウンロード方法については、「サンプルおよびチュートリアル」を参照してください。

サンプル ホストは学習目的のために利用されるものです。そのため、エラー チェックが本来より少なく、効率よりも読みやすさを重視して設計されていることに注意してください。

以下は、nethost および hostfxr ライブラリを使用してネイティブ アプリケーションで .NET ランタイムを起動し、静的マネージド メソッドを呼び出す手順を詳しくまとめたものです。 サンプルでは、.NET SDK でインストールされる nethost ヘッダーとライブラリ、および dotnet/runtime リポジトリからの coreclr_delegates.h および hostfxr.h ファイルのコピーが使用されます。

ステップ 1 - hostfxr を読み込んで、エクスポートされたホスティング関数を取得する

nethost ライブラリでは、hostfxr ライブラリを検索するための get_hostfxr_path 関数が提供されています。 hostfxr ライブラリでは、.NET ランタイムをホスティングするための関数が公開されています。 関数の完全な一覧については、hostfxr.h およびネイティブ ホスティング デザインのドキュメントを参照してください。 サンプルとこのチュートリアルでは以下を使います。

  • hostfxr_initialize_for_runtime_config: ホスト コンテキストを初期化し、指定されたランタイム構成を使って .NET ランタイムの初期化を準備します。
  • hostfxr_get_runtime_delegate:ランタイム機能に対するデリゲートを取得します。
  • hostfxr_close:ホスト コンテキストを閉じます。

hostfxr ライブラリは、nethost ライブラリにある get_hostfxr_path API を使って検索されます。 その後、読み込まれて、そのエクスポートが取得されます。

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

サンプルでは次が使用されます。

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

これらのファイルは、次の場所にあります。

ステップ 2 - .NET ランタイムを初期化して開始する

hostfxr_initialize_for_runtime_config および hostfxr_get_runtime_delegate 関数では、後で読み込まれるマネージド コンポーネントに対するランタイム構成を使って、.NET ランタイムが初期化されて開始されます。 マネージド アセンブリの読み込み、およびそのアセンブリ内の静的メソッドへの関数ポインターの取得を可能にするランタイムのデリゲートを取得するには、hostfxr_get_runtime_delegate 関数を使います。

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

ステップ 3 - マネージド アセンブリを読み込み、マネージド メソッドへの関数ポインターを取得する

マネージド アセンブリを読み込んで、マネージド メソッドへの関数ポインターを取得するには、ランタイムのデリゲートを呼び出します。 デリゲートでは、入力としてアセンブリのパス、型の名前、およびメソッドの名前が必要であり、マネージド メソッドの呼び出しに使用できる関数ポインターが返されます。

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

ランタイムのデリゲートを呼び出すときにデリゲートの型の名前として nullptr を渡すことにより、サンプルではマネージド メソッドに対して既定のシグネチャを使います。

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

ランタイムのデリゲートを呼び出すときに、デリゲートの型名を指定することにより、異なるシグネチャを使用できます。

ステップ 4 - マネージド コードを実行する

ネイティブ ホストでは、マネージド メソッドを呼び出し、目的のパラメーターを渡すことができます。

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

hello(&args, sizeof(args));