一、前言:

最近这两天由于项目需要,提供给客户的C++ 动态库需要返回自定义结构体数组,网上也查了很多资料, 推荐一本书, 《精通.NET互操作:P/Invoke、C++ Interop和COM Interop》 , 介绍Windows平台上的托管代码与非托管代码之间进行互操作的各种技术, 虽然里面没有结构体数组的传参例子。以前都是返回字节数组的,本以为很简单,意想不到的是,遇到各种坑。其中一个就是在C#中如何处理结构体嵌套以及定义结构体数组的问题,第二个是如何成功的获取由C++返回到结构体数组等问题。为了防止自己下次忘记,并重蹈覆辙,因此,写下这篇文章,记录下来。

二、相关资料

1、托管代码与非托管代码

1.1 托管代码 (managed code)

.NET Framework的核心是其运行库的执行环境,称为公共语言运行库(CLR)或.NET运行库。通常将在CLR的控制下运行的代码称为托管代码(managed code)。

运行库环境(而不是直接由操作系统)执行的代码。托管代码应用程序可以获得公共语言运行库服务,例如自动垃圾回收、运行库类型检查和安全支持等。这些服务帮助提供独立于平台和语言的、统一的托管代码应用程序行为。

1.2 非托管代码 (unmanaged code)

  在公共语言运行库环境的外部,由操作系统直接执行的代码。非托管代码必须提供自己的垃圾回收、类型检查、安全支持等服务;它与托管代码不同,后者从公共语言运行库中获得这些服务。

1.3 什么是托管?托管是什么意思?

托管代码就是基于.net元数据格式的代码,运行于.net平台之上,所有的与操作系统的交换有.net来完成,就像是把这些功能委托给.net,所以称之为托管代码。非托管代码则反之。

1.4 托管代码如何调用非托管代码(c sharp如何调用c++代码)?

两种常用的做法:

1. COM interop。

2. P/Invoke

a. 在托管客户端增加一条 DllImport语句和一个方法的调用。(摘自:超详细解析托管与非托管)

下面主要讲解P/Invoke方式。

2、P/Invoke:C#调用C++
P/Invoke的全称是Platform Invoke (平台调用) 它实际上是一种函数调用机制通 过P/Invoke我们就可以调用非托管DLL中的函数。

P/Invoke依次执行以下操作:

1. 查找包含该函数的非托管DLL

2. 将该非托管DLL加载到内存中

3. 查找函数在内存中的地址并将其参数按照函数的调用约定压栈

4. 将控制权转移给非托管函数

DllImport: 用来标识该方法是非托管的代码方法,在编译器编译的时候它能够正确的认识出被该特性标记的是外来代码段。当到达程序运行的时候,也能够正确的认识出该代码是引用非托管的代码,这样CLR会去加载非托管DLL文件,然后查找到入口点进行调用(关于此部分,《超详细解析托管与非托管》有详细说明)

CallingConvention:在平台调用的过程中起到查找入口点的作用,在托管代码进行非托管代码入口点查找时,会通过CallingConvention中的值进行确认非托管入口点的调用约定。

3、非托管函数和托管方法中数据类型对应

3.1 将C/C++ 等托管代码API中数据类型与C#托管方法数据类型进行转换,是数据封送。

对于每个 .NET Framework 类型均有一个默认非托管类型,公共语言运行库将使用此非托管类型在托管到非托管的函数调用中封送数据。string 类型默认非托管类型是LPTSTR,可以在非托管函数的 C# 声明中使用 MarshalAs 属性重写默认封送处理。例如:

[DllImport("msvcrt.dll")]
public static extern int puts([MarshalAs(UnmanagedType.LPStr)] string m);
puts 函数的参数的默认封送处理已从默认值 LPTSTR 重写为 LPSTR。

修改默认封送有什么用?

默认情况下,本机结构和托管结构在内存中的布局有所不同,因此,若要跨托管/非托管边界成功传递结构,需要执行一些额外步骤来保持数据的完整性。(摘自:《.NET学习之路----我对P/Invoke技术的理解(一)》)

因此,我传递结构体也是一样,由于自定义API中使用一个结构体,那么C#中没有一个对应的托管类型与之对应,需要为用户定义的结构指定自定义封送处理需要对参数进行处理。

可以为传递到非托管函数或从非托管函数返回的结构体和类的字段指定自定义封送处理属性。通过向结构体或类的字段中添加 MarshalAs 属性可以做到这一点。还必须使用 StructLayout 属性设置结构体或者类的布局,还可以控制字符串成员的默认封送处理,并设置默认封装大小。

这篇文章《MarshalAs的使用》有关于MarshalAs 详细说明。

