如何在 C++ 项目中调用 .NET Core 3.1 的 C# DLL?
可以保留现有的 Visual Studio 2010 原生 C/C++ 主程序,用支持 .NET Core 的较新工具链编译一个 C++/CLI 桥接 DLL。桥接层负责调用 C# DLL,并向旧程序导出普通 C 函数。
题目中的测试代码已经使用了 C++/CLI,需要先解决目标运行时不匹配的问题,再处理程序集部署。单独补一个 System.Runtime.dll 通常解决不了这类错误。
为什么编译成功,运行时却找不到 System.Runtime?
以下语法属于 C++/CLI:
using namespace System;
#using "MyDLL.dll"
int main(array<System::String^>^ args)
#using 让编译器读取 .NET 程序集中的类型信息。编译器能够识别 Class1::add,并不代表应用程序启动时使用的运行时能够加载它。
在题述的 Visual Studio 2010 环境中,首先要检查两个项目各自面向的运行时:
- C++/CLI 程序通过传统
/clr编译,面向 .NET Framework。 - C# DLL 的目标框架是
netcoreapp3.1,需要 .NET Core 运行时。
.NET Framework 与 .NET Core 是不同的运行时,不能通过普通程序集引用直接混用。即使安装了 .NET Core 3.1,面向 .NET Framework 的 C++/CLI EXE 也不会自动改用 CoreCLR。
异常中出现的:
System.Runtime, Version=4.2.2.0
需要结合项目的目标框架和依赖关系排查,不能理解为下载这个版本的 DLL 就能解决。这里的版本号是程序集版本号,无法据此判断应该安装哪个产品版本。mcrtexe.cpp 中的报错位置只是启动过程中暴露问题的位置,无需修改该文件。
保留旧主程序,增加 C++/CLI 桥接 DLL
调用关系可以设计为:
旧 C/C++ EXE(Visual Studio 2010,原生程序)
→ Bridge.dll(面向 .NET Core 3.1 的 C++/CLI)
→ MyDLL.dll(C#,netcoreapp3.1)
这个方案要求旧主程序是原生程序。如果它已经通过 /clr 使用了 .NET Framework,还需要单独评估运行时共存和部署问题。
1. 使用支持 .NET Core 的工具链创建桥接项目
Visual Studio 2010 不能编译面向 .NET Core 的 C++/CLI 项目。对 .NET Core 3.1 的 C++/CLI 支持从 Visual Studio 2019 16.4 系列开始提供,需要安装相应的 C++/CLI 组件和 .NET Core 3.1 SDK。Microsoft 的版本说明
为桥接层创建一个面向 .NET Core 的 CLR 类库项目,并检查 .vcxproj 中的以下配置:
<PropertyGroup>
<ConfigurationType>DynamicLibrary</ConfigurationType>
<CLRSupport>NetCore</CLRSupport>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
将这段配置合并到实际项目中后,还要检查各个构建配置,确认没有旧值覆盖这些设置。
面向 .NET Core 的 C++/CLI 只支持 Windows,且必须编译为 DLL,不能直接生成 C++/CLI EXE。因此,题目中的控制台入口应移到原生 C++ 项目中。C++/CLI 迁移文档
2. 在桥接层导出普通 C 函数
C# 中原有的 Class1.add 可以保持不变。在桥接项目中添加对 MyDLL 的引用,然后编写:
// Bridge.cpp
// 此示例假定已关闭预编译头。
#using "MyDLL.dll"
extern "C" __declspec(dllexport)
int __cdecl Add(double a, double b, double* result)
{
if (result == nullptr)
return -1;
try
{
*result = MyDLL::Class1::add(a, b);
return 0;
}
catch (System::Exception^)
{
return -2;
}
}
桥接项目编译时必须能找到 MyDLL.dll。可以添加项目引用;如果使用文件引用,则需要配置相应的程序集搜索目录。
这个接口只暴露原生类型,返回值表示状态,计算结果通过 double* 写回。正式接入蓝牙功能时,还应记录异常详情,并提供错误信息查询接口。运行时初始化也可能在进入函数体之前失败,因此这里的 catch 无法覆盖所有加载错误。
extern "C" 用于指定 C 链接,__declspec(dllexport) 用于导出函数。Microsoft 的 C 接口导出说明
3. 在旧 C++ 程序中加载桥接 DLL
旧程序保持原生编译,不需要 #using、System::String^ 或 /clr:
#include <windows.h>
#include <stdio.h>
typedef int (__cdecl* AddFn)(double, double, double*);
int main()
{
// 请替换为实际部署路径。
HMODULE bridge = LoadLibraryW(L"C:\\MyApp\\Bridge.dll");
if (bridge == NULL)
{
printf("LoadLibrary failed: %lu\n", GetLastError());
return 1;
}
FARPROC proc = GetProcAddress(bridge, "Add");
// 兼容可能带有前导下划线的 x86 导出名。
if (proc == NULL)
proc = GetProcAddress(bridge, "_Add");
if (proc == NULL)
{
printf("GetProcAddress failed: %lu\n", GetLastError());
return 2;
}
AddFn add = reinterpret_cast<AddFn>(proc);
double result = 0.0;
int status = add(40.1, 1.9, &result);
if (status != 0)
{
printf("Add failed: %d\n", status);
return 3;
}
printf("%.1f\n", result);
// 示例让桥接模块保持加载,直到进程退出。
return 0;
}
调用正常完成时,应显示 42.0。
如果找不到导出函数,可以在 Visual Studio 开发人员命令提示符中运行:
dumpbin /exports Bridge.dll
调用时以实际导出表中的名称为准。也可以通过 .def 文件固定对外导出名,避免调用端处理名称修饰。
4. 部署运行时和依赖文件
原生入口首次调用面向 .NET Core 的 C++/CLI DLL 时,需要相应的运行时配置。桥接层名为 Bridge.dll 时,应检查输出目录中是否有 Bridge.runtimeconfig.json。如果旧工具链没有自动生成,可以手动提供:
{
"runtimeOptions": {
"tfm": "netcoreapp3.1",
"framework": {
"name": "Microsoft.NETCore.App",
"version": "3.1.0"
}
}
}
部署时要一并提供 Bridge.dll、MyDLL.dll、对应架构的 ijwhost.dll,以及构建产生的依赖描述文件和实际依赖项。目标机器还需安装兼容的 .NET Core 3.1 运行时和桥接层所需的 Visual C++ 运行库。原生入口、ijwhost.dll 和运行时配置的具体要求见 Microsoft 部署说明。
还需要检查以下事项:
- 主程序为 32 位时,桥接 DLL、原生依赖和进程内使用的运行时也必须为 32 位。
- C# 程序集即使使用
Any CPU,也不能消除原生组件的位数限制。 - 不要在两种编译器生成的模块之间传递
std::string、STL 容器或 C++ 对象,也不要跨模块随意分配和释放内存。 - 蓝牙扫描、连接和通知可能持续运行,需要明确线程、回调和对象的生命周期。
不使用 C++/CLI,还有哪些选择?
使用 hostfxr 托管 .NET Core
原生程序可以通过 nethost、hostfxr 启动 .NET Core,再取得托管静态方法的函数指针。主要步骤是:
- 使用
get_hostfxr_path定位并加载hostfxr。 - 调用
hostfxr_initialize_for_runtime_config读取运行时配置。 - 通过
hostfxr_get_runtime_delegate获取hdt_load_assembly_and_get_function_pointer。 - 加载 C# 程序集,取得并调用目标方法。
如果保留 double add(double, double) 这样的签名,需要声明匹配的托管委托,并向加载 API 传入其程序集限定类型名,不能直接套用官方示例的默认入口签名。原生托管教程
采用这种方式,需要自行处理运行时初始化、函数签名和部署。旧程序也可以调用一个用较新工具链编译的原生适配 DLL,由该 DLL 完成托管工作。
将 C# 类暴露为 COM 组件
.NET Core 3.1 支持 COM Hosting。需要定义带 GUID 的 COM 可见接口及实现类,并在项目中启用:
<EnableComHosting>true</EnableComHosting>
然后由原生 C++ 通过 COM 激活对象。静态 add 需要包装为接口上的实例方法。
注册时应对生成的 MyDLL.comhost.dll 使用 regsvr32,不能直接注册托管 MyDLL.dll。.NET Core 也不会像 .NET Framework 那样从程序集自动生成类型库,需要自行提供接口声明或 IDL。COM Hosting 官方说明
将蓝牙通信放到独立进程
如果无需在同一进程中调用,可以让 C# 程序负责蓝牙通信,旧程序通过命名管道或本地 Socket 与它交换消息。
对于不便升级的旧系统,这种结构可以减少运行时和工具链之间的耦合,也允许两个进程使用不同位数。采用这种方式,需要设计消息格式,并处理超时、进程退出和重连。
如果考虑把 C# 库改为面向 .NET Framework,必须先确认所用蓝牙 API 和依赖支持该目标框架,再重新编译验证。仅修改目标框架名称不能保证移植成功。
BluetoothLE 示例还需要单独验证什么?
Microsoft 的 BluetoothLE 示例 属于 UWP 示例,使用 BLE/GATT API。将它封装为 DLL 时,需要确认具体 API 能否在桌面进程中使用,并检查 Windows 版本、设备权限、包身份和线程初始化要求。加载 DLL 后,执行环境仍由宿主进程决定。
还要确认设备实际使用的是 BLE/GATT,还是经典蓝牙的 RFCOMM 等协议。内置蓝牙和 USB dongle 只是适配器形式,无法据此决定通信 API。
如果使用 C# 只是为了访问 Windows 蓝牙 API,也可以考虑用较新工具链编写一个原生 DLL,通过 C++/WinRT 调用 WinRT 蓝牙接口,再向旧程序导出 C 接口。Windows 蓝牙 API 说明
.NET Core 3.1 已于 2022 年 12 月 13 日结束支持。维护现有组件时,可以按实际环境验证上述方案;新增通信模块宜选择仍受支持的 .NET 版本,并重新确认对应的工具链和部署要求。.NET 支持策略
备注:内容仅供参考。
