本文章是由機器翻譯。

CLR 全面透徹解析

託管和本機代碼互通性最佳實踐

Jesse Kaplan

目錄

託管和本機代碼交互操作適用哪種場合?
交互操作技術:三種選擇
交互操作技術:P/Invoke
交互操作技術:COM Interop
交互操作技術:C++/CLI
交互操作體系結構注意事項
API 設計和開發人員體驗
交互操作邊界的性能和位置
生存期管理

在 2009 年伊始的《MSDN 雜誌》中看到這樣一個專欄多少有些出人意料——自 2002 年的 1.0 版起,Microsoft .NET Framework 中已經支援託管代碼或本機代碼互通性,格式都大同小異。 此外,詳細的 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 創建簽名。 隨附的截圖顯示了處於使用狀態的工具。

fig01.gif

在 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 後,您得到的靈活性不亞于卸載單個的本機庫。

請將您想詢問的問題和提出的意見發送至clrinout@microsoft.com

Jesse Kaplan 現在是 Microsoft CLR 團隊負責託管/本機互通性的專案經理。 他以前負責的工作包括相容性和擴展性。