编写自定义 .NET Core 主机以从本机代码控制 .NET 运行时Write a custom .NET Core host to control the .NET runtime from your native code

本文内容

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

托管 .NET Core 运行时是高级方案,在大多数情况下,.NET Core 开发人员无需担心托管问题,因为 .NET Core 生成过程会提供默认主机来运行 .NET Core 应用程序。虽然在某些特殊情况下,它对显式托管 .NET Core 运行时非常有用 - 无论是作为一种在本机进程中调用托管代码的方式还是为了获得对运行时工作原理更好的控制。

本文概述了从本机代码启动 .NET Core 运行时和在其中执行托管代码的必要步骤。

系统必备Prerequisites

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

还将需要一个简单的 .NET Core 应用程序来测试主机,因此应安装 .NET Core SDK构建一个小型的 .NET Core 测试应用(例如,“Hello World”应用)。使用通过新 .NET Core 控制台项目模板创建的“Hello World”应用就足够了。

承载 APIHosting APIs

可以使用三种不同的 API 来托管 .NET Core。本文档(及其相关的示例)涵盖所有选项。

  • 在 .NET Core 3.0 及更高版本中托管 .NET Core 运行时的首选方法是借助 nethosthostfxr 库的 API。由这些入口点来处理查找和设置运行时进行初始化所遇到的复杂性;通过它们,还可启动托管应用程序和调用静态托管方法。
  • 托管低于 .NET Core 3.0 的 .NET Core 运行时的首选方法是使用 CoreClrHost.h API。此 API 公开一些函数,用于轻松地启动和停止运行时并调用托管代码(通过启动托管 exe 或通过调用静态托管方法)。
  • 也可以使用 mscoree.h 中的 ICLRRuntimeHost4 接口承载 .NET Core。此 API 比 CoreClrHost.h 出现的时间更早,因此你可能看到过使用此 API 的较旧版本的主机。它仍然可用,并且比起 CoreClrHost,它可以对主机进程进行更多控制。但是对于大多数方案,CoreClrHost.h 现在是首选,因为它的 API 更简单。

示例主机Sample Hosts

有关展示在下面的教程中所述步骤的示例主机,请访问 dotnet/samples GitHub 存储库。该示例中的注释清楚地将这些教程中已编号的步骤与它们在示例中的执行位置关联。有关下载说明,请参阅示例和教程

请记住,示例主机的用途在于提供学习指导,在纠错方面不甚严谨,其重在可读性而非效率。

使用 NetHost.h 和 HostFxr.h 创建主机Create a host using NetHost.h and HostFxr.h

以下步骤详细说明如何使用 nethosthostfxr 库在本机应用程序中启动 .NET Core 运行时并调用托管静态方法。示例使用安装了 .NET SDK 的 nethost 标头和库,并从 dotnet/core-setup 复制 coreclr_delegates.hhostfxr.h 文件。

步骤 1 - 加载 HostFxr 并获取导出的托管函数Step 1 - Load HostFxr and get exported hosting functions

nethost 库提供用于查找 hostfxr 库的 get_hostfxr_path 函数。hostfxr 库公开用于托管 .NET Core 运行时的函数。函数的完整列表可在 hostfxr.h本机托管设计文档中找到。示例和本教程使用以下函数:

  • hostfxr_initialize_for_runtime_config:初始化主机上下文,并使用指定的运行时配置准备初始化 .NET Core 运行时。
  • hostfxr_get_runtime_delegate:获取对运行时功能的委托。
  • hostfxr_close:关闭主机上下文。使用 get_hostfxr_path 找到了 hostfxr 库。随后加载此库并检索其导出。
  1. // Using the nethost library, discover the location of hostfxr and get exports
  2. bool load_hostfxr()
  3. {
  4. // Pre-allocate a large buffer for the path to hostfxr
  5. char_t buffer[MAX_PATH];
  6. size_t buffer_size = sizeof(buffer) / sizeof(char_t);
  7. int rc = get_hostfxr_path(buffer, &buffer_size, nullptr);
  8. if (rc != 0)
  9. return false;
  10. // Load hostfxr and get desired exports
  11. void *lib = load_library(buffer);
  12. init_fptr = (hostfxr_initialize_for_runtime_config_fn)get_export(lib, "hostfxr_initialize_for_runtime_config");
  13. get_delegate_fptr = (hostfxr_get_runtime_delegate_fn)get_export(lib, "hostfxr_get_runtime_delegate");
  14. close_fptr = (hostfxr_close_fn)get_export(lib, "hostfxr_close");
  15. return (init_fptr && get_delegate_fptr && close_fptr);
  16. }

