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

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

托管 .NET 运行时是一种高级方案,在大多数情况下,.NET 开发人员无需担心托管,因为 .NET 生成进程提供运行 .NET 应用程序的默认主机。 但是,在某些特殊情况下,显式托管 .NET 运行时可能很有用,无论是在本机进程中调用托管代码的方法,还是为了更好地控制运行时的工作原理。

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

先决条件

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

还需要生成一个 .NET 组件来测试主机,因此应安装 最新的 .NET SDK。 它包括链接所需的必要的标头和库。

托管 API

使用 nethosthostfxr 库的 API 来托管 .NET 运行时。 这些入口点处理查找和设置运行时以初始化的复杂性,并允许启动托管应用程序并调用静态托管方法。

重要

nethosthostfxr 托管 API 仅支持框架依赖的部署。 独立部署应被视为独立可执行文件。 如果要评估应用程序的部署模型,请使用依赖于框架的部署来确保与这些本机托管 API 的兼容性。

使用 nethost.hhostfxr.h 创建主机

dotnet/samples GitHub 存储库中提供一个示例主机,以演示以下教程中概述的步骤。 示例中的注释清楚地将本教程中的编号步骤与示例中执行的步骤相关联。 有关下载说明,请参阅 示例和教程

请记住,示例主机旨在用于学习目的,因此错误检查较简化,设计重点在于可读性而非效率。

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

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

nethost 库提供 get_hostfxr_path 函数以定位 hostfxr 库。 该 hostfxr 库公开用于托管 .NET 运行时的函数。 可以在hostfxr.h找到函数的完整列表。 此示例和本教程使用以下内容:

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

可以通过 hostfxr API 从 get_hostfxr_path 库中找到 nethost 库。 然后加载它并检索其导出。

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

局限性

单个进程内只能加载一个运行时。 hostfxr_initialize_for_runtime_config如果在加载运行时时调用 API,它将检查现有运行时是否与指定的初始化参数兼容。 如果兼容,将使用现有运行时,如果不兼容,API 将返回失败。