C#调用C++ DLL的两种实现方法:托管与非托管详解
C#作为托管语言,无法直接加载由C++编译的非托管原生DLL。要在项目中实现C#调用C++ DLL,通常有两种解决方案:一是创建C++/CLI中间件项目,将其编译为托管程序集供C#引用;二是利用P/Invoke技术,通过[DllImport]特性在非托管模式下直接调用动态链接库。本文将分别演示这两种方法的工程配置、代码实现及注意事项。
目标与准备:理解托管与非托管差异
C#属于.NET生态下的托管型代码,其内存由垃圾回收机制自动管理;而C++编写的DLL属于非托管代码,需要开发者手动管理资源,否则易引发内存溢出或崩溃。根据性能需求和使用场景的不同,可以选择以下两种主要集成路径:
- 托管调用:适合业务逻辑复杂、需要在C#和C++之间频繁交换数据的情况,开发效率较高,但增加了打包体积。
- 非托管调用:适合仅需调用底层高性能函数(如图形处理、硬件驱动),无需传递复杂对象,系统开销更小。
第1步:采用托管方式进行调用(C++/CLI)
此方法的核心是创建一个C++/CLI类库,充当C#与底层非托管C++代码之间的桥梁。编译器会自动将CLR类型转换为C++代码可识别的形式。
1.1 创建并配置C++/CLI项目
在Visual Studio中创建一个新的“C++/CLI”空项目(选择CLR支持)。首先定义头文件Function.h,使用ref class关键字声明一个受管类:
#pragma once
#include <string>
public ref class Function
{
public:
Function(void);
~Function(void);
int member;
// 简单的数值相加方法
int MemberFuncAdd(int a, int b);
// 字符串处理方法
System::String^ Say(System::String^ str);
};
接着在对应的.cpp文件中实现这些方法:
#include "Function.h"
Function::Function(void)
{
}
Function::~Function(void)
{
}
int Function::MemberFuncAdd(int a, int b)
{
return a + b;
}
System::String^ Function::Say(System::String^ str)
{
return str;
}关键配置提醒:进入项目属性页 → C/C++ → 常规 → 公共语言运行时支持 (Common Language Runtime Support),必须设置为/clr,否则无法生成托管程序集。
1.2 在C#项目中引用
在C#解决方案中添加对上面生成的C++/CLI项目的引用,或者在“引用”对话框中添加项目引用。随后即可像普通C#类一样实例化:
使用示例代码如下:
// 在C#代码中直接调用
Function fun = new Function();
int result = fun.MemberFuncAdd(1, 2);
string text = fun.Say("Hello World");1.3 运行环境与平台设置
由于C++/CLI生成的DLL依赖于.NET CLR环境,必须确保宿主进程的位宽与编译时设置一致。强烈建议将C#主项目和C++/CLI项目都设置为x86或x64,切忌混用x86与x64,否则会导致TypeLoadException。
正确配置后,程序可正常运行:
第2步:采用非托管方式进行调用(P/Invoke)
如果不希望引入CLR开销,可以直接让C#通过操作系统API读取原生C++导出的符号。这要求C++ DLL导出的是标准C接口,避免C++名称修饰(Name Mangling)造成的寻址问题。
2.1 配置原生C++ DLL
新建一个普通的Win32 Console Application项目(选择DLL类型),用于生成原生二进制文件。
在stdafx.h(或预处理头文件)中定义导出宏:
// stdafx.h #pragma once #include "targetver.h" #ifdef A_EXPORTS #define DLL_API __declspec(dllexport) #else #define DLL_API __declspec(dllimport) #endif // 强制C语言链接约定 extern "C" DLL_API void MessageBoxShow();
在dllmain.cpp中实现该函数:
#include "stdafx.h"
BOOL APIENTRY DllMain(HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved)
{
switch (ul_reason_for_call) {
case DLL_PROCESS_ATTACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
void MessageBoxShow()
{
MessageBox(NULL, TEXT("Hello World"), TEXT("In a DLL"), MB_OK);
}注意:虽然此处我们讨论的是非托管,但为了保持上下文一致性并确保项目在包含部分CLR组件时不冲突,部分旧版教程会提及启用运行时,但在纯非托管方案中实际上不需要勾选“公共语言运行时支持”。此处若原图有特定设置提示,请以实际构建需求为准,但标准原生DLL构建无需此选项。
2.2 在C#中声明P/Invoke入口
在C#项目中,使用System.Runtime.InteropServices.DllImport特性来映射非托管函数。假设生成的文件名为Native.dll:
using System.Runtime.InteropServices;
public class NativeHelper
{
// 指定DLL文件名和调用约定
[DllImport("Native.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern void MessageBoxShow();
}
平台匹配警告:与托管方式类似,非托管方式的稳定性高度依赖进程架构。必须在“生成”选项中将平台工具集和目标架构统一设置为x86(针对32位DLL)或x64。
执行调用即可看到原生弹窗:
总结与常见问题排错
- 找不到模块:检查DLL是否已复制到C#输出的根目录(Bin/Debug)。如果是非托管方式,且使用了相对路径,需确保工作目录正确。
- 架构不匹配:如果报错“试图加载格式不正确的程序”或访问违规,绝大多数情况是因为C++ DLL是64位的,而C#应用强行以32位(Any CPU → x86)运行导致的。
- 中文乱码:C++默认GBK编码,C#默认UTF-16,若传输中文参数需特别注意编码转换,或使用WideChar API。
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









