【在 C# 中通过 P/Invoke 调用 C++ DLL 时的数据类型转换】

发布于:2025-07-07 ⋅ 阅读:(20) ⋅ 点赞:(0)

在 C# 中通过 P/Invoke 调用 C++ DLL 时,数据类型需要手动转换,因为 C# 和 C++ 的底层内存表示不同。以下是常见数据类型的转换方法及注意事项:


一、基本数据类型

C++ 类型 C# 对应类型 备注
int int 32 位整数
long int (Windows) Windows 下 long 为 32 位
long long long 64 位整数
float float 32 位浮点数
double double 64 位浮点数
char bytesbyte 取决于 char 是否签名
bool [MarshalAs(UnmanagedType.I1)] bool C++ bool 可能是 1 字节

示例

// C++ 导出函数
extern "C" __declspec(dllexport) int Add(int a, int b);
// C# 声明
[DllImport("MyCppDll.dll")]
public static extern int Add(int a, int b);

二、字符串类型

C++ 的字符串(char*wchar_t*)需要显式转换:

C++ 类型 C# 对应类型 MarshalAs 属性
const char* string[In] StringBuilder UnmanagedType.LPStr
char* (可修改) [Out] StringBuilder UnmanagedType.LPStr
const wchar_t* string[In] StringBuilder UnmanagedType.LPWStr
wchar_t* (可修改) [Out] StringBuilder UnmanagedType.LPWStr

示例

// C++ 导出函数
extern "C" __declspec(dllexport) void Greet(char* name, wchar_t* message);
// C# 声明
[DllImport("MyCppDll.dll", CharSet = CharSet.Ansi)]
public static extern void Greet(
    [MarshalAs(UnmanagedType.LPStr)] string name,
    [MarshalAs(UnmanagedType.LPWStr)] StringBuilder message
);

// 调用
StringBuilder message = new StringBuilder(100);
Greet("Alice", message);
Console.WriteLine(message.ToString());

三、指针和句柄

1. 通用指针 (void*)

  • 在 C# 中用 IntPtr 表示,可存储任意指针。
  • 需要手动转换(如 Marshal.Copy 读写内存)。
// C++ 导出函数
extern "C" __declspec(dllexport) void ProcessData(void* data, int length);
// C# 声明
[DllImport("MyCppDll.dll")]
public static extern void ProcessData(IntPtr data, int length);

// 调用
byte[] buffer = { 1, 2, 3 };
fixed (byte* p = buffer)
{
    ProcessData((IntPtr)p, buffer.Length);
}

2. 结构体指针

  • 定义对应的 C# 结构体,并用 StructLayout 指定内存布局。
// C++ 结构体
struct Point { int x; int y; };
extern "C" __declspec(dllexport) void PrintPoint(Point* p);
// C# 结构体
[StructLayout(LayoutKind.Sequential)]
struct Point { public int x; public int y; }

[DllImport("MyCppDll.dll")]
public static extern void PrintPoint(ref Point p); // 或 IntPtr

// 调用
Point p = new Point { x = 10, y = 20 };
PrintPoint(ref p);

四、数组和缓冲区

1. 固定大小数组

  • 在结构体中用 [MarshalAs(UnmanagedType.ByValArray, SizeConst=N)]
// C++ 结构体
struct Data { int values[3]; };
extern "C" __declspec(dllexport) void ProcessData(Data data);
// C# 结构体
[StructLayout(LayoutKind.Sequential)]
struct Data
{
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 3)]
    public int[] values;
}

[DllImport("MyCppDll.dll")]
public static extern void ProcessData(Data data);

2. 动态数组

  • 使用 IntPtr + Marshal.Copy 手动转换。
// C++ 导出函数
extern "C" __declspec(dllexport) void ProcessArray(int* arr, int length);
// C# 声明
[DllImport("MyCppDll.dll")]
public static extern void ProcessArray(IntPtr arr, int length);

// 调用
int[] managedArray = { 1, 2, 3 };
IntPtr unmanagedArray = Marshal.AllocHGlobal(managedArray.Length * sizeof(int));
Marshal.Copy(managedArray, 0, unmanagedArray, managedArray.Length);

ProcessArray(unmanagedArray, managedArray.Length);

// 释放内存
Marshal.FreeHGlobal(unmanagedArray);

五、函数指针(回调)

  • 在 C# 中用 delegate 表示,需标记为 UnmanagedFunctionPointer
// C++ 导出函数
typedef void (*Callback)(int result);
extern "C" __declspec(dllexport) void RegisterCallback(Callback cb);
// C# 声明
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
delegate void CallbackDelegate(int result);

[DllImport("MyCppDll.dll")]
public static extern void RegisterCallback(CallbackDelegate cb);

// 调用(需保持委托不被垃圾回收)
static CallbackDelegate callback = result => Console.WriteLine($"Result: {result}");
RegisterCallback(callback);
GC.KeepAlive(callback); // 防止 GC 回收

六、类对象(不推荐直接传递)

  • C++ 类无法直接在 C# 中使用,需通过:
    1. C++/CLI 包装器(推荐)。
    2. 将类拆分为函数接口(如 Create/Destroy + 操作函数)。
// C++ 类和工厂函数
class MyClass { /*...*/ };
extern "C" __declspec(dllexport) MyClass* CreateInstance();
extern "C" __declspec(dllexport) void DisposeInstance(MyClass* ptr);
// C# 声明
[DllImport("MyCppDll.dll")]
public static extern IntPtr CreateInstance();

[DllImport("MyCppDll.dll")]
public static extern void DisposeInstance(IntPtr ptr);

// 调用
IntPtr instance = CreateInstance();
// ... 通过其他 P/Invoke 函数操作实例
DisposeInstance(instance);

七、常见问题与解决

  1. Marshal.SizeOf 错误

    • 确保结构体定义与 C++ 完全一致(包括对齐方式 Pack)。
  2. 字符串乱码

    • 明确指定 CharSet(如 CharSet = CharSet.Unicode)。
  3. 内存泄漏

    • 释放 Marshal.AllocHGlobal 分配的内存。
    • 对 C++ 分配的内存提供对应的释放函数。
  4. 调用约定不匹配

    • DllImport 中显式指定 CallingConvention.CdeclStdCall

八、总结

  • 简单类型intfloat):直接映射。
  • 字符串:用 StringBuilder + MarshalAs
  • 指针/数组IntPtr + Marshal.Copy
  • 回调函数delegate + UnmanagedFunctionPointer
  • 复杂类:通过 C++/CLI 包装或拆解为 C 风格接口。

通过正确处理类型转换,可以安全高效地在 C# 中调用 C++ 代码。

注:内容由AI生成