Partilhar via


Escreva um host .NET personalizado para controlar o tempo de execução do .NET a partir do seu código nativo

Como todo código gerenciado, os aplicativos .NET são executados por um host. O host é responsável por iniciar o tempo de execução (incluindo componentes como o JIT e o coletor de lixo) e invocar pontos de entrada gerenciados.

Hospedar o tempo de execução do .NET é um cenário avançado e, na maioria dos casos, os desenvolvedores do .NET não precisam se preocupar com a hospedagem porque os processos de compilação do .NET fornecem um host padrão para executar aplicativos .NET. Em algumas circunstâncias especializadas, porém, pode ser útil hospedar explicitamente o tempo de execução do .NET, seja como um meio de invocar código gerenciado em um processo nativo ou para obter mais controle sobre como o tempo de execução funciona.

Este artigo fornece uma visão geral das etapas necessárias para iniciar o tempo de execução do .NET a partir do código nativo e executar o código gerenciado nele.

Pré-requisitos

Como os hosts são aplicativos nativos, este tutorial aborda a construção de um aplicativo C++ para hospedar o .NET. Você precisará de um ambiente de desenvolvimento C++ (como o fornecido pelo Visual Studio).

Você também precisará criar um componente .NET para testar o host, portanto, você deve instalar o SDK do .NET. Inclui os cabeçalhos e bibliotecas necessários para vincular. Por exemplo, no Windows com o SDK do .NET 8 os arquivos podem ser encontrados em C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Host.win-x64\8.0.4\runtimes\win-x64\native.

APIs de hospedagem

Hospedar o tempo de execução do .NET no .NET Core 3.0 e superior é feito com as APIs das nethosthostfxr bibliotecas. Esses pontos de entrada lidam com a complexidade de localizar e configurar o tempo de execução para inicialização e permitem iniciar um aplicativo gerenciado e chamar para um método gerenciado estático.

Antes do .NET Core 3.0, a única opção para hospedar o tempo de execução era por meio da coreclrhost.h API. Essa API de hospedagem está obsoleta agora e não deve ser usada para hospedar tempos de execução do .NET Core 3.0 e superiores.

Crie um host usando nethost.h e hostfxr.h

Um host de exemplo demonstrando as etapas descritas no tutorial abaixo está disponível no repositório dotnet/samples do GitHub. Os comentários no exemplo associam claramente as etapas numeradas deste tutorial ao local onde elas são executadas no exemplo. Para obter instruções de download, consulte Exemplos e tutoriais.

Tenha em mente que o host de exemplo deve ser usado para fins de aprendizagem, portanto, é leve na verificação de erros e projetado para enfatizar a legibilidade sobre a eficiência.

As etapas a seguir detalham como usar as nethost bibliotecas e hostfxr para iniciar o tempo de execução do .NET em um aplicativo nativo e chamar um método estático gerenciado. O exemplo usa os cabeçalhos e a nethost biblioteca e os coreclr_delegates.h cabeçalhos e hostfxr.h instalados com o SDK do .NET.

Passo 1 - Carregue hostfxr e obtenha funções de hospedagem exportadas

A nethost biblioteca fornece a get_hostfxr_path função para localizar a hostfxr biblioteca. A hostfxr biblioteca expõe funções para hospedar o tempo de execução do .NET. A lista completa de funções pode ser encontrada no hostfxr.h documento de design de hospedagem nativa. O exemplo e este tutorial usam o seguinte:

  • hostfxr_initialize_for_runtime_config: Inicializa um contexto de host e prepara para a inicialização do tempo de execução do .NET usando a configuração de tempo de execução especificada.
  • hostfxr_get_runtime_delegate: Obtém um delegado para a funcionalidade de tempo de execução.
  • hostfxr_close: Fecha um contexto de host.

A hostfxr biblioteca é encontrada usando get_hostfxr_path a API da nethost biblioteca. Em seguida, é carregado e suas exportações são recuperadas.

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

O exemplo usa o seguinte inclui:

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

Esses arquivos podem ser encontrados nos seguintes locais:

Ou, se você tiver instalado o SDK do .NET 8 no Windows:

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

Etapa 2 - Inicializar e iniciar o tempo de execução do .NET

As hostfxr_initialize_for_runtime_config funções e hostfxr_get_runtime_delegate inicializam e iniciam o tempo de execução do .NET usando a configuração de tempo de execução para o componente gerenciado que será carregado. A hostfxr_get_runtime_delegate função é usada para obter um delegado de tempo de execução que permite carregar um assembly gerenciado e obter um ponteiro de função para um método estático nesse assembly.

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

Etapa 3 - Carregar o assembly gerenciado e obter o ponteiro de função para um método gerenciado

O delegado de tempo de execução é chamado para carregar o assembly gerenciado e obter um ponteiro de função para um método gerenciado. O delegado requer o caminho do assembly, o nome do tipo e o nome do método como entradas e retorna um ponteiro de função que pode ser usado para invocar o método gerenciado.

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

Ao passar nullptr como o nome do tipo de delegado ao chamar o delegado de tempo de execução, o exemplo usa uma assinatura padrão para o método gerenciado:

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

Uma assinatura diferente pode ser usada especificando o nome do tipo de delegado ao chamar o delegado de tempo de execução.

Passo 4 - Execute o código gerenciado!

O host nativo agora pode chamar o método gerenciado e passá-lo os parâmetros desejados.

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

hello(&args, sizeof(args));