最近在开发服务后台的时候,使用c#调用了多个c++编写的dll,期间遇到了一系列的问题,通过一番努力最后都一一解决了,在此作个总结,方便之后参考,毕竟这些问题也都是很常见的,主要有如下问题:html
c#调用c++方法时,首先要在类中定义一个与c++方法对应的外部方法,由于该方法是用C#语言定义的,那么确定要弄清楚C#类型与c++类型如何对应,不然会致使调用失败,关于这个问题其实不算什么问题,网上有不少类型对照的文章,都有很详细的对应列表,用的时候参考一下就能够了。还可使用工具,自动根据c++方法签名生成对应的C# import方法签名,参考P/Invoke Interop Assistant。不过有一个问题仍是要注意的,在x86模式下c#中的int对应c++中的int,而在x64模式下C#中的int是对应c++中的long,就这么一个小小的变量类型,在不经意间可能就会致使c++代码出错。c++
还有一个问题是:托管的 PInvoke 签名与非托管的目标签名不匹配
,能够在C#代码的方法特性上加上CallingConvention.Cdecl
。以下所示:web
[DllImport("dllname.dll", CharSet = CharSet.Ansi, EntryPoint = "methodname", CallingConvention = CallingConvention.Cdecl)]
因为这个问题常常遇到,而且若是不能解决的话确定不会再考虑使用该dll了,这是一个可用性的问题。因此我在调用c++方法的时候,一般都会先批量跑一边,经过日志记录下每调用一次方法后,当前进程所占用的内存大小,这样在运行一段时间之后,就能很清楚的看到内存是否持续增加,若是是的话就须要和编写该dll的同事进行沟通,给他们提供测试数据,确认产生问题的缘由。有时即便C++中的方法进行了内存释放,而且在c++测试代码中已经没有内存增加问题了,可是在C#中调用的时候内存仍是会持续增加,该问题可能跟使用的场景有关,我这里是由于调用了一个返回char *类型的c++方法,我直接用C#中的字符串类型的一个变量接收了,结果发现内存老是释放不了,后来让同事把c++的方法更改了一下参数,而后在C#中用StringBuilder类型的变量做为参数传入c++方法中来接收该方法的结果,这样该内存问题就解决了。express
C# // 在C#中声明与C++方法对应的dllimport方法 [DllImport("dllname.dll", CharSet = CharSet.Ansi, EntryPoint = "Handle", CallingConvention = CallingConvention.Cdecl)] public static extern bool CPPMethod(string content,StringBuilder result); // 该变量用来接收c++方法的处理结果,做为传出参数传入c++方法,在构造的时候必须明确指定大小 // 若是不指定或者指定的大小不足,会致使c++方法出现空间分配不够的异常 StringBuilder resultSB = new StringBuilder(length); string cppParam = "some content"; bool isSuccess = CPPMethod(cppParam,resultSB); // CPPMethod是与C++方法对应的dllimport方法 C++ // C++中的DLL函数原型,即:C#中要调用的方法,此处再也不返回char *类型的结果,而是将结果放到传出参数result中 extern "C" __declspec(dllexport) bool Handle(char* content, char* result); // result为传出参数
有的时候内存问题是纯粹因为c++代码致使的,通常遇到内存问题,我会用c++的测试工程再跑一遍,看看是否仍有该问题,若是是说明真是c++的bug了,能够通知同事去修改bug了。c#
内存问题有时候并不会体现的十分明显,这须要咱们更加细心的观察日志并发现致使问题的真正缘由。我以前遇到该方面的一个问题,刚开始内存涨幅很是明显,通过屡次与开发该dll的同事沟通后,问题已经解决的差很少了,可是大量测试后发现内存仍是会有一点上涨,虽然幅度很小,但第六感告诉我此中必有蹊跷,这要是上线跑个几天岂不是还得爆,后来我把每一次调用c++方法后当前进程占用的内存输出到文件中,通过仔细观察,发现绝大部分文件(文件内容要传入c++方法中进行处理)都没问题,内存都很平稳,可是有极小一部分文件在传入c++方法后,会致使内存相比其余文件有一个明显的增加,看来问题是出如今这些文件中,随后把这些文件单独放在一块儿进行循环调用,内存一会儿就大幅增加了,后面就不用说了,问题固然解决了。所以,要保持记日志的良好习惯,哪怕是在测试工程中
。服务器
版本不匹配的话,在调试时会提示正在加载格式不正确的dll
,若是使用的是32位的c++版dll,须要把C#项目的编译平台设置为x86,若是使用的是64位的c++版dll,则设置为any cpu和x64均可以,这个须要本身根据实际状况对应好就能够了。若是程序对内存的使用比较高,最好将程序编译为64位,由于32位程序对单进程的内存大小有限制,经测试最大不超过2G。由于个人程序刚开始使用的是32位的c++版dll,而且在运行时须要调用这些dll加载不少资源,加载完这些资源进程占用的内存就差很少快2G了,因此总会莫名其妙的崩掉,甚至在加载的过程当中就直接崩掉了,当时预感到是32位的问题,后来让同事将dll从新编译为64位后就没有这个问题了。能够经过dumpbin命令判断一个dll是32位仍是64位,打开vs开发人员命令提示,输入:dumpbin /headers 你的dll路径
,例如:dumpbin /headers d:\test.dll
,以下图所示:多线程
若是是32位dll,红框那里会显示 并发
这里有一个地方须要注意,默认asp.net项目在调试时会运行在32位下的iisexpress进程中,若是你的项目是64位的,那么须要在VS中将iisexpress配置为64位模式,以下图所示:asp.net
这个问题在运行时有时候会提示dll加载不成功,这个问题在不一样的电脑上会有不一样的体现,有的存在这个问题,有的就运行正常。而我本机就属于正常的,部署的服务器属于出问题的。出现这个问题后,在确认代码无误后,我用depends.exe
这个工具查看了一下致使问题的那个c++版的dll都依赖什么程序集,在出问题的机器上会提示有一些依赖的dll不存在,而这些dll在运行正常的机器上是存在的。下图红色框中的为某些机器上可能会缺乏的dll:函数
若是缺乏相关dll,该条目的左边会显示出一个黄色的问号。这个问题能够采用静态编译进行解决,关于什么是静态编译能够自行百度,总之就是将程序所依赖的dll编译到程序集中,这样即便其余机器不存在这些dll也能够正常运行了,静态编译能够在vs的项目属性中进行设置
默认是多线程 DLL(/MD)
,即:动态编译,这里更改成 多线程(/MT)
,即:静态编译。
刚才的配置只能解决缺乏MSVCP120.DLL和MSVCR120.DLL这一类问题,对于缺乏MFC相关的dll,还要通过下面的配置:
默认是使用标准Windows库
,这里改成在静态库中使用MFC
。
这个问题相对比较隐蔽,出现时不会抛出异常,只能经过c++方法返回的状态码来判断方法执行是否成功,要不是在这里放了一个断点,特地看了一下,可能就遗漏这个问题了。
场景是这样的:
我在webservice中调用c++版dll中的一个初始化方法,该方法会加载一些资源文件,我在vs中调试执行的时候没问题,发布之后竟然没法加载资源,貌似是路径问题,我把资源文件放到w3wp.exe的根目录下却是能够成功加载,放在其余目录中就不行,遇到这个问题首先想到的多是资源所在的目录权限不够致使iis没法正常加载,由于以前有个一样的问题就是这样,但此次将资源所在的目录更改成Everyone用户的彻底控制权限仍是不行,而且该问题只出如今b/s项目中,c/s项目没有这个问题。而且该目录中存放了不少资源文件,有好几个c++版的dll都须要从这里加载,其余几个都没问题,就这一个dll不行,看来不是权限的问题。这时候又想是否是相对路径的问题,那我改为绝对路径吧,结果问题依旧,后来在技术群里有个大牛说试试Directory.SetCurrentDirectory
,赶忙修改代码,测试了一下确实好使了。代码以下:
// 保存当前工做目录 string currWorkPath = Directory.GetCurrentDirectory(); // 切换当前工做目录 Directory.SetCurrentDirectory(resourcePath); // 初始化进行资源加载 Init(resourcePath); // 这里要注意,使用了SetCurrentDirectory方法后,resourcePath要用相对路径 // 还原当前工做目录 Directory.SetCurrentDirectory(currWorkPath);
如注释所示,使用SetCurrentDirectory切换了当前工做目录后,方法中所用的路径要改成相对路径,一开始我用的是绝对路径,竟然仍是没法加载。
后来发现了该问题的缘由,在使用的dll中又调用另一个dll进行资源加载,可能这样会致使那个间接调用的dll出现路径问题,因此出现资源加载失败。
关于异常捕获,虽然在方法中添加了特性HandleProcessCorruptedStateExceptions
与SecurityCritical
但仍是捕获不到c++中的异常,缘由多是c++在遇到某些异常时会形成程序直接退出,这样在C#中就天然捕获不到了,因此仍是尽可能保证c++代码的健壮性。
若是在c#中调用了多个c++版dll中的方法,由于有时捕获不到异常,很难经过常规方法找到问题的缘由,c++方法中一旦出现异常可能会直接致使进程退出了,这时能够借助操做系统中的事件查看器来找出异常是来自哪一个dll,同时在原有代码中注释掉那段调用该c++方法的代码,或者mock一个方法调用,保证该段代码无异常,而后再进行测试,若是无异常,那么只要解决了那个c++方法的问题便可,若是还有异常那么就是其余dll的问题,而后能够编写测试代码单独测试曾经出问题的dll中的方法。异常捕获+事件查看器+日志
能够帮助开发者发现程序的大部分问题与缘由。
这个是c++代码读取文件时可能会遇到的一个问题,虽然在调试某个问题的过程当中发现了这个状况,但后来经开发dll的同事说问题的缘由不是这个,这里就仅此记录一下吧,ifstream in("test.txt",'b');
这样加上第二个参数就不会截断了。
两次遇到这个问题都是在下班后出现的,当时也不知道什么缘由,后来经过windbg
看了一下测试程序和w3wp进程的转储文件,经过!gle -all
命令发现每一个线程都在等待状态,以下图所示:
iis进程也是如此,本觉得是代码死锁了,可是经过!locks
命令也没发现有任何异常(关于这个问题,能够参考 应用死锁分析,当时有点懵,不知道是什么形成了这种状况,后来发生一件事情让我弄明白了为何,那是在快下班的时候,程序正好出现了一个异常(虽是异常,其实不会致使程序崩溃退出),这时服务器上弹出了一个vs实时调试的提示窗口,我注意到iis的cpu使用率忽然就降为0,测试程序的控制台也输出了线程等待的消息,联想到以前那些STATUS_WAIT_0
的错误信息以及貌似死锁的状况,我感受到多是iis终止了全部线程,在等待vs实时调试这个交互窗口的结束,因为平时都是在下班后才会开启测试程序来验证程序的稳定性,因此当弹出这个交互窗口时,一直不会有人去处理,线程不会一直这么等下去,最后测试程序就退出了,iis也没法再继续处理请求了,这个交互窗口也貌似消失了(为何用貌似,由于我没有专门去留意,只是凭印象以为以前没见过),想到这我点了一下“取消调试”,程序继续往下运行了,也再也不阻塞了。因此在程序运行的时候,最好关闭VS的实时调试功能,以避免形成没必要要的问题。进入visual studio中,选择【工具】->【选项】,点击【调式】,在【实时】选项卡中把【本机】【脚本】【托管】三个对勾取消掉就能够了。
其实就算实时调试窗口不见了,咱们也能够经过系统事件来找到一些蛛丝马迹,以下图所示,只不过很难仅凭这个事件就判定问题的缘由,由于服务器上运行了多个w3wp实例,只能说经过这个状况增加一些经验了。
其实还有一些问题,到如今有点记不清了,就不敢贸然凭残存的那点记忆来描述了,以便形成没必要要的误解。对于遇到的问题,有些很明显,有些很隐蔽,有些须要仔细分析,有些须要在大量测试的状况下才会发现,这里只想说一句:测试很重要,工做需用心。