编写自定义 .NET 主机以从本机代码控制 .NET 运行时

像所有的托管代码一样,.NET 应用程序也由主机执行。 主机负责启动运行时(包括 JIT 和垃圾回收器等组件)和调用托管的入口点。

托管 .NET 运行时是高级方案,在大多数情况下,.NET 开发人员无需担心托管问题,因为 .NET 生成过程会提供默认主机来运行 .NET 应用程序。 虽然在某些特殊情况下,它对显式托管 .NET 运行时非常有用 - 无论是作为一种在本机进程中调用托管代码的方式还是为了获得对运行时工作原理更好的控制。

本文概述了从本机代码启动 .NET 运行时和在其中执行托管代码的必要步骤。

先决条件

由于主机是本机应用程序,所以本教程将介绍如何构造 C++ 应用程序以托管 .NET。 将需要一个 C++ 开发环境(例如,Visual Studio 提供的环境)。

还需要生成 .NET 组件以用来测试主机,因此你应该安装 .NET SDK。 它包括链接所需的标头和库。 例如,在具有 .NET 8 SDK 的 Windows 上,可以在 C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Host.win-x64\8.0.4\runtimes\win-x64\native 中找到文件。

承载 API

在 .NET Core 3.0 及更高版本中托管 .NET 运行时是通过 nethosthostfxr 库的 API 完成的。 由这些入口点来处理查找和设置运行时进行初始化所遇到的复杂性;通过它们,还可启动托管应用程序和调用静态托管方法。

在 .NET Core 3.0 之前,托管运行时的唯一选项是通过 coreclrhost.h API。 此托管 API 现已过时,不应用于托管 .NET Core 3.0 和更高版本的运行时。

使用 nethost.hhostfxr.h 创建主机

有关展示以下教程中所述的步骤的示例主机,请访问 dotnet/samples GitHub 存储库。 该示例中的注释清楚地将本教程中已编号的步骤与它们在示例中的执行位置关联。 有关下载说明,请参阅示例和教程

请记住,示例主机的用途在于提供学习指导,在纠错方面不甚严谨,其重在可读性而非效率。

以下步骤详细说明如何使用 nethosthostfxr 库在本机应用程序中启动 .NET 运行时并调用托管静态方法。 示例使用随 .NET SDK 一起安装的 nethost 标头和库以及 coreclr_delegates.hhostfxr.h 标头。

步骤 1 - 加载 hostfxr 并获取导出的托管函数

nethost 库提供用于查找 hostfxr 库的 get_hostfxr_path 函数。 hostfxr 库公开用于托管 .NET 运行时的函数。 函数的完整列表可在 hostfxr.h本机托管设计文档中找到。 示例和本教程使用以下函数:

  • hostfxr_initialize_for_runtime_config:初始化主机上下文,并使用指定的运行时配置准备初始化 .NET 运行时。
  • hostfxr_get_runtime_delegate:获取对运行时功能的委托。
  • hostfxr_close:关闭主机上下文。

使用 nethost 库中的 get_hostfxr_path API 找到了 hostfxr 库。 随后加载此库并检索其导出。

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

可以在以下位置找到这些文件:

或者,如果你已在 Windows 上安装了 .NET 8 SDK:

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

步骤 2 - 初始化和启动 .NET 运行时

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