步骤 2 - 初始化和启动 .NET Core 运行时Step 2 - Initialize and start the .NET Core runtime

hostfxr_initialize_for_runtime_confighostfxr_get_runtime_delegate 函数使用将加载的托管组件的运行时配置初始化并启动 .NET Core 运行时。hostfxr_get_runtime_delegate 函数用于获取运行时委托,允许加载托管程序集并获取指向该程序集中的静态方法的函数指针。

  1. // Load and initialize .NET Core and get desired function pointer for scenario
  2. load_assembly_and_get_function_pointer_fn get_dotnet_load_assembly(const char_t *config_path)
  3. {
  4. // Load .NET Core
  5. void *load_assembly_and_get_function_pointer = nullptr;
  6. hostfxr_handle cxt = nullptr;
  7. int rc = init_fptr(config_path, nullptr, &cxt);
  8. if (rc != 0 || cxt == nullptr)
  9. {
  10. std::cerr << "Init failed: " << std::hex << std::showbase << rc << std::endl;
  11. close_fptr(cxt);
  12. return nullptr;
  13. }
  14. // Get the load assembly function pointer
  15. rc = get_delegate_fptr(
  16. cxt,
  17. hdt_load_assembly_and_get_function_pointer,
  18. &load_assembly_and_get_function_pointer);
  19. if (rc != 0 || load_assembly_and_get_function_pointer == nullptr)
  20. std::cerr << "Get delegate failed: " << std::hex << std::showbase << rc << std::endl;
  21. close_fptr(cxt);
  22. return (load_assembly_and_get_function_pointer_fn)load_assembly_and_get_function_pointer;
  23. }

步骤 3 - 加载托管程序集并获取指向托管方法的函数指针Step 3 - Load managed assembly and get function pointer to a managed method

将调用运行时委托以加载托管程序集并获取指向托管方法的函数指针。委托需要程序集路径、类型名称和方法名称作为输入,并返回可用于调用托管方法的函数指针。

  1. // Function pointer to managed delegate
  2. component_entry_point_fn hello = nullptr;
  3. int rc = load_assembly_and_get_function_pointer(
  4. dotnetlib_path.c_str(),
  5. dotnet_type,
  6. dotnet_type_method,
  7. nullptr /*delegate_type_name*/,
  8. nullptr,
  9. (void**)&hello);

该示例通过在调用运行时委托时将 nullptr 作为委托类型名称传递,对托管方法使用默认签名:

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

可以通过在调用运行时委托时指定委托类型名称来使用其他签名。

步骤 4 - 运行托管代码!Step 4 - Run managed code!

本机主机现在可以调用托管方法,并向其传递所需的参数。

  1. lib_args args
  2. {
  3. STR("from host!"),
  4. i
  5. };
  6. hello(&args, sizeof(args));

使用 CoreClrHost.h 创建主机Create a host using CoreClrHost.h

以下步骤详细说明如何使用 CoreClrHost.h API 在本机应用程序中启动 .NET Core 运行时并调用托管静态方法。本文档中的代码片段使用一些特定于 Windows 的 API,但是完整示例主机同时显示 Windows 和 Linux 的代码路径。

Unix CoreRun 主机显示使用 coreclrhost.h 的更为复杂的真实托管示例。

步骤 1 - 查找和加载 CoreCLRStep 1 - Find and load CoreCLR

.NET Core 运行时 API 位于 coreclr.dll(对于 Windows)、libcoreclr.so(对于 Linux)或 libcoreclr.dylib(对于 macOS) 。承载 .NET Core 的第一步是加载 CoreCLR 库。一些主机探测不同的路径或使用输入参数来查找库,而另一些主机能够从某个路径(例如,紧邻主机的路径,或从计算机范围内的位置)加载库。

找到库之后,系统会使用 LoadLibraryEx(对于 Windows)或 dlopen(对于 Linux/Mac)加载库。

  1. HMODULE coreClr = LoadLibraryExA(coreClrPath.c_str(), NULL, 0);

