C#调用C++一些技巧
这段时间经常用C#调用自己写的和别人写的dll,有一点感觉,今天下午随手记下:
---------------------------------------------------CLRInsideOut---------------------------------------------------
1、C#调用C++,自己少不了一个工具CLRInsideOut,这个工具切到第三个选项卡,把C++代码(结构体,导出函数)粘上,生成一下,右边出现相应c#代码。
public const string dllName = "test.dll";
[System.Runtime.InteropServices.DllImportAttribute(dllName, EntryPoint = "HIK_DVR_SetLogPath", CallingConvention = System.Runtime.InteropServices.CallingConvention.StdCall)]
[return: System.Runtime.InteropServices.MarshalAsAttribute(System.Runtime.InteropServices.UnmanagedType.Bool)]
public static extern bool HIK_DVR_SetLogPath(string savepath);
记得把dllname的位置改一下,还有C++一致导出stdcall.
2、CLRInsideOut蛮神奇的,特别是对C++位域的转换,它的思路是对的,不过一般转出来都是错的,我们可以学着它思路,自己对齐好位,这就要去回顾一下各种类型的长度了。
3、总而言之CLRInsideOut生成的东西,有时候会有问题,但是它就像一个小老师一样,教你怎么pinvoke.
-------------------------------------------小技巧--------------------
技巧1:wrapper
一般你可以拿到dll,lib,h,so等文件,这些SDK往往是偏向于被C++调用的,所以你直接去pinvoke它当然可以,不过写起来古古怪的。最好加一层自己的wrapper,本文基于这样的方式。这样的wrapper更适合java,c#的语言风格,方便使用。
技巧2:最好都是由C#来分配内存
按照谁分配谁释放原则,编写wrapper层的时候,最好不管理内存,内存统一由C#来分配,C#有垃圾回收,可以省去不少时间,但要注意垃圾回收带来的内存移位问题。
技巧3:如何返回不定长对象数组
比如有一个查找日志的api是这样的,它的第4个参数是一个数组指针,指向一个日志对象数组,第五个是数组有效元素个数。
HIK_API BOOL __stdcall SearchLog(LONG lUserID, LPF_TIMESTRU startTime,LPF_TIMESTRU stopTime,LPF_DVR_LOG logs,int* Count)
=〉
[System.Runtime.InteropServices.DllImportAttribute(dllName, EntryPoint = "SearchLog")] [return: System.Runtime.InteropServices.MarshalAsAttribute(System.Runtime.InteropServices.UnmanagedType.Bool)] public static extern bool SearchLog(int lUserID, ref F_TIMESTRU startTime, ref F_TIMESTRU stopTime, [Out] F_DVR_LOG[] logs, ref int count);
哦,我加了一个[Out]这个东西,这样就可以返回了,但是内存是在C#分配的。
F_DVR_LOG[] logs = new F_DVR_LOG[10000]; int count=0; HikDll.SearchLog(userid, ref struStartTime, ref struStopTime, logs,ref count); var allLogs =( from x in logs.Take(count).ToList() select new { StartTime = x.StartTime, MajorType = x.MajorType, MinorType = x.MinorType, Info = x.Info,PanelUser=x.panelUser, NetUser = x.netUser }).ToList(); dataGridView1.DataSource = allLogs;
先在C#分配10000个元素(应该够用了),用linq 的take 把数组压缩一下基本上可以用了。
技巧4:如何使用回调
c++-----------------------------------
typedef void (CALLBACK *tagFDVRException) (int code); tagFDVRException FDVRException;//定制报警回调 //设置异常回调 HIK_API BOOL __stdcall HIK_DVR_SetExceptionCallBack(tagFDVRException callback){}
c#申明如下:------------------------------------
[System.Runtime.InteropServices.UnmanagedFunctionPointerAttribute(System.Runtime.InteropServices.CallingConvention.StdCall)] public delegate void tagFDVRException(int code); //设置异常回调 [System.Runtime.InteropServices.DllImportAttribute(dllName, EntryPoint = "HIK_DVR_SetExceptionCallBack", CallingConvention = System.Runtime.InteropServices.CallingConvention.StdCall)] public static extern bool HIK_DVR_SetExceptionCallBack(tagFDVRException callback);
如下使用即可:------------------------------------
HikDll.HIK_DVR_SetExceptionCallBack((code) => { switch (code) { case 7: MessageBox.Show("设备下线了"); break; default: break; } });
回调也可以返回数组,而且这个数组可以是C++的栈内变量,不存在回收问题,但是为了传回一个不定长对象数组,要专门写一个回调。这种解决方法与上面说的都可以使用,回调方式多走一步,但是不用在c#端开劈大空间。
技巧5:如何传入传出对象、结构体
C++如下:有个结构体obj
struct obj { int i; }; extern "C" DLL_API void __stdcall test(obj* inobj,obj* outobj) //5.给C#调用要加上extern "C"。 { outobj->i=inobj->i+1; }
对应的C#
[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] public struct obj { public int i; } [System.Runtime.InteropServices.DllImportAttribute("CsharpInvokeDll.dll", EntryPoint = "test", CallingConvention = System.Runtime.InteropServices.CallingConvention.StdCall)] public static extern void test(ref obj inobj, ref obj outobj); private void button1_Click(object sender, EventArgs e) { obj inobj = new obj(); inobj.i = 1; obj outobj = new obj(); test(ref inobj, ref outobj); }
注意:其中的红色ref,不管是输入还是输出,都要加ref。
技巧6:如何传入、传出一个字符串char *
例如我在C++中写到,test函数有两个char*参数,instr是输入,outstr是输出:
extern "C" DLL_API void __stdcall test(char* instr,char * outstr) { sprintf(outstr,"你输入了:%s",instr); }
那在c#可以如下:注意:都未加ref,instr的对应string ,outstr的对应StringBuilder.
static extern void test(string instr,StringBuilder outstr);
如下调用:
StringBuilder outstr = new StringBuilder(10); string instr = "wulala"; test(instr,outstr);
技巧6:如何联调。
在C#的项目属性中如下设置:
注:将C++的代码生成dll生成路径直接设置到到C#的debug目录下。这样在C++代码里打断点,每次编译,即可从C#调试到C++代码,不用再拷贝一次dll。