最近在開發服務後臺的時候,使用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實例,只能說經過這個狀況增加一些經驗了。
其實還有一些問題,到如今有點記不清了,就不敢貿然憑殘存的那點記憶來描述了,以便形成沒必要要的誤解。對於遇到的問題,有些很明顯,有些很隱蔽,有些須要仔細分析,有些須要在大量測試的狀況下才會發現,這裏只想說一句:測試很重要,工做需用心。