在 Windows 和 Linux 的非托管 C/C++ 进程中托管 .NET Core 组件





5.00/5 (9投票s)
为在非托管 C/C++ 代码中自定义托管 .NET Core 组件提供了紧凑的基础设施,并在 Windows 和 Linux 上运行的部分之间实现了双向方法调用。章节:线程与进程。
- 下载 Windows 演示 - 21.6 KB (请参阅 Read_Me.txt 文件)
- 下载 Linux 演示 - 19 KB (请参阅 Read_Me.txt 文件)
- 下载源代码 - 18.8 KB
为什么?
在编写第一个 .NET Core 应用程序时,最引人注目的是应用程序的执行方式。即使是 .NET Core 控制台应用程序,它也编译成 DLL 文件,而不是 EXE 文件。它通过 `dotnet` 命令运行,为我们的 DLL 执行提供了一个 `标准主机`。这时可能会出现一个问题:创建自定义主机来执行 .NET Core 应用程序是否有益?除了假设的优化和性能提升外,我认为自定义托管还有两个好处。
第一个是保护 .NET Core 二进制文件免受反射的侵害。正如你所见,普通的 .NET Framework 反射器无法反射 .NET Core DLL。但是,编写一个专门针对 .NET Core 的反射器相对容易,这与 Lutz Roeder 在 .NET 早期为 Framework 二进制文件所做的工作类似。.NET Core 反射器不会太受欢迎,因为在绝大多数情况下,.NET Core 代码都在服务器端运行,因此无法通过代码查找反射来访问。但在 .NET Core DLL 在客户端运行时,反反射保护可能会非常有用。最简单的情况下,可以通过自定义托管实现这种保护。磁盘上保存的 DLL 中的字节会根据某种算法进行重排,然后自定义主机将字节按正确的顺序放入内存。类似的想法在我的文章 Anti-Reflector .NET Code Protection 中已为 Windows 上的 .NET Framework 实现。
第二个(也是我认为更重要的)好处是与旧软件的升级相结合。确实,周围堆积着大量的旧代码。数百万行用 ANSI C 编写的代码在 Windows 和 Linux 上都有构建,并成功地为客户服务至今。这些代码存在一个陷阱:重写它们需要巨大的资源和时间,很难找到人来维护这些“恐龙”,但这些“恐龙”仍然被大量使用。最近,一位朋友注意到一个生产中的 C 文件是 1993 年编写的。这让我想起了我的童年,1975 年,我参观了我父亲工作的冶金厂,在那里我看到了一台仍在运行的蒸汽机,上面写着“伯明翰 1895”(那是它服役的最后一天)。但与蒸汽机不同的是,这个 C 文件仍然可以运行,其产品仍然很受欢迎。摆脱遗留代码陷阱的一个有前途的方法是使用新的编程方法/语言来开发新功能,并逐步、小心地重写有问题的旧部分。这需要旧组件和新组件之间紧密无缝的协作。而自定义 .NET Core 托管可以提供这种协作。最近,我偶然发现了 这篇文章,它提供了关于 .NET Core 自定义托管的解释和代码示例。基于这篇工作中的代码,我将尝试提出一个更通用的 .NET Core 自定义托管基础设施。
本文提供的紧凑软件示例能够在非托管 C/C++ 代码中托管 .NET Core 组件,并确保各部分之间的双向方法调用。它在 Windows 和 Linux 上都能运行。
托管-非托管双向调用机制
为了托管 .NET Core 组件,非托管 C/C++ 原生应用程序包含一个 C++ 网关类 `GatewayToManaged`,其头文件 `GatewayToManaged.h` 如下所示。
#pragma once
#include "coreclrhost.h"
using namespace std;
// Function pointer types for the managed call and unmanaged callback
typedef bool (*unmanaged_callback_ptr)(const char* actionName, const char* jsonArgs);
typedef char* (*managed_direct_method_ptr)(const char* actionName,
const char* jsonArgs, unmanaged_callback_ptr unmanagedCallback);
class GatewayToManaged
{
public:
GatewayToManaged();
~GatewayToManaged();
bool Init(const char* path);
char* Invoke(const char* funcName, const char* jsonArgs, unmanaged_callback_ptr unmanagedCallback);
bool Close();
private:
void* _hostHandle;
unsigned int _domainId;
managed_direct_method_ptr _managedDirectMethod;
void BuildTpaList(const char* directory, const char* extension, string& tpaList);
managed_direct_method_ptr CreateManagedDelegate();
#if WINDOWS
HMODULE _coreClr;
#elif LINUX
void* _coreClr;
#endif
};
它的 `Init()` 方法接受应用程序的执行路径并执行以下操作:
- 加载 `CoreCLR` 组件;
- 构建受信任的程序集 (TPA) 列表;
- 定义主 `CoreCLR` 属性;
- 启动 .NET Core 运行时并创建默认(且唯一的)`AppDomain`;最后
- 创建一个对象 `managed_direct_method_ptr _managedDirectMethod`,允许调用托管的委托。
为了实现任何直接的非托管到托管调用以及托管到非托管回调的统一机制,我们为每种情况定义一种类型(签名)。上面提供的头文件 `GatewayToManaged.h` 提供了这些类型:`managed_direct_method_ptr` 用于直接托管委托,`unmanaged_callback_ptr` 用于非托管回调。
因此,要从非托管代码调用托管方法,我们需要:
- 创建 `GatewayToManaged` 类的对象;
- 调用其 `Init()` 方法;然后
- 使用 `Invoke()` 方法执行实际的托管调用,该方法接受托管函数的名称及其参数作为 JSON 字符串对象,指向非托管回调,并返回 `char*`。
在托管端组件(DLL)中,`GatewayLib` 包含 `public static class Gateway`,它提供了如下所示的 `ManagedDirectMethod()` 方法。
// This method is called from unmanaged code
[return: MarshalAs(UnmanagedType.LPStr)]
public static string ManagedDirectMethod(
[MarshalAs(UnmanagedType.LPStr)] string funcName,
[MarshalAs(UnmanagedType.LPStr)] string jsonArgs,
UnmanagedCallbackDelegate dlgUnmanagedCallback)
{
_logger.Info($"ManagedDirectMethod(actionName: {funcName}, jsonArgs: {jsonArgs}");
string strRet = null;
if (_worker.Functions.TryGetValue(funcName,
out Func<string, string, UnmanagedCallbackDelegate, string> directCall))
{
try
{
strRet = directCall(funcName, jsonArgs, dlgUnmanagedCallback);
}
catch (Exception e)
{
strRet = $"ERROR in \"{funcName}\" invoke:{Environment.NewLine} {e}";
_logger.Error(strRet);
}
}
return strRet;
}
如上所示,此方法从非托管代码调用。
方法 `ManagedDirectMethod()` 使用对象 `IWorker _worker` 激活由非托管调用者指定的实际方法。接口 `IWorker` 提供了一个字典 getter `Dictionary<string, Func<string, string, UnmanagedCallbackDelegate, string>> Functions { get; }`。所需的非托管调用处理程序通过 `funcName` 键从字典中检索,并使用 `directCall()` 调用。我们假设字典 `_worker.Functions` 在对象构造时已填充,并且以后不会更改,因此它是线程安全的。
参数 `dlgUnmanagedCallback` 会传递给处理程序,以后可用于异步调用非托管代码。在我们的示例中,托管到非托管调用的处理是在 `callback.c` 文件中的普通 C 函数 `bool UnmanagedCallback(const char* actionName, const char* jsonArgs)` 中实现的。为了保持统一,双方调用的参数都作为 JSON `string` 提供。为了在 C 代码中解析 JSON,我使用了 这个开源 JSON 解析器。
通过提供的简单基础设施,可以轻松地在双方编写代码。非托管部分应创建 `GatewayToManaged` 类的对象并运行其 `Init()` 方法。之后,网关对象就可以调用托管方法了。如果非托管代码期望托管部分会调用它,它还应该提供适当的回调。托管部分只需实现要从其非托管主机调用的方法。这些方法将使用非托管回调(它们作为参数获得)来通知主机。
如何运行代码示例
该示例已使用 .NET Core 2.1.7 版本构建和测试。但我认为它在 2.2 版本或 2.1 的任何其他版本中都可以正常工作。
重要提示。在示例中,Core CLR 目录是硬编码的。它定义为:
- Windows 为 `C:\Program Files\dotnet\shared\Microsoft.NETCore.App\2.1.7`;
- Linux 为 `/usr/share/dotnet/shared/Microsoft.NETCore.App/2.1.7`。
如果您的机器上的位置不同,则演示将无法工作,并且源代码应使用正确的位置重新构建(即在 `GatewayToManaged.cpp` 文件中定义 `CORECLR_DIR`)。
要运行演示,请解压缩它,然后运行主机应用程序文件,即 Windows 的 `Host.exe` 和 Linux 的 `Host.out`。应用程序将在控制台输出。来自非托管代码的消息将以您的控制台标准颜色显示,而托管代码的消息将以青色显示。双方都将 `Device` 类及其 C 代码中的非托管对等体 `struct Device` 的对象传递。非托管主机(通过 `Gateway.ManagedDirectMethod()` 中介)调用 `Worker` 组件的方法 `GetDevice()`、`SubscribeForDevice()` 和 `UnsubscribeFromDevice()`。执行时,第一个方法调用一次非托管回调,而第二个方法启动一个计时器,该计时器重复调用非托管回调,提供从托管部分到非托管部分的设备数据流。第三个方法用于取消订阅设备数据流(为保持简单,它实际上会停止计时器)。
源代码应按如下方式构建和运行:
Windows
定义了正确的 Core CLR 目录位置后,请构建整个解决方案。
重要提示。`CoreCLR` 的位数(32 位或 64 位)应与构建主机的位数匹配。在我们的例子中,整个解决方案应以 **x64** 模式构建。
结果将放在您的 `$(SolutionDir)\x64` 目录中。从那里运行 `Host.exe`。
Linux
由于我的开发环境是 Windows,我将描述如何将相应的文件移动到 Linux(我使用的是 Ubuntu 18.04)并在那里执行额外的操作。创建目录 `Hosting`,然后将 Windows 目录 `$(SolutionDir)\Host` 中的所有 `*.h`、`*.c` 和 `*.cpp` 文件以及 Windows 目录 `$(SolutionDir)\x64` 中的所有 [托管] DLL 文件复制到其中。托管 DLL 将按原样使用,非托管代码应进行构建。后者使用以下命令完成:
g++ -o Host.out -D LINUX jsmn.c GatewayToManaged.cpp SampleHost.cpp -ldl
这将生成 `Host.out` 文件,该文件应通过 `./Host.out` 命令运行。
结论
这项工作提供了一个紧凑的基础设施,用于在非托管 C/C++ 代码中自定义托管 .NET Core 组件,并确保各部分之间的双向方法调用。它在 Windows 和 Linux 上都能运行。这种方法可能有助于 .NET Core 代码的反反射保护,以及最重要的是,用 .NET Core 编写的新功能来升级遗留代码。