Написание пользовательского хост-приложения .NET для управления средой выполнения .NET из машинного кода

Как и весь управляемый код, приложения .NET выполняются основным приложением. Основное приложение отвечает за запуск среды выполнения (включая такие компоненты, как JIT и сборщик мусора) и вызов управляемых точек входа.

Размещение среды выполнения .NET является расширенным сценарием, и в большинстве случаев разработчикам .NET не нужно беспокоиться о размещении, так как процессы сборки .NET предоставляют основное приложение по умолчанию для запуска приложений .NET. Но в отдельных ситуациях бывает удобно разместить среду выполнения .NET явным образом — либо как средство вызова управляемого кода в собственном процессе, либо для получения большего контроля над работой среды выполнения.

В этой статье приводится обзор действий, необходимых для запуска среды выполнения .NET из машинного кода и выполнения в нем управляемого кода.

Необходимые компоненты

Так как основные приложения являются собственными, в этом учебнике рассматривается создание приложения C++ для размещения .NET. Вам потребуется среда разработки C++ (например, предоставляемая в Visual Studio).

Также потребуется создать компонент .NET для тестирования узла с помощью, поэтому необходимо установить пакет SDK для .NET.

Размещение API

Размещение среды выполнения .NET Core в .NET Core 3.0 и более поздних версиях выполняется с помощью API библиотек nethost и hostfxr. Эти точки входа решают все сложности по поиску и настройке среды выполнения для инициализации, а также поддерживают как запуск управляемого приложения, так и вызов статического управляемого метода.

До .NET Core 3.0 единственным вариантом размещения среды выполнения был API coreclrhost.h. Этот API размещения устарел и не должен использоваться для размещения сред выполнения .NET Core 3.0 и более поздних версий.

Создание узла с помощью nethost.h и hostfxr.h

Пример основного приложения, в котором демонстрируются описанные здесь действия, доступен в репозитории dotnet/samples на сайте GitHub. Комментарии в примере четко соответствуют пронумерованным шагам в этом учебнике. Инструкции по загрузке см. в разделе Просмотр и скачивание примеров.

Помните, что пример основного приложения предназначен для учебных целей, поэтому в нем реализована лишь минимальная проверка ошибок, а удобочитаемость поставлена выше эффективности.

Следующая процедура описывает применение библиотек nethost и hostfxr для запуска среды выполнения .NET в собственном приложении и вызова управляемого статического метода. В этом примере используются заголовок nethost и библиотеки, установленные в составе пакета SDK для .NET, чтобы скопировать файлы coreclr_delegates.h и hostfxr.h из репозитория dotnet/runtime.

Шаг 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_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));