步骤 2 - 获取 .NET Core 承载函数Step 2 - Get .NET Core hosting functions

CoreClrHost 有几个可用于承载 .NET Core 的重要方法:

  • coreclr_initialize:启动 .NET Core 运行时并设置默认(且仅设置)AppDomain。
  • coreclr_execute_assembly:执行托管程序集。
  • coreclr_create_delegate:创建指向托管方法的函数指针。
  • coreclr_shutdown:关闭 .NET Core 运行时。
  • coreclr_shutdown_2:如 coreclr_shutdown,但还会检索托管代码的退出代码。加载 CoreCLR 库之后,下一步是使用 GetProcAddress(对于 Windows)或 dlsym(对于 Linux/Mac)引用这些函数。
  1. coreclr_initialize_ptr initializeCoreClr = (coreclr_initialize_ptr)GetProcAddress(coreClr, "coreclr_initialize");
  2. coreclr_create_delegate_ptr createManagedDelegate = (coreclr_create_delegate_ptr)GetProcAddress(coreClr, "coreclr_create_delegate");
  3. coreclr_shutdown_ptr shutdownCoreClr = (coreclr_shutdown_ptr)GetProcAddress(coreClr, "coreclr_shutdown");

步骤 3 - 准备运行时属性Step 3 - Prepare runtime properties

在启动运行时之前,有必要准备一些属性来指定行为(特别是关于程序集加载器的行为)。

常用属性包括:

  • TRUSTED_PLATFORM_ASSEMBLIES 这是程序集路径列表(对于 Windows,使用“;”分隔,对于 Linux,使用“:”分隔),运行时在默认情况下能够解析这些路径。一些主机有硬编码清单,其中列出了它们可以加载的程序集。其他主机将把任何库放在这个列表上的特定位置(例如 coreclr.dll 旁边) 。
  • APP_PATHS 这是一个用来探测程序集的路径的列表(如果在受信任的平台程序集 (TPA) 列表中找不到程序集)。因为主机使用 TPA 列表可以更好地控制加载哪些程序集,所以对于主机来说,确定要加载的程序集并显式列出它们是最佳做法。但是,如果需要探测运行时,则此属性可以支持该方案。
  • APP_NI_PATHS 此列表与 APP_PATHS 相似,不同之处在于其中的路径用于探测本机映像。
  • NATIVE_DLL_SEARCH_DIRECTORIES 此属性是一个路径列表,加载程序在查找通过 p/invoke 调用的本机库时应使用这些路径进行探测。
  • PLATFORM_RESOURCE_ROOTS 此列表包含的路径用于探测资源附属程序集(在区域性特定的子目录中)。在此示例主机中,TPA 列表是通过简单列出当前目录中的所有库来进行构造的:
  1. void BuildTpaList(const char* directory, const char* extension, std::string& tpaList)
  2. {
  3. // This will add all files with a .dll extension to the TPA list.
  4. // This will include unmanaged assemblies (coreclr.dll, for example) that don't
  5. // belong on the TPA list. In a real host, only managed assemblies that the host
  6. // expects to load should be included. Having extra unmanaged assemblies doesn't
  7. // cause anything to fail, though, so this function just enumerates all dll's in
  8. // order to keep this sample concise.
  9. std::string searchPath(directory);
  10. searchPath.append(FS_SEPARATOR);
  11. searchPath.append("*");
  12. searchPath.append(extension);
  13. WIN32_FIND_DATAA findData;
  14. HANDLE fileHandle = FindFirstFileA(searchPath.c_str(), &findData);
  15. if (fileHandle != INVALID_HANDLE_VALUE)
  16. {
  17. do
  18. {
  19. // Append the assembly to the list
  20. tpaList.append(directory);
  21. tpaList.append(FS_SEPARATOR);
  22. tpaList.append(findData.cFileName);
  23. tpaList.append(PATH_DELIMITER);
  24. // Note that the CLR does not guarantee which assembly will be loaded if an assembly
  25. // is in the TPA list multiple times (perhaps from different paths or perhaps with different NI/NI.dll
  26. // extensions. Therefore, a real host should probably add items to the list in priority order and only
  27. // add a file if it's not already present on the list.
  28. //
  29. // For this simple sample, though, and because we're only loading TPA assemblies from a single path,
  30. // and have no native images, we can ignore that complication.
  31. }
  32. while (FindNextFileA(fileHandle, &findData));
  33. FindClose(fileHandle);
  34. }
  35. }

