目錄
托管和本機代碼互操作適用哪種場合?
互操作技術:三種選擇
互操作技術:P/Invoke
互操作技術:COM Interop
互操作技術:C++/CLI
互操作體系結構注意事項
API 設計和開發人員體驗
互操作邊界的性能和位置
生存期管理
托管和本機代碼互操作適用哪種場合?
有關使用托管和本機代碼互操作適宜時機的論述並不多,現有的論述也以自相矛盾者居多。有時,指南還缺乏實踐體驗做依據。因此,我先聲明我編寫的這個指南以我們互操作團隊的實踐經驗為基礎,它已向各類內部和外部客戶提供過幫助。
在總結這一經驗時,我們采納了三種產品,由它們充當成功使用互操作和典型使用方式的上佳示例。提及互操作時,我首先想到的應用程序就是 Visual Studio Tools for Office,它是 Office 的托管擴展性工具集。它代表互操作的一種典型使用情況——一個想要啟用托管擴展或加載項的大型應用程序。另一個就是 Windows Media Center,從一開始,它就是一個混合了托管和本機的應用程序。開發 Windows Media Center 主要使用的是托管代碼和本機代碼中內置的一些內容,即負責直接處理 TV 調諧器和其他硬件驅動程序的代碼段。最後是 Expression Design,一個具備大型預置本機代碼庫的應用程序,它計劃利用 Windows Presentation Foundation (WPF) 這一新的托管技術,提供全新的用戶體驗。
這三個應用程序解釋了使用互操作的三個最普遍的原因:讓原有的本機應用程序具備托管擴展性;讓應用程序的大部分內容能利用托管代碼的優點,同時又能在本機代碼中編寫最基礎的代碼段;為現有本機應用程序注入全新的用戶體驗。
過去,指南中給出的對策是用托管代碼徹底重新編寫應用程序。采納這一建議並目睹許多人將其拒之門外之後,您會清楚這一方案對於大部分現有應用程序來說都不適用。互操作非常有助於開發人員維護其在本機代碼中的投資,同時還能讓他們利用新的托管環境。如果您由於其他原因計劃重新編寫應用程序,托管代碼是個不錯的選擇。但一般而言,您不想只為使用新的托管技術而重新編寫程序,因此也就談不上互操作。
互操作技術:三種選擇
.NET Framework 中有三種主要的互操作技術,具體選用哪種由您用於互操作的 API 類型及控制邊界的要求和需要決定。Platform Invoke(或 P/Invoke)基本上是從托管到本機的互操作技術,您可以用它從托管代碼調用 C 類本機 API。您還可以使用 COM interop 技術從托管代碼使用本機 COM 接口,或從托管 API 導出本機 COM 接口。最後是 C++/CLI(先前稱為托管 C++),它允許您創建包含托管和本機 C++ 混合編譯代碼的程序集,該程序集旨在為托管代碼和本機代碼搭建起溝通的橋梁。
互操作技術:P/Invoke
P/Invoke 是三種技術中最簡單的一個,它的主要功能是讓托管代碼能訪問 C 類 API 。使用 P/Invoke 時,您需要分別封裝每個 API。如果要封裝的 API 數量不多且其簽名也不復雜,這是個不錯的選擇。但是,如果 API 有很多參數,且這些參數沒有好的托管對等項,如變量長度結構、void *、重疊的共同體等,那麼 P/Invoke 使用起來會相當難。
.NET Framework 基類庫 (BCL) 包含 API 的多個示例,它們就是多個 P/Invoke 聲明外部厚實的包裝。在包裝非托管 Windows API 的 .NET Framework 中,幾乎所有功能都是使用 P/Invoke 構建的。實際上,即便是 Windows 窗體,也差不多完全是使用 P/Invoke 在本機 ComCtl32.dll 基礎上構建的。
這裡有幾個非常有用的資源,可以極大地降低 P/Invoke 的使用難度。首先,pinvoke.net 網站上有一個 wiki,最初是由 CLR 互操作團隊的 Adam Nathan 設置的,裡面有大量由用戶為各種通用 Windows API 貢獻的簽名。
還有非常便於使用的 Visual Studio 加載項,利用它可以輕松從 Visual Studio 訪問 pinvoke.net。對於 pinvoke.net 上沒有的 API(可能是您自己或他人庫中的 API),互操作團隊已發布了一個 P/Invoke 簽名生成工具,稱為 P/Invoke Interop Assistant,它能根據頭文件自動為本機 API 創建簽名。隨附的截圖顯示了處於使用狀態的工具。
在 P/Invoke Interop Assistant 中創建簽名
互操作技術:COM Interop
COM interop 允許您從托管代碼使用本機 COM 接口,或將托管 API 公開為 COM 接口。您可以使用 TlbImp 工具生成托管庫,讓它公開一個托管接口,以便與特定的 COM tlb 通話。TlbExp 執行相反的任務,生成一個 COM tlb,其中的接口與托管程序集中的 ComVisible 類型相對應。
如果您已經在應用程序中使用 COM 或將其視為擴展模型,則非常適合使用 COM interop。它還是在托管代碼和本機代碼之間維護完全保真的 COM 語義的最簡便途徑。如果您與基於 Visual Basic 6.0 的組件互操作,尤其適合使用 COM interop,因為 CLR 基本與 Visual Basic 6.0 遵循相同的 COM 規則。
如果您尚未在內部使用 COM,或您不需要完全保真的 COM 語義且它的性能不滿足您應用程序的要求,則 COM interop 的作用不大。
在應用程序中,Microsoft Office 是使用 COM interop 在托管代碼和本機代碼間實現互操作的最典型示例。Office 是 COM interop 的上佳備選項,因為它一直將 COM 用做其擴展機制,也是 Visual Basic for Applications (VBA) 或 Visual Basic 6.0 最常使用的工具。
Office 原本完全依靠 TlbImp 和瘦互操作程序集做為其托管對象模型。但是,隨著 Visual Studio 中內置了 Visual Studio Tools for Office (VSTO) 產品,這就提供了越來越豐富的開發模型,這些模型中融入了本專欄所述的諸多准則。現在使用 VSTO 產品時,有時很容易忘記 COM interop 是 VSTO 的基礎,就象忘記 P/Invoke 是許多 BCL 的基礎一樣。
互操作技術:C++/CLI
C++/CLI 旨在為托管代碼和本機代碼搭建起溝通的橋梁,您可使用它將托管和本機 C++ 同時編譯到同一程序集(甚至同一類)中,並在程序集的兩部分之間執行標准的 C++ 調用。如果您使用 C++/CLI,您可選擇想讓程序集的哪一部分成為托管形式,哪一部分成為本機形式。生成的程序集是 MSIL(Microsoft 中間語言,可在所有托管程序集中找到)與本機程序集代碼的混合。C++/CLI is 是非常強大的互操作技術,您幾乎可以用它完全控制互操作邊界。它的缺點是強制您取得對邊界的絕大部分控制權。
如果需要靜態類型檢查、滿足嚴格的性能要求且可預測性更強的定案,C++/CLI 可以出色擔當橋梁作用。如果 P/Invoke 或 COM interop 能滿足您的需要,通常它們更易於使用,尤其是開發人員對 C++ 不甚熟悉時更是如此。
考慮 C++/CLI 時,有幾點需要注意。首先需要注意的是如果您計劃使用 C++/CLI 充當速度更快的 COM interop,由於 COM interop 替您完成大量工作,所以它的速度要比 C++/CLI 慢。如果您只是想在應用程序中使用一下 COM,並不要求完全保真的 COM interop,這是一個不錯的折衷方案。
但是,如果您使用了許多 COM 規范,您會發現一旦要將 COM 語義內容加入 C++/CLI 解決方案,需要做大量的工作,並且它的性能比不上 COM interop。Microsoft 的幾個團隊試用過這種方法,發現它的這一缺點後轉為繼續使用 COM interop。
使用 C++/CLI 時,第二個需要注意的事項是它的作用僅限為托管代碼和本機代碼搭建橋梁,不適合用於編寫應用程序的主體內容。雖然您確實可以用它編寫程序,但與純 C++ 或純 C#/Visual Basic 環境相比,開發人員的生產率要低很多,並且應用程序的啟動速度也慢得多。因此,如果您使用 C++/CLI,建議僅用 /clr 開關編譯哪些必需的文件,而使用純托管或純本機程序集的組合構建應用程序的核心功能。
互操作體系結構注意事項
一旦您已決定在應用程序中使用互操作且確定了要用的技術,在建立解決方案的體系結構時,有幾個高層級的注意事項,包括您的 API 設計和開發人員在針對互操作邊界編寫代碼時的體驗。還需考慮本機托管轉換的放置位置和可能對應用程序產生的性能影響。最後要考慮您是否需要填補托管環境中的垃圾收集與本機環境內手動/確定性生存期管理間的差異。
API 設計和開發人員體驗
在考慮 API 設計時,您必須先問自己幾個問題:誰將為我的互操作層編寫代碼,我是應該通過優化改進他的體驗,還是應該將構建邊界的成本降至最小?針對這一邊界編寫代碼的開發人員是不是就是編寫本機代碼的人員?還是他們不負責編寫本機代碼?他們是負責擴展您的應用程序或將其用作服務的第三方開發人員嗎?他們的技術水准如何?他們願意使用本機模式嗎?還是只習慣編寫托管代碼?
如能回答這些問題,則有助於在本機代碼的超薄包裝與內部使用本機代碼的豐富托管對象間確定合適的統一體。在超薄包裝中,所有本機模式清晰可見,開發人員可以對邊界了如指掌,並清楚認識到他們是在針對本機 API 編寫代碼。對於厚實的包裝,您幾乎可以完全隱藏有本機代碼參與這一事實——BCL 中的文件系統 API 就是提供了一流托管對象模型的超厚互操作層的極好示例。
互操作邊界的性能和位置
在花費大量時間優化應用程序前,有必要先確定您是否有互操作性能問題。許多應用程序在對性能有嚴格要求的內容中使用互操作,它們對此應尤為注意。但對於其他那些在對用戶的鼠標單擊響應中使用互操作的應用程序而言,不想看到會為用戶帶來延遲的成百上千的互操作轉換。這就是說,如果您確實關注互操作解決方案的性能,應把握兩個原則:減少互操作轉換的數量和每個轉換所傳遞的數據量。
托管和本機代碼間具備給定數據量的給定互操作的成本基本上也是固定的。具體的固定成本視您選擇的互操作技術而定,但如果您選擇的前提是需要用到某項技術的功能,那麼通常不會再有更改。這意味著您的側重點就是先減小邊界的干擾,然後減少跨邊界傳輸的數據量。
如何達成這一目標很大程度上取決於您的應用程序。但常用策略是在定義繁忙和大數據量接口的邊界的一側編寫幾行代碼,來移動隔離邊界,這一策略已有多個成功運用的實例。基本方法是編寫一個抽象層,將調用分批放入非常繁忙的接口,更好的辦法是在邊界間移動需要與此 API 交互的應用程序邏輯塊,並且僅跨邊界傳送輸入和結果。
生存期管理
對於互操作客戶而言,托管與本機環境之間生存期管理的差異是最大的難題。.NET Framework 中基於垃圾收集的系統與本機環境中的手動和確定性系統間存在著本質差異,這種差異的表現形式常常十分怪異,難以診斷。
互操作解決方案中第一個需要注意的問題是在托管環境使用完本機資源後,一些托管對象仍長時間占有這些資源。如果本機資源十分稀少,需要調用方使用之後迅速釋放(數據庫連接就是這方面的確切示例),這種占用通常會造成問題。
如果這類資源很充足,您只需讓垃圾收集器調用對象的終結器,然後讓該終結器顯式或隱式釋放本機資源即可。如果資源稀少,托管 Dispose 模式就非常有用。您不必將本機對象直接公開給托管代碼,而是至少為它們加上一層薄包裝,由該包裝實現 IDisposable 並沿用標准 Dispose 模式。這樣,如果您發現資源耗盡問題,可以顯式在托管代碼中處理這些對象,並在用完後迅速釋放資源。
經常影響應用程序的第二個生存期管理問題是開發人員總是感覺垃圾收集的作用不明顯:他們的內存使用持續上升,但某些原因使垃圾收集器運行得極不穩定,對象長時間占用資源。他們不得不反復調用 GC.Collect 來解決這一問題。
造成這一問題的主要原因是大量非常小的托管對象持續占用很大的本機數據結構。垃圾收集器本身進行自我調節,嘗試避免浪費時間進行不必要或無用處的收集。在決定是否進行另外一項收集時,它不僅查看進程的當前內存壓力,還會查看每項垃圾收集釋放的內存量。
如在此環境中運行,它看到的是每個收集只釋放了少量內存(記住,它只了解釋放的內存量),並未意識到釋放這些小對象可以極大地減輕總體壓力。這就導致內存使用持續提高,但收集反而越來越少。
解決方案是通知垃圾收集器每個此類小托管包裝的實際內存消耗高過了本機資源。為此,我們專門在 .NET Framework 2.0 中新增了一對 API。您可使用向稀有資源添加 Dispose 模式所用的包裝,但要將它們設定為向垃圾收集器提供提示,而不是必須由自己顯式釋放資源。
在此對象的構造函數中,您只需調用方法 GC.AddMemoryPressure 並傳入本機對象的本機內存的大約成本即可。然後在對象的終結器方法中調用 GC.RemoveMemoryPressure。這兩項調用將會幫助垃圾收集器理解這些對象的真實成本及釋放它們後能空出的實際內存。注意:必須要確保能出色平衡對 Add/RemoveMemoryPressure 的調用。
上述兩種環境中的第三個常見生存期管理問題與單個資源的管理沒有太多聯系,它涉及的是整個程序集或庫。本機庫在應用程序用過之後可以輕松卸載,但托管庫無法依靠自己卸載。CLR 有稱為 AppDomains 的隔離單元,可以單獨卸載並能在卸載時整理所有程序集、對象,甚至該域中運行的線程。如果您構建的是本機應用程序並習慣在處理完成後卸載加載項,您將發現分別對每個托管加載項使用不同的 AppDomains 後,您得到的靈活性不亞於卸載單個的本機庫。