C#调用含自定义结构体参数的非托管C++ DLL函数的实现疑问
解决C#调用C++ DLL中含自定义结构体指针函数的问题
我来帮你梳理下这个问题的解决步骤,你目前的结构体转换已经有了基础,但还有几个关键点需要调整,以及函数调用的正确方式:
一、修正C#结构体的字符串字段定义
你当前用UnmanagedType.ByValArray来处理C的WCHAR[]是不合适的,因为ByValArray是用来处理数组元素集合的,而C里的WCHAR[128]本质是固定长度的宽字符字符串缓冲区,应该改用UnmanagedType.ByValTStr来告诉CLR正确处理字符串的封送:
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] public struct PrinterItem { // 用ByValTStr对应C++的WCHAR[128] [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] public string name; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)] public string id; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)] public string dev; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] public string desc; public int pid; } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] public struct PrinterList { public int n; // 实际返回的打印机数量 // 固定大小数组,用ByValArray指定SizeConst=32 [MarshalAs(UnmanagedType.ByValArray, SizeConst = 32)] public PrinterItem[] item; }
二、正确定义DllImport函数
C++的GetDeviceList接收的是PRINTER_LIST*指针,对应C#里的引用传递(用ref关键字),同时要注意调用约定(这是很多人踩坑的点):
- 如果C++函数是默认编译(无
__stdcall修饰),调用约定用CallingConvention.Cdecl - 如果函数声明带
__stdcall(比如WINAPI宏),则用CallingConvention.StdCall
修正后的DllImport定义:
public class Program { // 请根据实际DLL的调用约定调整CallingConvention [DllImport("dllName.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Unicode)] public static extern int GetDeviceList(ref PrinterList pList);
三、完整的调用流程
调用前必须初始化PrinterList的item数组(C#不会自动初始化结构体里的数组成员),然后通过ref传入结构体,让DLL填充数据:
static void Main(string[] args) { // 初始化结构体 PrinterList deviceList = new PrinterList(); // 必须手动创建固定大小的数组,否则封送时会出错 deviceList.item = new PrinterItem[32]; // 调用DLL函数,传入ref允许DLL修改结构体内容 int result = GetDeviceList(ref deviceList); // 根据返回值判断调用结果(通常0表示成功,具体参考DLL文档) if (result == 0) { Console.WriteLine($"找到 {deviceList.n} 台打印机:"); // 注意用deviceList.n遍历,而不是32(因为实际返回数量可能小于最大值) for (int i = 0; i < deviceList.n; i++) { PrinterItem item = deviceList.item[i]; Console.WriteLine($"--- 打印机 {i+1} ---"); Console.WriteLine($"名称:{item.name.TrimEnd('\0')}"); // 去除字符串末尾的空字符 Console.WriteLine($"ID:{item.id.TrimEnd('\0')}"); Console.WriteLine($"设备信息:{item.dev.TrimEnd('\0')}"); Console.WriteLine($"描述:{item.desc.TrimEnd('\0')}"); Console.WriteLine($"PID:{item.pid}\n"); } } else { Console.WriteLine($"调用失败,错误码:{result}"); } } }
几个关键注意事项
- 字符串空字符处理:C++的字符串缓冲区末尾会有
\0,所以用TrimEnd('\0')去除多余的空字符,避免输出乱码。 - 调用约定匹配:如果调用约定不匹配,会导致栈溢出或程序崩溃,一定要确认DLL的编译约定。
- 数组初始化:必须手动创建
PrinterItem[32]赋值给item,否则CLR无法分配足够的内存给DLL填充数据。
内容的提问来源于stack exchange,提问作者blueOct
相关产品推荐
相关产品推荐