因为该示例简单,所以只需要 TRUSTED_PLATFORM_ASSEMBLIES 属性:

  1. // Define CoreCLR properties
  2. // Other properties related to assembly loading are common here,
  3. // but for this simple sample, TRUSTED_PLATFORM_ASSEMBLIES is all
  4. // that is needed. Check hosting documentation for other common properties.
  5. const char* propertyKeys[] = {
  6. "TRUSTED_PLATFORM_ASSEMBLIES" // Trusted assemblies
  7. };
  8. const char* propertyValues[] = {
  9. tpaList.c_str()
  10. };

步骤 4 - 启动运行时Step 4 - Start the runtime

与 mscoree.h hosting API(上面所述)不同,CoreCLRHost.h API 使用一个调用启动运行时并创建默认的 AppDomain。coreclr_initialize 函数采用基本路径、名称和前面描述的属性,并通过 hostHandle 参数将图柄返回到主机。

  1. void* hostHandle;
  2. unsigned int domainId;
  3. // This function both starts the .NET Core runtime and creates
  4. // the default (and only) AppDomain
  5. int hr = initializeCoreClr(
  6. runtimePath, // App base path
  7. "SampleHost", // AppDomain friendly name
  8. sizeof(propertyKeys) / sizeof(char*), // Property count
  9. propertyKeys, // Property names
  10. propertyValues, // Property values
  11. &hostHandle, // Host handle
  12. &domainId); // AppDomain ID

步骤 5 - 运行托管代码!Step 5 - Run managed code!

启动运行时之后,主机可以调用托管代码。这可以通过两种不同的方法实现。与本教程相关的示例代码使用 coreclr_create_delegate 函数创建静态托管方法的委托。此 API 采用程序集名称、符合命名空间条件的类型名称和方法名称作为输入,并返回可用于调用该方法的委托。

  1. doWork_ptr managedDelegate;
  2. // The assembly name passed in the third parameter is a managed assembly name
  3. // as described at https://docs.microsoft.com/dotnet/framework/app-domains/assembly-names
  4. hr = createManagedDelegate(
  5. hostHandle,
  6. domainId,
  7. "ManagedLibrary, Version=1.0.0.0",
  8. "ManagedLibrary.ManagedWorker",
  9. "DoWork",
  10. (void**)&managedDelegate);

在此示例中,主机现在可以调用 managedDelegate 来运行 ManagedWorker.DoWork 方法。

或者,可以使用 coreclr_execute_assembly 函数启动托管可执行文件。此 API 采用程序集路径和实参数组作为输入形参。它在该路径加载程序集并调用其主方法。

  1. int hr = executeAssembly(
  2. hostHandle,
  3. domainId,
  4. argumentCount,
  5. arguments,
  6. "HelloWorld.exe",
  7. (unsigned int*)&exitCode);

步骤 6 - 关闭和清理Step 6 - Shutdown and clean up

最后,主机完成运行托管代码后,使用 coreclr_shutdowncoreclr_shutdown_2 关闭 .NET Core 运行时。

  1. hr = shutdownCoreClr(hostHandle, domainId);

CoreCLR 不支持重新初始化或卸载。请勿重新调用 coreclr_initialize 或卸载 CoreCLR 库。

使用 Mscoree.h 创建主机Create a host using Mscoree.h

如前所述,CoreClrHost.h 现在是承载 .NET Core 运行时的首选方法。但是,如果 CoreClrHost.h 接口不够用(例如,需要非标准的启动标志,或者在默认域上需要 AppDomainManager),仍然可以使用 ICLRRuntimeHost4 接口。这些说明将指导你使用 mscoree.h 执行承载 .NET Core 的操作。

CoreRun 主机显示使用 mscoree.h 的更为复杂的真实托管示例。

有关 mscoree.h 的说明A note about mscoree.h

ICLRRuntimeHost4 .NET Core 承载接口在 MSCOREE.IDL 中定义。主机需要引用的此文件 (mscoree.h) 的标头版本,该版本是在构建 .NET Core 运行时时通过 MIDL 生成。如果不想构建 .NET Core 运行时,还可在 dotnet/coreclr 存储库中将 mscoree.h 获取为预生成的标头有关构建 .NET Core 运行时的说明可在其 GitHub 存储库中找到。