3.2 封送,在此看来算是传数据,但是为什么要封送呢?封送是为了在托管内存和非托管内存中正常传递数据。有时,出于对性能的考虑,会对托管结构的成员进行重新排列,因此有必要使用 StructLayoutAttribute 特性指示该结构为顺序布局。 将结构封装设置显式设置为与本机结构所使用的设置相同的设置也是一个好办法。(摘自:《.NET学习之路----我对P/Invoke技术的理解(一)》)

其中,这个还是涉及C/C++结构体内存对齐,

根据相关资料,摘自《C#调用C++ dll时,结构体引用传参的方法》

在C/C++中,struct类型中的成员的一旦声明,则实例中成员在内存中的布局(Layout)顺序(C/C++结构体内存对齐)就定下来了,即与成员声明的顺序相同,并且在默认情况下总是按照结构中占用空间最大的成员进行对齐(Align);

然而在.net托管环境中,CLR提供了更自由的方式来控制struct中Layout:我们可以在定义struct时,在struct上运用StructLayoutAttribute特性来控制成员的内存布局。默认情况下,struct实例中的字段在栈上的布局(Layout)顺序与声明中的顺序相同,即在struct上运用[StructLayoutAttribute(LayoutKind.Sequential)]特性,这样做的原因是结构常用于和非托管代码交互的情形。如果我们正在创建一个与非托管代码没有任何互操作的struct类型,我们很可能希望改变C#编译器的这种默认规则,因此LayoutKind除了Sequential成员之外,还有两个成员Auto和Explicit,给StructLayoutAttribute传入LayoutKind.Auto可以让CLR按照自己选择的最优方式来排列实例中的字段;传入LayoutKind.Explicit可以使字段按照我们的在字段上设定的FieldOffset来更灵活的设置字段排序方式,但这种方式也挺危险的,如果设置错误后果将会比较严重。

因此,把结构体显示的声明为  [StructLayout(LayoutKind.Sequential)]即可.

默认(LayoutKind.Sequential)情况下,CLR对struct的Layout的处理方法与C/C++中默认的处理方式相同,即按照结构中占用空间最大的成员进行对齐(Align); 
使用LayoutKind.Explicit的情况下,CLR不对结构体进行任何内存对齐(Align),而且我们要小心就是FieldOffset; 
使用LayoutKind.Auto的情况下,CLR会对结构体中的字段顺序进行调整,使实例占有尽可能少的内存,并进行byte的内存对齐(Align)。

3.3 数据封送中指针处理

数据封送中指针处理的两种情况 :

3.3.1 普通指针

  在非托管代码中,基本数据类型对应的指针变量和数组,及单个结构体或类的指针,在C#中使用ref或out来实行封送。3,

3.3.2 Handle类型和自定义结构和类等数组

  C#中使用IntPtr来进行封送。

三、托管代码C#与非托管C++ 嵌套结构体的对应

1、在最近的项目中,非托管C++ 的结构体定义如下:

typedef struct CARDINFOBEAN
{unsigned char channel;    // 通道 0~23unsigned char exist;      // 0---当前通道没有扫描到卡,1---当前通道扫描到卡unsigned char snlen;      // 0---序列号4字节, 1---序列号7字节unsigned char alarm;      // 报警unsigned char cardtype;   // 卡片类型unsigned char sn[7];      // 卡号
}CardInfoBean;typedef struct PROTOCOLBEAN
{unsigned char cardInfoBeanListCount;               //卡片信息结构体实际个数int protocolLength;                                //协议数据长度int channel;                                       //天线通道号unsigned char protocol[1024];                      //协议数据CardInfoBean cardInfoBeanList[255];                //天线号每个对应的卡片信息
}ProtocolBean;

在.h中API声明如下:

ReaderDLL int CallReader CardTest(UCHAR addr, UCHAR testCardType, ProtocolBean *protocolBeanList, int *protocolBeanListCount, int maxLength);

在上面的声明中,使用到了结构体的嵌套,API中通过指针返回嵌套结构体数组。

2、项目中需要使用C#调用C++ 中CardTest() API ,这个时候就遇到了,非托管代码与托管代码中自定义数据类型的对应和封送问题。

在前面的篇幅中,已经讲解的很明了,因此不做过多的叙述。

2.1 在C#对应的结构体定义如下:

