在 Windows 和 Linux 的非托管 C/C++ 进程中托管 .NET Web 服务






4.91/5 (15投票s)
用于在非托管 C/C++ 代码中自定义托管 .NET Web 组件的紧凑型基础设施,支持运行在 Windows 和 Linux 的组件之间进行双向方法调用。
目录
简介
我之前的 CodeProject 文章 《在 Windows 和 Linux 的非托管 C/C++ 进程中托管 .NET Core 组件》 展示了将 .NET 代码放置在以 C/C++ 为主编写的本地 Windows 和 Linux 进程中的技术。最近,我注意到开发社区对此主题的兴趣日益增加(文章代码的下载量稳定,同事的提问也增多)。这促使我重新审视这个问题并为软件添加更多功能。这篇新文章为应用程序增加了两个主要功能,即:
- 在本地进程中使用 ASP.NET 中间件,从而可以轻松处理 HTTP(S) 请求;
- 能够将非托管回调的结果返回给 .NET 代码。
应用程序(或者更确切地说,简单的 HTTP(S) 服务器)的总体结构如图所示。为了突出新功能,代码本身比上一篇文章有所简化(尽管省略的部分可以很容易地恢复)。
托管/非托管代码协作
与之前的工作类似,C++ 网关类 GatewayToManaged
提供了来自非托管端的主要功能。Trusted Platform Assemblies (TPA) 列表已添加了 ASP.NET DLL(请参阅 GatewayToManaged::Init()
方法的实现)。该类的头文件如下所示:
#pragma once
#include "Export.h"
#include "coreclrhost.h"
using namespace std;
// Function pointer types for the managed call and unmanaged callback
EXPORTED typedef long (*unmanaged_callback_ptr)
(const char* funcName, const char* args, long long int lArrAddr);
EXPORTED typedef char* (*managed_direct_method_ptr)
(const char* funcName, const char* args, unmanaged_callback_ptr unmanagedCallback);
class EXPORTED GatewayToManaged
{
public:
GatewayToManaged();
~GatewayToManaged();
bool Init(int argc, char* argv[]);
char* Invoke(const char* funcName, const char* args,
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
};
方法 GatewayToManaged::Init()
执行大部分准备工作。它:
- 加载
CoreCLR
组件; - 构建 Trusted Platform Assemblies (TPA) 列表,包括 ASP.NET DLL;
- 定义主要的
CoreCLR
属性; - 启动 .NET Core 运行时并创建默认(也是唯一的)
AppDomain
;最后 - 创建一个
object managed_direct_method_ptr _managedDirectMethod
,允许调用托管委托。
与之前的工作一样,托管委托通过托管的 static
方法 Gateway.ManagedDirectMethod()
进行调用。要调用的托管方法位于 Worker.dll 中。为了简单起见,只提供了一个这样的方法 Worker.StartWebApi()
。可以通过 Gateway.ManagedDirectMethod()
中的反射获取 class Worker
中可调用方法的列表。
包含进程 int main(int argc, char* argv[])
的 start
函数的 Host.cpp 文件如下所示:
#include <iostream>
#include "Host.h"
using namespace std;
// This files contains callback to be called from managed code
#include "callback.h"
int main(int argc, char* argv[])
{
cout << "Host started" << endl;
cout << "To quit please insert any char and press <enter>" << endl << endl;
GatewayToManaged gateway;
gateway.Init(argc, argv);
string args = "";
string retStrQ = gateway.Invoke("StartWebApi",
args.c_str(), UnmanagedCallback);
cout << retStrQ.c_str() << endl;
char ch;
cin >> ch;
gateway.Close();
return 0;
}
可以看出,它包含了 callback.h 文件,其中包含由托管代码调用的函数 static long UnmanagedCallback(const char* funcName, const char* args, long long int lArrAddr)
。在我们的例子中,此函数从 WebApi.dll 中的托管控制器调用。该函数的参数代表要调用的实际非托管函数的名称、其参数以及返回值地址。lArrAddr
指向的内存由托管和非托管代码共享,用于数据交换。此机制的实现将在下面介绍。
方法 UnmanagedCallback()
构成了一个简单的非托管回调示例。它对 char[]
字符串执行一些简单的操作。关键是要将结果转换为 Unicode,即转换为 wchar_t
,以便返回给托管代码。在这样做时,必须考虑到 wchar_t
在 Windows(2 字节)和 Linux(4 字节)中的不同长度。我采用了一种相当原始的方法,只考虑了一个有意义的字节。在实际应用程序中,这个问题应该得到妥善处理。
Web API
本工作的主要目标是将 ASP.NET 中间件添加到非托管进程中,以简化 HTTP(S) 请求的处理。托管的 WebApi DLL 包含 ASP.NET 中间件(class Web
)和控制器(在我们示例中,为了简单起见,只有一个 class JobController
)。但是,已经发现,默认情况下,基于主机反射的托管进程的控制器路由标准在这里不起作用。可以通过实现类似的基于反射的路由机制来相对容易地完成。但为了保持简单,我选择在 Web.Run()
方法中硬编码路由,如下所示:
app.UseEndpoints(endpoints =>
{
foreach (var key in new[] { "/api/job", "/api/job/{arg}" })
endpoints.MapGet(key, async context => await JobGetHandler(context));
endpoints.MapPost("/api/job",
async context => await JobPostHandler(context));
});
因此,我们的控制器没有属性。它也没有基类,这与传统的 ASP.NET 控制器常见的情况不同。方法 JobGetHandler()
和 JobPostHandler()
封装了对控制器类相应方法的调用,并通过 context.Response.WriteAsync(jsonString)
返回它们的输出。这些返回的对象作为上述代码片段中 endpoints.MapGet()
和 endpoints.MapPost()
方法的参数。
"固定"内存
我们的控制器通过运行其提供的扩展方法 Exec()
调用非托管代码:UnmanagedCallbackDelegate callback
。
public static class CallbackExtension
{
public static string Exec(this UnmanagedCallbackDelegate callback,
ILogger logger, string funcName = null, string args = null)
{
const int PINNED_LEN = 200;
string ret = "Server error";
if (callback != null)
{
int len = -1;
var chs = new string(' ', PINNED_LEN).ToCharArray();
var handleChs = GCHandle.Alloc(chs, GCHandleType.Pinned);
var chsPtr = handleChs.AddrOfPinnedObject();
try
{
len = callback(funcName, args, (long)chsPtr);
chs = handleChs.Target as char[];
}
catch (Exception e)
{
logger.Error($"ERROR while executing callback.
{Environment.NewLine}{e}");
}
finally
{
handleChs.Free();
}
if (len > 0)
ret = new string(chs).Substring(0, len);
else
logger.Error($"ERROR while executing callback in
method {funcName}(), params: {args}");
}
else
logger.Error($"ERROR: callback is null in method {funcName}()");
return ret;
}
}
该方法中最重要的部分是处理由托管和非托管代码共享的内存。分配的内存被“固定”,供非托管部分使用,然后在 finally
块中释放。这种技术使得同一块内存可从托管和非托管代码中访问。它的性能不是很高,但允许非托管代码将数据返回给托管部分。总的来说,非托管-托管混合通常只在替代方案是巨大且通常不可行的非托管代码重构的特殊情况下使用。因此,在这种情况下,性能不是首要考虑因素。
如何使用
为了运行代码,我们需要指定 CLR 和 ASP.NET 的目录。它们在可执行文件(Windows 为 Host.exe,Linux 为 Host.out)的两个主要命令行参数中定义。
可以使用以下命令获取这些目录:
dotnet --info
此命令的输出(除其他有用信息外)提供了 .NET 和 ASP.NET 运行时目录。
注意:.NET 和 ASP.NET 运行时目录取决于您使用的 .NET SDK 和运行时。因此,它们可能与 Windows 和 Linux 的演示命令文件中提供的不同。
第三个命令行参数(如果提供,可以是任何 string
)会在宿主进程开始时产生一个“断点”,允许开发人员将调试器附加到该进程。
Windows
在 Windows 中,使用 Visual Studio 构建解决方案。在 Host 项目中,为了确保为 Windows 构建,我们必须在配置 -> 配置属性 -> C/C++ -> 预处理器 -> 预处理器定义中定义 WINDOWS。在相同的配置路径下,应定义参数 _CRT_SECURE_NO_WARNINGS
以允许本机代码使用简单的 C 函数处理 char[]
字符串。
配置 -> 配置属性 -> 调试 -> 命令参数应包含上述命令参数,即 .NET 和 ASP.NET 运行时目录以及[可选]断点参数。重要的是,CoreCLR 的位(32 位或 64 位)必须与宿主构建的位匹配。在我们的例子中,C++ 项目应以 x64 模式构建。构建后,所有可执行文件和配置文件将放在 $(SolutionDir)x64 目录中。为了方便起见,命令文件 Host.cmd 和 Host-with-breakpoint.cmd 包含可执行文件 Host.exe 以及所需的命令行参数。
重要:在运行命令文件之前,请检查您的 .NET 和 ASP.NET 运行时目录(参见上面的注意事项)。
Linux
为了在 Linux 环境中测试软件,我使用了 Ubuntu Windows 子系统 (WSL) 和 Ubuntu 版本 22.04。为了构建 Linux 可执行文件 Host.out,应从 $(SolutionDir)Host 目录执行以下 Linux 命令:
g++ -o Host.out -D LINUX GatewayToManaged.cpp Host.cpp -ldl
它将生成一个输出的可执行文件 Host.out。此文件应复制到 $(SolutionDir)x64 目录。在同一个 Linux 目录中,放置了命令 shell 脚本文件 Host.sh 和 Host-with-breakpoint.sh,其中包含运行应用程序的命令。
重要:在运行命令 shell 脚本文件之前,请检查您的 .NET 和 ASP.NET 运行时目录(参见上面的注意事项)。
为了在 Linux 中测试应用程序,需要提供一个 TLS 证书。这可以通过安装 .NET SDK(而不仅仅是运行时)并执行以下命令来实现:
dotnet dev-certs https
结果
当 Host 可执行文件启动时,它会在控制台显示正在监听端口 7000 上的 HTTP 请求和端口 7001 上的 HTTS 请求。现在我们可以使用浏览器和 Postman 通过 HTTP(S) 请求对其进行测试。
GET
请求
https://:7001/api/job/arg0
产生
{"Payload":"Echo from Unmanaged~ funcGet(arg0)","Os":"WINDOWS Server",
"Message":"Current Local Time","Time":"16/10/2022 9:26:16"}
POST
请求
https://:7001/api/job
带有 Body -> raw -> JSON = { "arg0": "zero arg" }
的请求将产生以下输出:
"{\"Payload\":\"Echo from Unmanaged~ funcPost
({ \\"arg0\\": \\"zero arg\\" })\",\"Os\":\"LINUX Server\",
\"Message\":\"Current Local Time\",\"Time\":\"10/16/2022 09:19:17\"}"
结论
这项工作扩展了我之前的文章 《在 Windows 和 Linux 的非托管 C/C++ 进程中托管 .NET Core 组件》,通过将 ASP.NET 中间件集成到非托管进程中。它允许开发人员使用托管的 C# 代码几乎像在正常的托管 Web API 中一样处理 HTTP(S) 请求,从托管控制器调用非托管函数,并将非托管代码执行的结果返回给托管代码。它在 Windows 和 Linux 上都能运行。
历史
- 2022 年 10 月 17 日:初始版本