步骤 1 - 标识托管的入口点Step 1 - Identify the managed entry point

引用必要的标头后(例如,mscoree.h 和 stdio.h),.NET Core 主机必须完成的首要任务之一就是找到要使用的托管入口点。在示例主机中,通过将主机的第一个命令行参数作为托管的二进制文件(将执行该文件的 main 方法)的路径,即可完成此操作。

  1. // The managed application to run should be the first command-line parameter.
  2. // Subsequent command line parameters will be passed to the managed app later in this host.
  3. wchar_t targetApp[MAX_PATH];
  4. GetFullPathNameW(argv[1], MAX_PATH, targetApp, NULL);

步骤 2 - 查找和加载 CoreCLRStep 2 - Find and load CoreCLR

.NET Core 运行时 API 位于 CoreCLR.dll(在 Windows 上)中。若要获取托管接口 (ICLRRuntimeHost4),就必须查找并加载 CoreCLR.dll由主机定义用于规定 CoreCLR.dll 查找方式的约定。一些主机会预期文件位于一个常用的计算机范围内的位置(如 %programfiles%\dotnet\shared\Microsoft.NETCore.App\2.1.6) 。其他主机会预期 CoreCLR.dll 从主机本身或要托管的应用旁的某个位置进行加载。还有一些主机可能会参考环境变量来查找库。

在 Linux 或 Mac 上,核心运行时库分别是 libcoreclr.so 或者 libcoreclr.dylib

示例主机会为 CoreCLR.dll 探测几个常用位置。找到后,必须通过 LoadLibrary(在 Linux/Mac 上通过 dlopen)进行加载。

  1. HMODULE ret = LoadLibraryExW(coreDllPath, NULL, 0);

步骤 3 - 获取 ICLRRuntimeHost4 实例Step 3 - Get an ICLRRuntimeHost4 Instance

通过在 GetCLRRuntimeHost 上调用 GetProcAddress(或在 Linux/Mac 上调用 dlsym),然后再调用该函数来检索 ICLRRuntimeHost4 托管接口。

  1. ICLRRuntimeHost4* runtimeHost;
  2. FnGetCLRRuntimeHost pfnGetCLRRuntimeHost =
  3. (FnGetCLRRuntimeHost)::GetProcAddress(coreCLRModule, "GetCLRRuntimeHost");
  4. if (!pfnGetCLRRuntimeHost)
  5. {
  6. printf("ERROR - GetCLRRuntimeHost not found");
  7. return -1;
  8. }
  9. // Get the hosting interface
  10. HRESULT hr = pfnGetCLRRuntimeHost(IID_ICLRRuntimeHost4, (IUnknown**)&runtimeHost);

步骤 4 - 设置启动标志并启动运行时Step 4 - Set startup flags and start the runtime

有了 ICLRRuntimeHost4,现在便可指定运行时范围内的启动标志并启动该运行时。启动标志决定要使用的垃圾回收器 (GC)(并发垃圾回收器或服务器)、是使用单个 AppDomain 还是多个 Appdomain,以及要使用的加载程序优化策略(对于非特定于域的程序集加载)。

  1. hr = runtimeHost->SetStartupFlags(
  2. // These startup flags control runtime-wide behaviors.
  3. // A complete list of STARTUP_FLAGS can be found in mscoree.h,
  4. // but some of the more common ones are listed below.
  5. static_cast<STARTUP_FLAGS>(
  6. // STARTUP_FLAGS::STARTUP_SERVER_GC | // Use server GC
  7. // STARTUP_FLAGS::STARTUP_LOADER_OPTIMIZATION_MULTI_DOMAIN | // Maximize domain-neutral loading
  8. // STARTUP_FLAGS::STARTUP_LOADER_OPTIMIZATION_MULTI_DOMAIN_HOST | // Domain-neutral loading for strongly-named assemblies
  9. STARTUP_FLAGS::STARTUP_CONCURRENT_GC | // Use concurrent GC
  10. STARTUP_FLAGS::STARTUP_SINGLE_APPDOMAIN | // All code executes in the default AppDomain
  11. // (required to use the runtimeHost->ExecuteAssembly helper function)
  12. STARTUP_FLAGS::STARTUP_LOADER_OPTIMIZATION_SINGLE_DOMAIN // Prevents domain-neutral loading
  13. )
  14. );