[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential, CharSet = System.Runtime.InteropServices.CharSet.Ansi)]
public struct CARDINFOBEAN
{/// unsigned charpublic byte channel;/// unsigned charpublic byte exist;/// unsigned charpublic byte snlen;/// unsigned charpublic byte alarm;/// unsigned charpublic byte cardtype;/// unsigned char[7][System.Runtime.InteropServices.MarshalAsAttribute(System.Runtime.InteropServices.UnmanagedType.ByValArray, SizeConst = 7)]public byte[] sn;}[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential, CharSet = System.Runtime.InteropServices.CharSet.Ansi)]
public struct PROTOCOLBEAN
{/// unsigned charpublic byte cardInfoBeanListCount;/// intpublic int protocolLength;/// intpublic int channel;/// unsigned char[1024]                         [System.Runtime.InteropServices.MarshalAsAttribute(System.Runtime.InteropServices.UnmanagedType.ByValArray, SizeConst = 1024)]public byte[] protocol;/// CardInfoBean[255]   [System.Runtime.InteropServices.MarshalAsAttribute(System.Runtime.InteropServices.UnmanagedType.ByValArray, SizeConst = 255, ArraySubType = System.Runtime.InteropServices.UnmanagedType.Struct)]public CARDINFOBEAN[] cardInfoBeanList;
}

在结构体CARDINFOBEAN中,对于基本数据类型只要与c++中的基本数据类型对应即可,需要特别说明的是基本数据类型数组的对应,默认在C#结构体中,与C++ 对应数组都需要使用MarshalAsAttribute指示指示如何在托管代码和非托管代码对应,并且使用UnmanagedType 的ByValArray 用于在结构体中出现的数组,应始终使用MarshalAsAttribute的SizeConst字段来指示数组的大小。

验证结构体的大小是否正确,可以使用Marshal.SizeOf(),即获取对象的非托管大小时,获得的是自己在C#定义的大小,然后使用sizeof()计算C/C++结构体的大小,即获取非托管的大小,对比是否一致。

在结构体PROTOCOLBEAN中,cardInfoBeanList是嵌套了CARDINFOBEAN的结构体数组,所以对应C#的数组,同样需要使用MarshalAsAttribute指定类型为ByValArray ,并且使用SizeConst字段来指示数组的大小。同样也需要使用ArraySubType = System.Runtime.InteropServices.UnmanagedType.Struct 指定成员为结构体类型。

MarshalAs这个属性很难用,很容易用错,用好需要对C#、C++布局方式有一定的了解才能做。

因此为微软提供了一个很好用的工具,Signature Tool ,具体使用可以阅读《使用Signature Tool自动生成P/Invoke调用Windows API的C#函数声明》这篇文章

如下图,直接可以使用工具生成:

四、嵌套结构体的封送

4.1 在前面也提到,对于当个结构体,可以直接使用ref 或out 。对于结构体数组,则使用使用IntPtr来进行封送。

在C# 中,CardTest() API 对应方法定义如下:

