(已更新)关于混合编程中C#调用C/C++DLL碰到具有数组、指针的参数或返回的解决办法(亲测)!

更新部分直接看文章最后!


        

如题,最近一段一直在搞工控类项目的上位机调度软件开发,由于扫描模块是余博用C++写的, 所以涉及到混合编程的问题了。

        C#调用C++DLL的方法网上都有,把DLL放进exe的生成目录内,然后引用System.Runtime.InteropServices命名空间,然后在主类内部加上外部引用声明就好了。

        [DllImport("CLMS511Data_MFC.dll", EntryPoint = "CLMS511Data", CallingConvention = CallingConvention.Cdecl)]
        public static extern void clms511data(ref Int32 m,ref Int32 n);

具体声明如上,EntryPoint是DLL内对应的方法名,下面的方法命是自己重命名的,这个随意,后面调用就用这个名字。修饰符public可以换别的或去掉,其他照写吧。

        重点就是在这里,dll内的方法根据接口和余博的描述看,无返回,但接受两个Int32型数组指针,方法格式为大致为:

void function(int32* a,int32* b)

        C++DLL的部分功能就是读取两个数组指针,输出至文本文档,修改数组内容(第一个数组每个+2,第二个数组每个+5)。

        而我的C#主函数流程很简单:初始化数组,调用DLL方法将数组作为参数传递,然后输出两个数组各自最后的元素至textBox1。

        以上有两个输出,做个记号。

        现在说说我的程序对接口的要求,不关只是传递数组内容,而且最好是能共享内存区块,这样dll负责写入数据,主程序读就好了,不需要主程序与dll频繁通信,共享内存就好了。学过C/C++的同学都知道,这对于C来说无非就是传个指针,规定数据类型和长度就好了。但就这个指针对于C#来说却不是那么容易的。C#是托管代码,依附于.Net,对机器内存直接操作不是那么方便的。

 

        当然,网上有各种解决方法,但大都是一长串代码,对于C#不是很精通的人来说理解起来还是有困难的,理解的不好,用起来就容易出bug。最有代表性的一种方法就是下面全部代码中被我注释掉的部分,利用intPtr型数据和Marshal类。intPtr这个看官方中文api说明是c#的指针,而Marshal类包含开辟内存的方法,所以声明一个intPtr类并让它指向Marshal.AllocHGlobal()返回的对象

intPtr px=Marshal.AllocHGlobal(400)//400为100维int32数组所占字节数

然后将dll方法的声明和调用的参数改为ref intptr型。这个方法不报错,但是输出的结果很有意思,两个数组前面5个数据都是错误的无意义数据,从第6个数据开始输出数组的第一个元素,输出到数组第95个数据为止。迄今没搞明白原因是什么,有知道的大神留个盐,不甚感激。

        我用的方法网上也有端倪,就是有关混合编程数据类型对照的问题。C需要指针的时候你传什么过去呢?对于数组,有人说直接传数组名啊,比如我定义的int32[] x,直接ref x就好了啊,其实这个方法你试过就知道不行,你传过去这个数组,你再访问这个数组就会报索引越界错误。

        解决办法是我拍脑袋想到的:纯粹试一试,因为想到C++读数组或指针其实都是数组第一个元素的地址,所以想到直接传x[0]过去。万万没想到,居然出奇的好用,dll里输出的就是我数组的结果,然后在dll里修改了之后,程序主体输出发现数组被改变了,证实确实是达到了共享内存的目的(至少看起来是,内部实现是不是不好说)。

        此处只是做实验的程序demo,数据类型理论上可以改为double等等,二维数组也简单没测试,但其实两边都把二维数组处理成一维就ok了呀。

        总结起来就一句话:对于C/C++需要的指针类型的形参,dll的方法声明里用ref 类型 x,调用的实参用ref a[0]就好了,a就是你C#里的数组。

        全部C#代码如下,C++功能看上面:

using System;
using System.Windows.Forms;
using System.Runtime.InteropServices;

namespace WindowsFormsApp01
{
    public partial class Form1 : Form
    {

        [DllImport("CLMS511Data_MFC.dll", EntryPoint = "CLMS511Data", CallingConvention = CallingConvention.Cdecl)]
        public static extern void clms511data(ref Int32 m,ref Int32 n);

        public Int32[] x = new Int32[100];
        public Int32[] time = new Int32[100];
        //IntPtr px = Marshal.AllocHGlobal(400);
        //IntPtr ptime = Marshal.AllocHGlobal(400);
        
        public Form1()
        {
            InitializeComponent();
            int i = 0;
            while (i < 100)
            {
                x[i] = i * 2;
                time[i] = i * 3;
                i++;
            }
            //Marshal.Copy(x,0,px,100);
            //Marshal.Copy(time, 0, ptime, 100);

        }



        private void Form1_Load(object sender, EventArgs e)
        {
            clms511data(ref x[0], ref time[0]);
            textBox1.Text = x[99].ToString()+"    "+time[99].ToString();
        }
    }
}

        以上有两个输出,第一个输出由调用的dll完成,文本文档内容无误,最后的元素分别为198和297;另一个由C#程序主体完成,textBox1内容为200和302第二次输出的结果可以看出数组元素被改变了,这里就不贴图了。


更新部分:经实践,本文中已ref或out引用方式直接传数组首地址的方法在程序刚开始运行时确实可行,可随着程序后续的继续运行,特别是在某些关键操作之后,发出的指针在接收方会发生偏移或改变,原因未知(猜测是由C#的内存分配导致,C#的数组地址是会在某些与内存有关的操作后移动的,这种移动对C#程序不可见,但C++端按原固定地址取读或写数据就会发生错误。比较正确的方法应该是:1.用IntPtr类和Marshal.AllocHGlobal()方法开辟非托管内存给指针,这种非托管内存形成的指针与平台无关,不会移动;2.将C#内的数据与指针所指内存数据利用Marshal.Copy()方法进行数据交互;3.由于是非托管内存,C#垃圾回收不会动它,因此尽量在用完后利用Marshal.FresHGlobal()释放掉内存块。需要注意的是,IntPtr实例本身就是指针,可以直接作为实参传递给方法,无需再加ref或out,我此前用IntPtr就是在此跌了跟头导致走了很多弯路,所以才有本文的传递数组首元素的不完全方法。

你可能感兴趣的:(C#)