通过调用 Start 函数启动运行时。

  1. hr = runtimeHost->Start();

步骤 5 - 准备 AppDomain 设置Step 5 - Preparing AppDomain settings

启动运行时后,将需要设置 AppDomain。但创建 .NET AppDomain 时必须指定大量选项,因此必须先准备这些选项。

AppDomain 标志指定与安全性和互操作性相关的 AppDomain 行为。早期 Silverlight 主机对沙盒用户代码使用这些设置,但大多数现代 .NET Core 主机以完全信任的方式运行用户代码并启用互操作。

  1. int appDomainFlags =
  2. // APPDOMAIN_FORCE_TRIVIAL_WAIT_OPERATIONS | // Do not pump messages during wait
  3. // APPDOMAIN_SECURITY_SANDBOXED | // Causes assemblies not from the TPA list to be loaded as partially trusted
  4. APPDOMAIN_ENABLE_PLATFORM_SPECIFIC_APPS | // Enable platform-specific assemblies to run
  5. APPDOMAIN_ENABLE_PINVOKE_AND_CLASSIC_COMINTEROP | // Allow PInvoking from non-TPA assemblies
  6. APPDOMAIN_DISABLE_TRANSPARENCY_ENFORCEMENT; // Entirely disables transparency checks

确定要使用的 AppDomain 标志后,必须定义 AppDomain 属性。该属性是字符串的键/值对。这些属性中的许多与 AppDomain 程序集的加载方式相关。