 [DllImport("ICReader.dll", EntryPoint = "CardTest", CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]public static extern int CardTest(byte addr, byte testCardType, IntPtr protocolBeanList, ref int protocolBeanListCount, int maxLength);

则C#调用如下:


int memSize = Marshal.SizeOf(typeof(PROTOCOLBEAN)) * 1024;
IntPtr protocolBeanListBuf = Marshal.AllocHGlobal(memSize);                    // 通过使用指定的字节数,从进程的非托管内存中分配内存//调用非托管类型api
CardTest(mAddrress, testCardType, protocolBeanListBuf, ref protocolBeanListCount, 1024);//分配托管类型的内存
PROTOCOLBEAN[] protocolBeanList = new PROTOCOLBEAN[1024];
for (int ik = 0; ik < protocolBeanListCount; ik++)
{IntPtr ptr = new IntPtr(protocolBeanListBuf.ToInt64() + Marshal.SizeOf(typeof(PROTOCOLBEAN)) * ik);protocolBeanList[i] = (PROTOCOLBEAN)Marshal.PtrToStructure(ptr, typeof(PROTOCOLBEAN));        //保存数据
}Marshal.FreeHGlobal(protocolBeanListBuf); // 释放内存

在上面中通过Marshal.Sizeof(),计算非托管类型所占用的空间,然后通过Marshal.AllocHGlobal()方法从进程的非托管内存中分配内存(字节流)。然后调用非托管类型的API,将API返回数据暂存到前面通过Marshal.AllocHGlobal()方法分配的内存中。接着,就讲非托管内存中的数据拷贝出来,使用下面这个方法:

        //// 摘要://     将数据从非托管内存块封送到新分配的指定类型的托管对象。//// 参数://   ptr://     指向非托管内存块的指针。////   structureType://     待创建对象的类型。此对象必须表示格式化类或结构。//// 返回结果://     一个托管对象,包含 ptr 参数指向的数据。//// 异常://   System.ArgumentException://     structureType 参数布局不是连续或显式的。- 或 -structureType 参数是泛型类型。////   System.ArgumentNullException://     structureType 为 null。[ComVisible(true)][SecurityCritical]public static object PtrToStructure(IntPtr ptr, Type structureType);

这个方法需要一个带有当前非托管内存地址的IntPtr指针对象,因此,可以根据结构体大小和非托管内存的首地址计算每个结构体地址(类似C/C++ 内存操作)

IntPtr ptr = new IntPtr(protocolBeanListBuf.ToInt64() + Marshal.SizeOf(typeof(PROTOCOLBEAN)) * ik);

protocolBeanListBuf.ToInt64() 记录的就是非托管内存首地址,然后Marshal.SizeOf(typeof(PROTOCOLBEAN)) 是每个非托管类型所占用的空间。

最后,使用Marshal.PtrToStructure(), 将数据从非托管内存块封送到新分配的指定类型的托管对象(类似C/C++ 的memcpy() )

其实在C/C++ 中,结构体的拷贝和复制都是将结构体当做字节流进行操作的,因此这个与C/C++ 类似的。

最后使用Marshal.FreeHGlobal(), 释放以前从进程的非托管内存中分配的内存。

好了,终于写完了,又到半夜12点了,睡觉。

追加结构体,工具类:

class StructUtils<T>{/// <summary>/// 获取非托管内存/// </summary>/// <param name="count"></param>/// <returns></returns>public static IntPtr GetStructToIntPtr(int count){int memSize = Marshal.SizeOf(typeof(T)) * count;IntPtr intPtr = Marshal.AllocHGlobal(memSize);                    // 通过使用指定的字节数,从进程的非托管内存中分配内存return intPtr;}/// <summary>/// 将非托管内存数据封送到托管内存/// </summary>/// <param name="intPtr"></param>/// <param name="count"></param>/// <returns></returns>public static T[] IntPtrtToStructArray(IntPtr intPtr, int count){T[] tArray = new T[count];IntPtr ptr;for (int ik = 0; ik < count; ik++){ptr = new IntPtr(intPtr.ToInt64() + Marshal.SizeOf(typeof(T)) * ik);tArray[ik] = (T)Marshal.PtrToStructure(ptr, typeof(T));        //保存数据}return tArray;}/// <summary>/// 将非托管内存数据封送到托管内存/// </summary>/// <param name="intPtr"></param>/// <param name="count"></param>/// <returns></returns>public static void IntPtrtToStructArray(T[] tArray, int offset, int length, IntPtr intPtr){IntPtr ptr;for (int ik = 0; ik < length; ik++){ptr = new IntPtr(intPtr.ToInt64() + Marshal.SizeOf(typeof(T)) * ik);tArray[offset + ik] = (T)Marshal.PtrToStructure(ptr, typeof(T));        //保存数据}}/// <summary>/// 将托管内存数据封送到非托管内存/// </summary>/// <param name="tArray"></param>/// <param name="intPtr"></param>public static void CopyStructToIntPtr(T[] tArray, IntPtr intPtr){IntPtr ptr;for (int ik = 0; ik < tArray.Length; ik++){ptr = new IntPtr(intPtr.ToInt64() + Marshal.SizeOf(typeof(T)) * ik);Marshal.StructureToPtr(tArray[ik], ptr, true);}} /// <summary>/// 将托管内存数据封送到非托管内存/// </summary>/// <param name="tArray"></param>/// <param name="intPtr"></param>public static void CopyStructToIntPtr(T[] tArray, int offset, int length, IntPtr intPtr){IntPtr ptr;for (int ik = 0; ik < length; ik++){ptr = new IntPtr(intPtr.ToInt64() + Marshal.SizeOf(typeof(T)) * ik);Marshal.StructureToPtr(tArray[offset + ik], ptr, true);}}/// <summary>/// 释放资源/// </summary>/// <param name="ptr"></param>public static void CloseIntPtr(IntPtr ptr){Marshal.FreeHGlobal(ptr);}}

参考大神文章:

C# 调用dll 封送结构体 结构体数组

使用Signature Tool自动生成P/Invoke调用Windows API的C#函数声明

超详细解析托管与非托管

.NET学习之路----我对P/Invoke技术的理解(一)

托管代码C#调用非托管C++ API, 封送嵌套结构体数组相关推荐

  1. C#调用C++DLL传递结构体数组的终极解决方案

    在项目开发时,要调用C++封装的DLL,普通的类型C#上一般都对应,只要用DllImport传入从DLL中引入函数就可以了.但是当传递的是结构体.结构体数组或者结构体指针的时候,就会发现C#上没有类型 ...

  2. 使用C#调用非托管DLL函数

    由于工作需要,学习了GDI+编程的一些知识.其中看到了一个比较好的Demo,深入的了解后,却发现自己对如何用C#调用非托管DLL函数也有了更好的理解,于是整理了一下,跟大家一起分享. 引用: 用C#来 ...

  3. C#调用非托管C++DLL:直接调用法

    在实际软件开发过程中,由于公司使用了多种语言开发,在C#中可能需要实现某个功能,而该功能可能用其他语言已经实现了,那么我们可以调用其他语言写好的模块吗?还有就是,由于C#开发好的项目,我们可以利用re ...

  4. 利用c#实现远程注入非托管WIN32程序,并利用嵌入汇编调用非托管WIN32程序中的内部过程...

    c#通过调用windows API函数,可以很轻松的完成非托管WIN32程序的注入.内存读写等操作,以下为c#实现远程注入非托管WIN32程序,并利用嵌入汇编调用非托管WIN32程序中的内部过程的源码 ...

  5. C#调用非托管Dll

    最近在项目中碰到需要调用非托管C++生成的dll,下面将自己遇到的问题,以及解决的办法总结如下:   1.      问题:     我们通常去映射dll的方法是使用         public c ...

  6. C#调用非托管dll文件

    C#调用非托管dll文件 C#中如何调用动态链接库DLL C#对两种类型动态库的使用 1.托管 2.非托管 C#调用非托管dll 一.C++头文件样子 解决方案: 二.使用DLLImport类 三.二 ...

  7. 关于C#调用非托管DLL,报“内存已损坏的”坑,坑,坑

    因客户需求,与第三方对接,调用非托管DLL,之前正常对接的程序,却总是报"内存已损坏的异常",程序进程直接死掉,折腾到这个点(2018-05-11 00:26),终于尘埃落定,直接 ...

  8. C#:向C++封送结构体数组

    在使用第三方的非托管API时,我们经常会遇到参数为指针或指针的指针这种情况, 一般我们会用IntPtr指向我们需要传递的参数地址: 但是当遇到这种一个导出函数时,我们如何正确的使用IntPtr呢, e ...

  9. 结构体内容引用自非结构体数组对象axes(handles.axes1)

    Matlab结构体内容引用自非结构体数组对象 matlab的gui报错axes(handles.axes1) 如何解决 起因 代码 matlab的gui报错axes(handles.axes1) 废话 ...

最新文章

  1. Android进阶知识:事件分发与滑动冲突(一)
  2. UITableVeiw相关的需求解决
  3. 人均奖金300万,2021年“科学探索奖”名单揭晓:高会军周昆上榜,女性获奖人8位创纪录...
  4. 物联网产品:你需要知道的9种智能家居产品
  5. linux 查看cpu和磁盘使用情况
  6. 表单中的只读和禁用属性
  7. 单位内部一个计算机系统属于,2012年计算机一级MsOffice第五十九套练习题及答案解析...
  8. 云播自带解析php,使用PHP SDK,web端的华为云视频点播接入,加密视频播放的坑与解决方案-全代码篇...
  9. git commit -m 'comment' 遇到 'npm' 不是内部或外部命令,也不是可运行的程序 或批处理文件。
  10. Android Gradle动态打32位或者64位的包
  11. 使用Java实现面向对象编程(6)
  12. 备忘录模式-Memento
  13. 【概率论与数理统计 Probability and Statistics 3】—— (important)全概率公式和贝叶斯公式
  14. 如何使用PDF编辑软件给PDF删除页码
  15. 错误码errno和perror函数
  16. win7系统下 安装anaconda时报错“failed to create menus”的解决方案
  17. 【JS处理excel,SheetJS入门笔记】
  18. MATLAB冒号用法
  19. 网上下的--ARM入门笔记
  20. window下使用C++ Bonjour配置服务

热门文章

  1. iOS开发网络篇 一一 文件上传
  2. html给按钮加颜色代码,css按钮属性 html中按钮的字体颜色怎么设置?
  3. 关于LoRa模块你需要知道的一切
  4. 四十四、Docker-网络模式
  5. 【制作脑图】万彩脑图大师教程 | 卸载万彩脑图大师
  6. 【C++】DLL文件的编写与实现——三步走
  7. 中国电信计算机岗待遇,中国电信的abc类员工待遇及应届生工资定级
  8. 整体大于各部分功能之和
  9. 计算机网络原理2020年8,2020年自考计算机网络原理考前资料
  10. 最新心形拼图小程序源码+带流量主