모든 관리 코드와 마찬가지로 .NET 애플리케이션은 호스트에 의해 실행됩니다. 호스트는 런타임(JIT 및 가비지 수집기와 같은 구성 요소 포함)을 시작하고 관리되는 진입점을 호출합니다.
.NET 런타임 호스팅은 고급 시나리오이며, 대부분의 경우 .NET 빌드 프로세스는 .NET 애플리케이션을 실행하는 기본 호스트를 제공하기 때문에 .NET 개발자는 호스팅에 대해 걱정할 필요가 없습니다. 그러나 일부 특수한 상황에서는 네이티브 프로세스에서 관리 코드를 호출하거나 런타임의 작동 방식을 더 잘 제어하기 위해 .NET 런타임을 명시적으로 호스트하는 것이 유용할 수 있습니다.
이 문서에서는 네이티브 코드에서 .NET 런타임을 시작하고 해당 코드에서 관리 코드를 실행하는 데 필요한 단계를 간략하게 설명합니다.
필수 조건
호스트는 네이티브 애플리케이션이므로 이 자습서에서는 .NET을 호스트할 C++ 애플리케이션을 구성하는 방법을 설명합니다. 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 이상에서는 nethost
및 hostfxr
라이브러리의 API를 사용하여 .NET 런타임이 호스팅됩니다. 이러한 진입점은 초기화를 위해 런타임을 찾고 설정하는 복잡성을 처리하고 관리되는 애플리케이션을 시작하고 정적 관리되는 메서드를 호출할 수 있도록 허용합니다.
.NET Core 3.0 이전에는 런타임을 호스트하는 유일한 옵션은 API를 통해서 coreclrhost.h
였습니다. 이 호스팅 API는 이제 사용되지 않으며 .NET Core 3.0 이상 런타임을 호스팅하는 데 사용하면 안 됩니다.
nethost.h
및 hostfxr.h
를 사용하여 호스트 생성
아래 자습서에 설명된 단계를 보여주는 샘플 호스트 는 dotnet/samples GitHub 리포지토리에서 사용할 수 있습니다. 샘플 내의 주석은 이 자습서의 단계 번호와 샘플에서 그것들이 수행되는 위치를 명확하게 연결합니다. 다운로드 지침은 샘플 및 자습서참조하세요.
샘플 호스트는 학습 목적으로 사용되므로 오류 검사에 가벼워지고 효율성보다 가독성을 강조하도록 설계되었습니다.
nethost
및 hostfxr
라이브러리를 사용하여 네이티브 애플리케이션에서 .NET 런타임을 시작하고 관리되는 정적 메서드를 호출하는 방법에 대해 다음 단계에서 자세히 설명합니다.
이 샘플은 nethost
헤더와 라이브러리를 사용하며, .NET SDK와 함께 설치된 coreclr_delegates.h
및 hostfxr.h
헤더를 사용합니다.
1단계 - 내보낸 호스팅 함수 로드 hostfxr
및 가져오기
nethost
라이브러리는 get_hostfxr_path
라이브러리를 찾기 위한 hostfxr
함수를 제공합니다. 라이브러리는 hostfxr
.NET 런타임을 호스트하기 위한 함수를 노출합니다. 함수의 전체 목록은 네이티브 호스팅 디자인 문서에서 찾을 hostfxr.h
수 있습니다. 샘플 및 이 자습서에서는 다음을 사용합니다.
-
hostfxr_initialize_for_runtime_config
: 호스트 컨텍스트를 초기화하고 지정된 런타임 구성을 사용하여 .NET 런타임의 초기화를 준비합니다. -
hostfxr_get_runtime_delegate
: 런타임 기능에 대한 대리자를 가져옵니다. -
hostfxr_close
: 호스트 컨텍스트를 닫습니다.
hostfxr
라이브러리에서 nethost
API를 사용하여 get_hostfxr_path
라이브러리를 찾을 수 있습니다. 그런 다음 로드되고 내보내기가 검색됩니다.
// 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>
이러한 파일은 다음 위치에서 찾을 수 있습니다.
- https://github.com/dotnet/runtime/blob/main/src/native/corehost/nethost/nethost.h
- https://github.com/dotnet/runtime/blob/main/src/native/corehost/coreclr_delegates.h
- https://github.com/dotnet/runtime/blob/main/src/native/corehost/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_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));
제한점
하나의 프로세스 내에 하나의 런타임만 로드할 수 있습니다. 런타임이 hostfxr_initialize_for_runtime_config
이미 로드될 때 API가 호출되면 기존 런타임이 지정된 초기화 매개 변수와 호환되는지 확인합니다. 호환되는 경우 기존 런타임이 사용되고 호환되지 않는 경우 API는 오류를 반환합니다.
.NET