常见 AppDomain 属性包括:

  • TRUSTEDPLATFORM_ASSEMBLIES 这是一个程序集路径的列表(在 Windows 上以 ; 分隔,在 Linux/Mac 上以 : 分隔),AppDomain 应优先加载它们并对其授予完全信任(甚至在部分受信任域中也一样)。此列表应包含“框架”程序集和其他受信任的模块,与 .NET Framework 方案中的 GAC 类似。一些主机会将任何库置于此列表上的 _coreclr.dll 旁,其他主机具有硬编码的清单,其中列出了用于所需用途的受信任程序集。
  • APP_PATHS 这是一个用来探测程序集的路径的列表(如果在受信任的平台程序集 (TPA) 列表中找不到程序集)。因为主机使用 TPA 列表可以更好地控制加载哪些程序集,所以对于主机来说,确定要加载的程序集并显式列出它们是最佳做法。但是,如果需要探测运行时,则此属性可以支持该方案。
  • APP_NI_PATHS 此列表与 APP_PATHS 非常相似,不同之处在于其中的路径用于探测本机映像。
  • NATIVE_DLL_SEARCH_DIRECTORIES 此属性是一个路径列表,加载程序在查找通过 p/invoke 调用的本机 DLL 时应使用这些路径进行探测。
  • PLATFORM_RESOURCE_ROOTS 此列表包含的路径用于探测资源附属程序集(在区域性特定的子目录中)。简单示例主机中,这些属性将进行如下设置:
  1. // TRUSTED_PLATFORM_ASSEMBLIES
  2. // "Trusted Platform Assemblies" are prioritized by the loader and always loaded with full trust.
  3. // A common pattern is to include any assemblies next to CoreCLR.dll as platform assemblies.
  4. // More sophisticated hosts may also include their own Framework extensions (such as AppDomain managers)
  5. // in this list.
  6. size_t tpaSize = 100 * MAX_PATH; // Starting size for our TPA (Trusted Platform Assemblies) list
  7. wchar_t* trustedPlatformAssemblies = new wchar_t[tpaSize];
  8. trustedPlatformAssemblies[0] = L'\0';
  9. // Extensions to probe for when finding TPA list files
  10. const wchar_t *tpaExtensions[] = {
  11. L"*.dll",
  12. L"*.exe",
  13. L"*.winmd"
  14. };
  15. // Probe next to CoreCLR.dll for any files matching the extensions from tpaExtensions and
  16. // add them to the TPA list. In a real host, this would likely be extracted into a separate function
  17. // and perhaps also run on other directories of interest.
  18. for (int i = 0; i < _countof(tpaExtensions); i++)
  19. {
  20. // Construct the file name search pattern
  21. wchar_t searchPath[MAX_PATH];
  22. wcscpy_s(searchPath, MAX_PATH, coreRoot);
  23. wcscat_s(searchPath, MAX_PATH, L"\\");
  24. wcscat_s(searchPath, MAX_PATH, tpaExtensions[i]);
  25. // Find files matching the search pattern
  26. WIN32_FIND_DATAW findData;
  27. HANDLE fileHandle = FindFirstFileW(searchPath, &findData);
  28. if (fileHandle != INVALID_HANDLE_VALUE)
  29. {
  30. do
  31. {
  32. // Construct the full path of the trusted assembly
  33. wchar_t pathToAdd[MAX_PATH];
  34. wcscpy_s(pathToAdd, MAX_PATH, coreRoot);
  35. wcscat_s(pathToAdd, MAX_PATH, L"\\");
  36. wcscat_s(pathToAdd, MAX_PATH, findData.cFileName);
  37. // Check to see if TPA list needs expanded
  38. if (wcsnlen(pathToAdd, MAX_PATH) + (3) + wcsnlen(trustedPlatformAssemblies, tpaSize) >= tpaSize)
  39. {
  40. // Expand, if needed
  41. tpaSize *= 2;
  42. wchar_t* newTPAList = new wchar_t[tpaSize];
  43. wcscpy_s(newTPAList, tpaSize, trustedPlatformAssemblies);
  44. trustedPlatformAssemblies = newTPAList;
  45. }
  46. // Add the assembly to the list and delimited with a semi-colon
  47. wcscat_s(trustedPlatformAssemblies, tpaSize, pathToAdd);
  48. wcscat_s(trustedPlatformAssemblies, tpaSize, L";");
  49. // Note that the CLR does not guarantee which assembly will be loaded if an assembly
  50. // is in the TPA list multiple times (perhaps from different paths or perhaps with different NI/NI.dll
  51. // extensions. Therefore, a real host should probably add items to the list in priority order and only
  52. // add a file if it's not already present on the list.
  53. //
  54. // For this simple sample, though, and because we're only loading TPA assemblies from a single path,
  55. // we can ignore that complication.
  56. }
  57. while (FindNextFileW(fileHandle, &findData));
  58. FindClose(fileHandle);
  59. }
  60. }
  61. // APP_PATHS
  62. // App paths are directories to probe in for assemblies which are not one of the well-known Framework assemblies
  63. // included in the TPA list.
  64. //
  65. // For this simple sample, we just include the directory the target application is in.
  66. // More complex hosts may want to also check the current working directory or other
  67. // locations known to contain application assets.
  68. wchar_t appPaths[MAX_PATH * 50];
  69. // Just use the targetApp provided by the user and remove the file name
  70. wcscpy_s(appPaths, targetAppPath);
  71. // APP_NI_PATHS
  72. // App (NI) paths are the paths that will be probed for native images not found on the TPA list.
  73. // It will typically be similar to the app paths.
  74. // For this sample, we probe next to the app and in a hypothetical directory of the same name with 'NI' suffixed to the end.
  75. wchar_t appNiPaths[MAX_PATH * 50];
  76. wcscpy_s(appNiPaths, targetAppPath);
  77. wcscat_s(appNiPaths, MAX_PATH * 50, L";");
  78. wcscat_s(appNiPaths, MAX_PATH * 50, targetAppPath);
  79. wcscat_s(appNiPaths, MAX_PATH * 50, L"NI");
  80. // NATIVE_DLL_SEARCH_DIRECTORIES
  81. // Native dll search directories are paths that the runtime will probe for native DLLs called via PInvoke
  82. wchar_t nativeDllSearchDirectories[MAX_PATH * 50];
  83. wcscpy_s(nativeDllSearchDirectories, appPaths);
  84. wcscat_s(nativeDllSearchDirectories, MAX_PATH * 50, L";");
  85. wcscat_s(nativeDllSearchDirectories, MAX_PATH * 50, coreRoot);
  86. // PLATFORM_RESOURCE_ROOTS
  87. // Platform resource roots are paths to probe in for resource assemblies (in culture-specific sub-directories)
  88. wchar_t platformResourceRoots[MAX_PATH * 50];
  89. wcscpy_s(platformResourceRoots, appPaths);

步骤 6 - 创建 AppDomainStep 6 - Create the AppDomain

