사용자 지정 .NET 호스트를 작성하여 네이티브 코드에서 .NET 런타임 제어

모든 관리 코드와 같이 .NET 애플리케이션은 호스트에서 실행됩니다. 호스트는 런타임(가비지 수집기 및 JIT와 같은 구성 요소 포함)을 시작하고 관리 진입점을 호출합니다.

.NET 런타임 호스트는 고급 시나리오이며, .NET 빌드 프로세스는 .NET 애플리케이션을 실행하는 기본 호스트를 제공하므로 대부분의 경우 .NET 개발자는 호스트에 대해 걱정할 필요가 없습니다. 그러나 일부 특수한 경우, 네이티브 프로세스에서 관리 코드를 호출하는 수단으로나 런타임 작동 방식에 대해 더 많은 제어 권한을 얻기 위해 .NET 런타임을 명시적으로 호스트한 것이 유용할 수 있습니다.

이 문서에서는 네이티브 코드에서 .NET 런타임을 시작하고 관리 코드를 실행하는 데 필요한 단계에 대한 개요를 제공합니다.

필수 조건

호스트는 네이티브 애플리케이션이기 때문에 이 자습서에서는 .NET을 호스트하는 C++ 애플리케이션 생성을 다룹니다. Visual Studio에서 제공하는 C++ 개발 환경 같은 C++ 개발 환경이 필요합니다.

또한 호스트를 테스트할 .NET 구성 요소를 빌드해야 하므로 .NET SDK를 설치해야 합니다.

호스팅 API

.NET Core 3.0 이상에서 .NET Core 런타임을 호스트하려면 nethosthostfxr 라이브러리 API를 사용해야 합니다. 이러한 진입점은 런타임을 찾아 초기화에 대해 설정하는 복잡성을 처리하고, 관리형 애플리케이션 시작과 정적 관리형 메서드 호출을 모두 허용합니다.

.NET Core 3.0 이전에는 런타임을 호스트하는 유일한 옵션이 coreclrhost.h API를 통하는 것이었습니다. 이 호스팅 API는 이제 사용되지 않으며 .NET Core 3.0 이상 런타임을 호스트하는 데 사용해서는 안 됩니다.

nethost.hhostfxr.h를 사용하여 호스트 만들기

아래의 자습서에 설명된 단계를 보여 주는 샘플 호스트는 GitHub의 dotnet/samples 리포지토리에서 사용할 수 있습니다. 샘플에 있는 주석은 이 자습서에서 번호가 매겨진 단계를 샘플에서 수행되는 위치와 명확하게 연결합니다. 다운로드 지침은 샘플 및 자습서를 참조하세요.

샘플 호스트는 학습 목적으로 사용되므로 오류 검사가 부족하며 효율성보다 가독성을 강조하도록 설계되었습니다.

다음 단계에서는 nethosthostfxr 라이브러리를 사용하여 네이티브 애플리케이션에서 .NET 런타임을 시작하고 관리형 정적 메서드를 호출하는 방법을 자세히 설명합니다. 샘플은 .NET SDK와 함께 설치된 nethost 헤더 및 라이브러리와 dotnet/runtime 리포지토리의 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: 호스트 컨텍스트를 닫습니다.

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