在 C# 中引用 C++ 程序集(DLL)主要通过 P/Invoke(平台调用) 和 C++/CLI 包装器 两种方式实现。由于 C# 和 C++ 的底层机制不同(如内存管理、异常处理),直接调用 C++ DLL 需要处理类型转换、调用约定等细节。以下是详细指南:
一、P/Invoke(平台调用)
适用场景:调用 C++ 导出的 纯函数(非类成员函数),通常通过 extern "C"
和 __declspec(dllexport)
暴露接口。
特点:
- 无需修改 C++ 代码,但需手动处理数据类型映射。
- 适合简单函数调用,不支持 C++ 类、对象或复杂数据结构。
步骤 1:C++ 端准备
// MathLibrary.h
#pragma once
#ifdef MATHLIBRARY_EXPORTS
#define MATHLIBRARY_API __declspec(dllexport)
#else
#define MATHLIBRARY_API __declspec(dllimport)
#endif
extern "C" {
// 导出纯函数(避免名称修饰)
MATHLIBRARY_API int Add(int a, int b);
MATHLIBRARY_API double Multiply(double a, double b);
}
// MathLibrary.cpp
#include "MathLibrary.h"
extern "C" {
MATHLIBRARY_API int Add(int a, int b) { return a + b; }
MATHLIBRARY_API double Multiply(double a, double b) { return a * b; }
}
步骤 2:C# 端调用
using System;
using System.Runtime.InteropServices;
class Program
{
// 声明 DLL 导入(需指定路径或放在输出目录)
[DllImport("MathLibrary.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern int Add(int a, int b);
[DllImport("MathLibrary.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern double Multiply(double a, double b);
static void Main()
{
int sum = Add(3, 5);
double product = Multiply(2.5, 4.0);
Console.WriteLine($"Sum: {sum}, Product: {product}");
}
}
关键注意事项
调用约定:
- C++ 默认使用
__cdecl
或__stdcall
,需在DllImport
中通过CallingConvention
显式指定(如CallingConvention.Cdecl
)。 - 若约定不匹配,可能导致栈破坏或崩溃。
- C++ 默认使用
数据类型映射:
- 基本类型:
int
→int
,double
→double
。 - 字符串:C++ 的
char*
需用[MarshalAs(UnmanagedType.LPStr)]
转换。 - 结构体:需在 C# 中定义
[StructLayout(LayoutKind.Sequential)]
的对应结构。
- 基本类型:
DLL 路径:
- 将 C++ DLL 放在 C# 程序的输出目录(如
bin\Debug
),或指定绝对路径。 - 调试时可用
SetDllDirectory
动态加载路径。
- 将 C++ DLL 放在 C# 程序的输出目录(如
二、C++/CLI 包装器(推荐复杂场景)
适用场景:需要调用 C++ 类、STL 容器或复杂对象时,通过 C++/CLI 创建托管-非托管桥接层。
特点:
- 支持面向对象调用,自动处理内存管理和类型转换。
- 需在 Visual Studio 中创建 C++/CLI 类库项目。
步骤 1:创建 C++/CLI 包装器
// MathWrapper.h
#pragma once
#include "../MathLibrary/MathLibrary.h" // 原始 C++ 头文件
namespace MathWrapper {
public ref class Calculator // 托管类
{
public:
int Add(int a, int b) { return ::Add(a, b); } // 调用原生 C++ 函数
double Multiply(double a, double b) { return ::Multiply(a, b); }
};
}
步骤 2:C# 端引用
- 在 C# 项目中添加对 C++/CLI 程序集 的引用(非原生 DLL)。
- 直接调用托管类:
using MathWrapper;
class Program
{
static void Main()
{
Calculator calc = new Calculator();
int sum = calc.Add(3, 5);
double product = calc.Multiply(2.5, 4.0);
Console.WriteLine($"Sum: {sum}, Product: {product}");
}
}
优势对比
方式 | P/Invoke | C++/CLI 包装器 |
---|---|---|
适用性 | 简单函数 | 复杂类、STL、面向对象调用 |
性能 | 较高(直接调用) | 较低(需托管/非托管转换) |
开发复杂度 | 需手动处理类型和调用约定 | 自动类型转换,代码更简洁 |
内存管理 | 需手动处理(如 IntPtr ) |
自动托管内存 |
三、高级场景处理
1. 处理复杂数据结构
C++ 结构体:
// C++ 端
struct Point { int x; int y; };
extern "C" MATHLIBRARY_API int GetDistance(Point p1, Point p2);
C# 端:
[StructLayout(LayoutKind.Sequential)]
struct Point { public int x; public int y; }
[DllImport("MathLibrary.dll")]
public static extern int GetDistance(Point p1, Point p2);
2. 回调函数
C++ 端:
typedef void (*Callback)(int result);
extern "C" MATHLIBRARY_API void RunAsync(Callback cb);
C# 端:
delegate void CallbackDelegate(int result);
[DllImport("MathLibrary.dll")]
public static extern void RunAsync(CallbackDelegate cb);
// 调用时需保持委托不被垃圾回收
static void Main()
{
CallbackDelegate callback = result => Console.WriteLine($"Result: {result}");
RunAsync(callback);
Console.ReadLine(); // 防止主线程退出
}
3. 内存管理
- C++ 分配的内存(如
char*
)需在 C++ 端提供释放函数,或在 C# 中用Marshal.FreeHGlobal
释放。 - 避免内存泄漏:确保每块分配的内存都有明确的释放路径。
四、常见问题排查
DllNotFoundException
- 检查 DLL 路径是否在输出目录或系统路径(如
PATH
环境变量)中。 - 使用
Dependency Walker
确认 C++ DLL 的依赖项是否完整。
- 检查 DLL 路径是否在输出目录或系统路径(如
EntryPointNotFoundException
- 确保 C++ 函数名未被修饰(用
extern "C"
禁用名称修饰)。 - 检查调用约定是否匹配。
- 确保 C++ 函数名未被修饰(用
数据损坏
- 结构体对齐问题:在 C# 中用
[StructLayout(LayoutKind.Sequential, Pack=1)]
指定对齐方式。 - 字符串编码:明确使用
LPStr
(ANSI)或LPWStr
(Unicode)。
- 结构体对齐问题:在 C# 中用
五、总结
- 简单函数:优先用 P/Invoke,但需处理类型和调用约定。
- 复杂对象/类:使用 C++/CLI 包装器,简化调用并自动管理内存。
- 调试技巧:
- 在 C++ 端添加日志,确认函数是否被正确调用。
- 用
Marshal.PtrToStringAnsi
等工具检查内存数据。
通过合理选择方式,可以高效地在 C# 中复用 C++ 代码,平衡性能与开发效率。
注:内容由AI生成