最近的項目中,使用了GO來開發一些服務中轉程序。業務比較簡單,但是有一些業務需要復用原有C++開發的代碼。而在WINDOWS,用CGO方式來集成C/C++代碼並不是太方便。所以用DLL把C++的代碼封裝起來,然后提供基本的API來完成復用。在這個過程中遇到了一些問題及解決方法,記錄下來,也給遇到類似或者同樣問題的人一個借鑒。
如果你還不清楚怎么在GO中調用DLL,可以參考這篇文章《WindowDLLs》。
Callback的限制
在WINDOWS下調用一些API時會要求傳入回調函數,在C/C++下使用非常簡單,直接傳入函數指針就可以了。但是在GO這種有GC特性,又有運行時庫的語言要稍微麻煩一點。
GO為了解決這種回調要求,在syscall包里提供了NewCallback和NewCallbackCDecl兩個函數來幫助用戶解決回調的問題。具體的Callback機制這里先不說了,只是說一下在GO里,Callback的使用是有限制的。而且對傳入的GO函數也是有對應的要求。在我看的go1.4正式版本中,src/runtime/syscall_windows.go line 71:會檢查callback的數量是否超過了最大的限制值,這個限制值目前是2000。如果超過就會拋出一個異常。這個是非常蛋疼的事情。而且從GO的ISSUE庫里,這個問題的解決是一推再推。
GO的ISSUE里關於CALLBACK的事情已經很明確了,就是要讓大家復用CALLBACK,也就是用全局函數來搞。在golang-china討論組里,minux給予了這樣的回答:
我說一下callback上限的原因。由於系統調用callback函數的時候不提供任何其他的參數,導致區分不同的callback只能通過被調用函數的地址,也就是說,一個Go的callback函數必須對應一個單獨的C函數地址
舊的機制是對每個Go函數,在堆上動態構造一個對應的C函數。這樣在 Go 1.1 之前的時候沒問題,因為當時函數閉包也需要可執行的堆,但是 1.1 修改了函數的表示方式,閉包不再需要動態代碼生成了,為了把 Windows 上也不再需要可執行的堆,必須想另外一個辦法來實現 callback,新的機制參加 issue 5494,是我提議的。既然不動態生成代碼,很明顯的一個問題就是 callback 的總數會有一個上限。這是這有任何辦法的了。
其實一般的 Windows 程序也不會有那么多 callback,2000個絕對是綽綽有余的;之所以 Go 這里很容易用光,是因為可能大家願意使用 closure,而不是一個全局函數做 callback,使用全局函數做 callback (C/C++程序就是這么做的),2000個絕對是綽綽有余;但是由於閉包每次建立都是不同的,就算你其實就一個地方需要創建 callback,用閉包的話2000次就用光了所有的 callback。使用 callback 的時候建議用全局函數,也不要用 method,因為那樣也是閉包,2000 個絕對足夠足夠。
所以如果你要使用CALLBACK的時候,盡量的想辦法復用,否則會很囧。
棧溢出(0xC00000FD, _chkstk)
這個問題比較囧。我封裝的DLL里用到了另外同事寫的代碼,他的風格是把函數寫的巨大,在C++下運行一切正常,但是當在GO寫的程序里調用的時候,內部拋出了0xC00000FD的錯誤,也就是棧溢出。
發生的地方是_chkstk。這個函數是VS下的C++編譯器在代碼生成的時候加進去的,所以別想着通過參數啊什么的干掉了。這個函數的作用是說當你的函數內有超過了一頁大小的變量時,編譯器會把在函數頭插入對_chkstk的調用代碼,_chkstk會檢查棧的大小是否足夠該函數的局部變量使用,如果不夠,它會訪問棧的GUARD PAGE,然后會觸發系統內核檢查到該錯誤,這個時候操作系統會擴展棧的大小。首先這里就有矛盾了,GO語言本身會擴展棧,而為了做到這點就要把堆拿來當棧使用,但是這個時候的問題是,它和OS默認的棧空間不同,導致內核檢查到的錯誤是不可擴展的,這個時候OS也搞不定了,只能讓程序掛掉了。而且OS本身支持的棧擴展是有限制的,不像GO實現的棧擴展,WINDOWS下這個擴展最終會觸發到一個無法申請的棧地址,如果無法擴展棧,還是要讓程序掛掉。可以點這里《What is the purpose of the _chkstk() function》查看更詳細的解釋。所以問題的本質就是局部變量太大(自己反匯編DLL發現確實有個對象變量體積巨大...),解決方法就是該把變量該放到堆里的放到堆里的就放到堆里。最后的效果是,再也不拋出這個0xC00000FD的異常了。
感想
其實上面用到的C++代碼,可以用GO直接重寫的,但是考慮到時間成本,最后還是放棄了。CALLBACK的問題我是暫時重寫了對應的API代碼來搞定。
GO在寫網絡通信,並發場景時比較嗨皮,但是這種跨語言的交互操作實在是太蛋疼。只能且行且蛋疼了
Update:
這個問題后來被我提交給GO的開發團隊了。具體詳情可以見這里:https://github.com/golang/go/issues/9457
按照minux的說法就是需要在使用DLL時的,導入runtime/cgo即可。是說編譯器編譯的時候,發現沒有使用CGO的話,就會把系統線程的棧大小設置為64KB,而且是不擴展的;但是如果使用 了CGO,就會變大默認棧,同時變為可擴展的。
最后還是得吐槽下,What the fuck!對於GO的這種使用方法真心不喜歡,實在是太蛋疼。CGO方式還要依賴GCC之類的玩意。WINDOWS下想愉快的玩耍根本不是太可能。改天再說下用GO來的感受。