所有 AppDomain 标志和属性都准备都就绪后,可使用 ICLRRuntimeHost4::CreateAppDomainWithManager 设置 AppDomain。此函数选择性地采用完全限定的程序集名称和类型名称作为域的 AppDomain 管理器。AppDomain 管理器可允许主机控制 AppDomain 行为的某些方面,并且如果主机不打算直接调用用户代码,它可能会提供用于启动托管代码的入口点。

  1. DWORD domainId;
  2. // Setup key/value pairs for AppDomain properties
  3. const wchar_t* propertyKeys[] = {
  4. L"TRUSTED_PLATFORM_ASSEMBLIES",
  5. L"APP_PATHS",
  6. L"APP_NI_PATHS",
  7. L"NATIVE_DLL_SEARCH_DIRECTORIES",
  8. L"PLATFORM_RESOURCE_ROOTS"
  9. };
  10. // Property values which were constructed in step 5
  11. const wchar_t* propertyValues[] = {
  12. trustedPlatformAssemblies,
  13. appPaths,
  14. appNiPaths,
  15. nativeDllSearchDirectories,
  16. platformResourceRoots
  17. };
  18. // Create the AppDomain
  19. hr = runtimeHost->CreateAppDomainWithManager(
  20. L"Sample Host AppDomain", // Friendly AD name
  21. appDomainFlags,
  22. NULL, // Optional AppDomain manager assembly name
  23. NULL, // Optional AppDomain manager type (including namespace)
  24. sizeof(propertyKeys) / sizeof(wchar_t*),
  25. propertyKeys,
  26. propertyValues,
  27. &domainId);

步骤 7 - 运行托管代码!Step 7 - Run managed code!

现在 AppDomain 启动并运行后,主机可以开始执行托管的代码。执行此操作的最简单方法是使用 ICLRRuntimeHost4::ExecuteAssembly 调用托管程序集的入口点方法。请注意,此函数仅适用于单一域方案。

  1. DWORD exitCode = -1;
  2. hr = runtimeHost->ExecuteAssembly(domainId, targetApp, argc - 1, (LPCWSTR*)(argc > 1 ? &argv[1] : NULL), &exitCode);

如果 ExecuteAssembly 不满足主机的需要,那么另一种方法是使用 CreateDelegate 创建指向静态托管方法的函数指针。这要求主机知道要调用的方法的签名(以创建函数指针类型),但允许主机调用代码而不是程序集的入口点。第二个参数中提供的程序集名称是要加载的库的完全托管程序集名称

  1. void *pfnDelegate = NULL;
  2. hr = runtimeHost->CreateDelegate(
  3. domainId,
  4. L"HW, Version=1.0.0.0, Culture=neutral", // Target managed assembly
  5. L"ConsoleApplication.Program", // Target managed type
  6. L"Main", // Target entry point (static method)
  7. (INT_PTR*)&pfnDelegate);
  8. ((MainMethodFp*)pfnDelegate)(NULL);

步骤 8 - 清理Step 8 - Clean up

最后,主机应随后通过卸载 Appdomain、停止运行时并释放 ICLRRuntimeHost4 引用来进行清理。

  1. runtimeHost->UnloadAppDomain(domainId, true /* Wait until unload complete */);
  2. runtimeHost->Stop();
  3. runtimeHost->Release();

CoreCLR 不支持卸载。请勿卸载 CoreCLR 库。

结束语Conclusion

构建主机后,可以通过从命令行运行主机并传递其所需的任何参数(例如,要运行用于 mscoree 示例主机的托管应用)来对主机进行测试。指定主机要运行的 .NET Core 应用时,请务必使用 dotnet build 生成的 .dll。dotnet publish 为独立应用程序生成的可执行文件(.exe 文件)实际上是默认的 .NET Core 主机(以便可直接从主流方案中的命令行启动应用);用户代码被编译为具有相同名称的 dll。

如果开始时操作不起作用,请再次检查 coreclr.dll 是否在主机预期的位置可用、是否 TPA 列表中包含了所有必需的框架库以及 CoreCLR 的位数(32 位或 64 位)是否匹配主机的构建方式。

托管 .NET Core 运行时是高级方案,许多开发人员并不需要实施这一方案,但对于那些需要从本机进程启动托管代码的人员,或需要更好地控制 .NET Core 运行时的行为的人员而言,它会非常有用。