共用方式為


Managed/Unmanaged 程式碼互通性概觀

 

Sonja Keserovic,Program Manager
David Mortenson,首席軟體設計工程師
Adam Nathan,測試中的首席軟體設計工程師 Adam Nathan

Microsoft Corporation

2003 年 10 月

適用於:
   Microsoft® .NET Framework
   COM Interop

總結: 本文提供有關 Managed 和 Unmanaged 程式碼之間互通性的基本事實,以及從 Managed 程式碼存取和包裝 Unmanaged API 的指導方針和常見作法,以及將 Managed API 公開給 Unmanaged 呼叫端。 也會強調開發程式的安全性和可靠性考慮、效能資料和一般作法。 (14 個列印頁面)

先決條件: 本檔的目標物件包括需要針對使用 Managed 程式碼位置做出高階決策的開發人員和管理員。 為了這樣做,瞭解 Managed 和 Unmanaged 程式碼之間的互動運作方式,以及目前指導方針如何套用至特定案例會很有説明。

目錄

互通性簡介
互通性指導方針
安全性
可靠性
效能
附錄 1:跨越互通性界限
附錄 2:資源
附錄 3:詞彙詞彙

互通性簡介

Common Language Runtime (CLR) 升級 Managed 程式碼與 COM 元件、COM+ 服務、Win32® API 和其他非受控程式碼類型的互動。 資料類型、錯誤處理機制、建立和解構規則,以及設計指導方針會因 Managed 和 Unmanaged 物件模型而異。 為了簡化 Managed 和 Unmanaged 程式碼之間的互通性,以及簡化移轉路徑,CLR Interop 層會隱藏來自用戶端和伺服器之這些物件模型之間的差異。

互通性 (「interop」) 是雙向的,這可讓您:

  • 從 Managed 程式碼呼叫 Unmanaged API

    這可以針對靜態 DLL 匯出 (一般 API 來完成,例如 WIN32 API,這些 API 會從 dll 公開,例如 kernel32.dll 和 user32.dll) 和 COM API (物件模型,例如 Microsoft® Word、Excel、Internet Explorer、ActiveX® Data Objects (ADO) 等) 。

  • 將受控 API 公開給 Unmanaged 程式碼

    這樣做的範例包括為 Windows Media® Player 之類的 COM 型應用程式建立增益集,或在 MFC 表單上內嵌 Managed Windows Forms 控制項。

三種互補技術可啟用這些受控/非受控互動:

  • 平台叫用 (有時稱為 P/Invoke) 只要在 Managed 原始程式碼中重新宣告其簽章,就可以在任何 Unmanaged 語言中呼叫任何函式。 這類似于 Visual Basic® 6.0 中 語句所提供的 Declare 功能。
  • COM Interop 可讓您以類似使用一般 Managed 元件的方式來呼叫 COM 元件,反之亦然。 COM Interop 是由 CLR 提供的核心服務所組成,再加上 System.Runtime.InteropServices 命名空間中的一些工具和 API。
  • C++ Interop (有時稱為 It Just Works (IJW) ) 是 C++特定的功能,可讓一般 API 和 COM API 直接使用,因為它們一律已使用。 這比 COM Interop 更強大,但需要更小心。 使用這項技術之前,請務必先檢查 C++ 資源。

互通性指導方針

從 Managed 程式碼呼叫 Unmanaged API

有數種類型的 Unmanaged API 和數種類型的 Interop 技術可供呼叫。 本節說明如何使用這些技術的建議和時機。 請注意,這些建議非常一般,而且不會涵蓋每個案例。 您應該仔細評估您的案例,並套用適合您案例的開發做法和/或解決方案。

呼叫 Unmanaged 一般 API

從 Managed 程式碼呼叫 Unmanaged 一般 API 有兩種機制:透過平台叫用 (適用于所有 Managed 語言,) 或 C++ interop (C++) 。

在您決定使用上述任一 Interop 技術呼叫一般 API 之前,您應該先判斷.NET Framework中是否有相等的功能。 建議您盡可能使用.NET Framework功能,而不是呼叫非受控 API。

針對只呼叫幾個 Unmanaged 方法或呼叫簡單的一般 API,建議使用平台叫用,而不是 C++ Interop。 撰寫簡單一般 API 的平台叫用宣告很簡單。 CLR 會負責 DLL 載入和所有參數封送處理。 即使撰寫一些適用于複雜一般 API 的平台叫用宣告工作,相較于使用 C++ Interop 的成本,並引進以 C++ 撰寫的全新模組,仍可忽略此工作。

若要包裝複雜的 Unmanaged 一般 API,或包裝在開發 Managed 程式碼時變更的 Unmanaged 一般 API,建議使用 C++ Interop 而不是平臺叫用。 C++ 層可能非常精簡,而且其餘的 Managed 程式碼都可以以任何其他受控語言撰寫。 在這些案例中使用平臺叫用需要大量心力,才能在 Managed 程式碼中重新宣告複雜的 API 部分,並將它們與 Unmanaged API 保持同步。 使用 C++ Interop 可透過允許直接存取 Unmanaged API 來解決此問題,這不需要重寫,只要包含標頭檔即可。

呼叫 COM API

有兩種方式可從 Managed 程式碼呼叫 COM 元件:透過 COM Interop (適用于所有 Managed 語言) 或 C++ Interop (C++) 。

若要呼叫與 OLE Automation 相容的 COM 元件,建議使用 COM Interop。 CLR 會負責 COM 元件啟用和參數封送處理。

若要根據介面定義語言 (IDL) 呼叫 COM 元件,建議使用 C++ Interop。 C++ 層可能非常精簡,而且其餘的 Managed 程式碼可以使用任何 Managed 語言撰寫。 COM Interop 依賴型別程式庫的資訊來進行正確的 Interop 呼叫,但類型程式庫通常不包含 IDL 檔案中的所有資訊。 使用 C++ Interop 可藉由允許直接存取這些 COM API 來解決此問題。

對於擁有已出貨 COM API 的公司,請務必考慮為這些 API (PIA) 出主要 Interop 元件,以便輕鬆取用受控用戶端。

呼叫 Unmanaged API 的決策樹

圖 1. 呼叫非受控 API 決策樹

將 Managed API 公開至 Unmanaged 程式碼

有兩種主要方式可將受控 API 公開給純非受控呼叫端:作為 COM API 或一般 API。 對於願意使用 Visual Studio® .NET 重新編譯其程式碼的 C++ Unmanaged 用戶端,有第三個選項:透過 C++ Interop 直接存取 Managed 功能。 本節說明如何使用這些選項的建議和時機。

直接存取受控 API

如果 Unmanaged 用戶端是以 C++ 撰寫,則可以使用 Visual Studio .NET C++ 編譯器編譯為「混合模式映射」。完成後,Unmanaged 用戶端可以直接存取任何受控 API。 不過,某些程式碼撰寫規則確實適用于從 Unmanaged 程式碼存取 Managed 物件;如需詳細資訊,請參閱 C++ 檔。

直接存取是慣用的選項,因為它不需要來自受控 API 開發人員的任何特殊考慮。 他們可以根據受控 API 設計指導方針來設計其受控 API (DG) ,並確信非受控呼叫者仍然可以存取 API。

將受控 API 公開為 COM API

每個公用 Managed 類別都可以透過 COM Interop 向 Unmanaged 用戶端公開。 此程式很容易實作,因為 COM Interop 層會負責處理所有 COM 管子。 因此,例如,每個 Managed 類別似乎都會實作 IUnknownIDispatchISupportErrorInfo,以及一些其他標準 COM 介面。

儘管將 Managed API 公開為 COM API 很簡單,但 Managed 和 COM 物件模型非常不同。 因此,將 Managed API 公開至 COM 應該一律是明確的設計決策。 受控世界中可用的某些功能在 COM 世界中沒有對等專案,而且無法從 COM 用戶端使用。 因此,受控 API 設計指導方針 (DG) 與 COM 的相容性之間通常會有差異。

如果 COM 用戶端很重要,請根據受控 API 設計指導方針撰寫受控 API,然後針對將公開給 COM 的受控 API 撰寫精簡 COM 易記的 Managed 包裝函式。

將受控 API 公開為一般 API

有時候非受控用戶端無法使用 COM。 例如,它們可能已經寫入使用一般 API,而且無法變更或重新編譯。 C++ 是唯一可讓您將受控 API 公開為一般 API 的高階語言。 這麼做並不簡單,就像將受控 API 公開為 COM API 一樣簡單。 這是一項非常進階的技術,需要 C++ Interop 的進階知識,以及 Managed 與 Unmanaged 世界之間的差異。

只有在絕對必要時,才會將您的受控 API 公開為一般 API。 如果您沒有任何選擇,請務必檢查 C++ 檔,並完全瞭解所有限制。

用於公開受控 API 的決策樹

圖 2. 公開受控 API 決策樹

安全性

Common Language Runtime 隨附安全性系統、 程式碼存取安全性 (CAS) ,其會根據元件來源的相關資訊來規範受保護資源的存取。 呼叫 Unmanaged 程式碼會產生重大安全性風險。 在沒有適當的安全性檢查的情況下,Unmanaged 程式碼可以操作 CLR 程式中任何受控應用程式的任何狀態。 您也可以直接呼叫 Unmanaged 程式碼中的資源,而不需要這些資源受到任何 CAS 許可權檢查。 基於這個理由,任何轉換到 Unmanaged 程式碼都會被視為高度保護的作業,而且應該包含安全性檢查。 此安全性檢查會尋找需要包含 Unmanaged 程式碼轉換之元件的 Unmanaged 程式碼許可權,以及呼叫它的所有元件,才能實際叫用 Unmanaged 程式碼。

有一些有限的 Interop 案例,其中不需要完整安全性檢查,而且不會過度限制元件的效能或範圍。 如果從 Unmanaged 程式碼公開的資源沒有安全性相關性, (系統時間、視窗座標等) ,或資源只會在元件內部使用,而且不會公開給任意呼叫端。 在這種情況下,您可以針對相關 API 的所有呼叫端隱藏 Unmanaged 程式碼許可權的完整安全性檢查。 做法是將 SuppressUnmanagedCodeSecurity 自訂屬性套用至個別 Interop 方法或類別。 請注意,這會假設您判斷沒有任何部分信任的程式碼可能會利用這類 API 的仔細安全性檢閱。

可靠性

Managed 程式碼的設計目的是比 Unmanaged 程式碼更可靠且強固。 提升這些品質的 CLR 功能的其中一個範例是垃圾收集,其負責釋放未使用的記憶體,以防止記憶體流失。 另一個範例是 Managed 型別安全,用來防止緩衝區溢位錯誤和其他類型相關錯誤。

當您使用任何類型的 Interop 技術時,您的程式碼可能不如純 Managed 程式碼一樣可靠或強固。 例如,您可能需要手動設定 Unmanaged 記憶體,並記得在完成時釋出記憶體。

撰寫任何非簡單 Interop 程式碼需要對撰寫 Unmanaged 程式碼的可靠性與健全性相同的注意。 即使所有 Interop 程式碼都正確撰寫,您的系統仍會和其非受控元件一樣可靠。

效能

每次從 Managed 程式碼轉換到 Unmanaged 程式碼 (,反之亦然) ,會有一些效能額外負荷。 額外負荷量取決於所使用的參數類型。 CLR Interop 層會根據轉換類型和參數類型使用三個層級的 Interop 呼叫優化:Just-In-Time (JIT) 內嵌、編譯的元件存根,以及解譯的封送處理存根 (,以最快到最慢的呼叫類型) 。

平台叫用呼叫的大約額外負荷:x86 處理器上的 10 部電腦指令 ()

COM Interop 呼叫的大約額外負荷:x86 處理器上的 50 部機器指令 ()

這些指示完成的工作會顯示在呼叫一般 API 的附錄章節:逐步呼叫和呼叫 COM API:逐步解說: 逐步解說。 除了確保垃圾收集行程不會在呼叫期間封鎖 Unmanaged 執行緒,以及處理呼叫慣例和 Unmanaged 例外狀況,COM Interop 還會執行額外的工作,將目前執行時間可呼叫包裝函式 (RCW) 轉換成適用于目前內容的 COM 介面指標。

每個 Interop 呼叫都會帶來一些額外負荷。 根據這些呼叫發生的頻率,以及方法實作內發生工作的重要性,每個呼叫的額外負荷範圍可能從可忽略到非常明顯。

根據這些考慮,下列清單提供一些您可能會發現有用的一般效能建議:

  • 如果您控制 Managed 和 Unmanaged 程式碼之間的介面,請將它設為「區塊」,而不是「聊天」,以減少所做的轉換總數。

    聊天介面是進行許多轉換的介面,不需要在 Interop 界限的另一端執行任何重大工作。 例如,屬性 setter 和 getter 是聊天的。 區塊介面是只會進行幾個轉換的介面,而且界限的另一端完成的工作量相當重要。 例如,開啟資料庫連接並擷取某些資料的方法是區塊式。 區塊化介面牽涉到較少的 Interop 轉換,因此您可以消除一些效能額外負荷。

  • 盡可能避免 Unicode/ANSI 轉換。

    將字串從 Unicode 轉換為 ANSI,反之亦然是昂貴的作業。 例如,如果需要傳遞字串,但其內容並不重要,您可以將字串參數宣告為 IntPtr ,Interop 封送處理器將不會執行任何轉換。

  • 針對高效能案例,將參數和欄位宣告為 IntPtr 可以提升效能,但代價是容易使用和維護。

    有時候,使用 封送處理 類別上可用的方法進行手動封送處理會更快,而不是依賴預設 Interop 封送處理。 例如,如果必須跨 Interop 界限傳遞大型字串陣列,但只需要幾個元素,請將陣列宣告為 IntPtr ,並手動只存取這些少數元素會更快。

  • 以明智方式使用 InAttributeOutAttribute 以減少不必要的封送處理。

    Interop 封送處理器會在決定呼叫之前是否需要封送處理特定參數,並在呼叫之後封送處理時使用預設規則。 這些規則是以間接取值層級和參數類型為基礎。 根據方法的語意,這些作業可能並非必要專案。

  • 只有在之後呼叫Marshal.GetLastWin32Error時,才在平臺上使用SetLastError=false叫用簽章。

    在平臺叫用簽章上設定 SetLastError=true 需要 Interop 層的額外工作才能保留最後一個錯誤碼。 只有在您依賴這項資訊,並在呼叫之後使用它時,才使用此功能。

  • 如果 且只有在非受控呼叫以不可惡意探索的方式公開時,才使用 SuppressUnmanagedCodeSecurityAttribute 來減少安全性檢查的數目。

    安全性檢查非常重要。 如果您的 API 未公開任何受保護的資源或敏感性資訊,或它們受到妥善保護,廣泛的安全性檢查可能會造成不必要的額外負荷。 不過,未執行任何安全性檢查的成本很高。

附錄 1:跨越互通性界限

呼叫一般 API:逐步執行

圖 3. 呼叫一般 API

  1. 取得 LoadLibraryGetProcAddress
  2. 從包含目標位址的簽章建置 DllImport 存根。
  3. 推播被呼叫端已儲存的暫存器。
  4. 設定 DllImport 框架,並將其推送至框架堆疊。
  5. 如果已配置暫存記憶體,請初始化清除清單,以在呼叫完成時快速釋放。
  6. 封送處理參數。 (這可能會配置 memory.)
  7. 將垃圾收集模式從合作式變更為先占式,因此可以隨時發生垃圾收集。
  8. 載入目標位址並加以呼叫。
  9. 如果 已設定 SetLastError 位,請呼叫 GetLastError 並將結果儲存線上程本機儲存體中儲存的執行緒抽象。
  10. 切換回合作式垃圾收集模式。
  11. 如果 PreserveSig=false 且方法傳回失敗 HRESULT,則擲回例外狀況。
  12. 如果未擲回例外狀況,則回傳 by-ref 參數。
  13. 將擴充堆疊指標還原為其原始值,以考慮呼叫端快顯引數。

呼叫 COM API:逐步執行

圖 4. 呼叫 COM API

  1. 從簽章建置 Managed 到 Unmanaged 存根。
  2. 推播被呼叫端已儲存的暫存器。
  3. 設定 Managed 到 Unmanaged COM Interop 框架,並將其推送至框架堆疊。
  4. 保留轉換期間所使用之暫存資料的空間。
  5. 如果已配置暫存記憶體,請初始化清除清單,以在呼叫完成時快速釋放。
  6. 僅) (x86 清除浮點例外狀況旗標。
  7. 封送處理參數。 (這可能會配置 memory.)
  8. 擷取執行時間可呼叫包裝函式內目前內容的正確介面指標。 如果無法使用快取的指標,請在 COM 元件上呼叫 QueryInterface 以取得它。
  9. 將垃圾收集模式從合作式變更為先占式,因此可以隨時發生垃圾收集。
  10. 從 vtable 指標,依位置編號編制索引、取得目標位址,然後呼叫它。
  11. 如果先前呼叫QueryInterface,請在介面指標上呼叫Release
  12. 切換回合作式垃圾收集模式。
  13. 如果簽章未標示 為 PreserveSig,請檢查失敗 HRESULT 並擲回例外狀況, (可能填入 IErrorInfo 資訊) 。
  14. 如果未擲回例外狀況,則回傳 by-ref 參數。
  15. 將擴充堆疊指標還原至原始值,以考慮呼叫端快顯引數。

從 COM 呼叫受控 API:逐步執行

圖 5. 從 COM 呼叫受控 API

  1. 從簽章建置 Unmanaged-to-managed 存根。
  2. 推播被呼叫端已儲存的暫存器。
  3. 設定 Unmanaged 到 Managed COM Interop 框架,並將其推送至畫面堆疊。
  4. 保留轉換期間所使用之暫存資料的空間。
  5. 將垃圾收集模式從合作式變更為先占式,以便隨時發生垃圾收集。
  6. 從介面指標擷取 COM 可呼叫包裝函式 (CCW) 。
  7. 擷取 CCW 內的 Managed 物件。
  8. 視需要轉換 appdomains。
  9. 如果 appdomain 不完全信任,請針對目標 appdomain 執行方法可能具有的任何連結要求。
  10. 如果已配置暫存記憶體,請初始化清除清單,以在呼叫完成時快速釋放。
  11. 封送處理參數。 (這可能會配置 memory.)
  12. 尋找要呼叫的目標 Managed 方法。 (這牽涉到將介面呼叫對應至目標實作。)
  13. 快取傳回值。 (如果它是浮點傳回值,請從浮點 register.)
  14. 變更回合作式垃圾收集模式。
  15. 如果擲回例外狀況,請擷取其 HRESULT 以傳回,並呼叫 SetErrorInfo
  16. 如果未擲回任何例外狀況,請回傳 by-ref 參數。
  17. 將擴充堆疊指標還原至原始值,以考慮呼叫端快顯引數。

附錄 2:資源

必須讀取!.NET 和 COM:Adam Nathan 的完整互通性指南

與 Unmanaged 程式碼交互操作,Microsoft .NET Framework開發人員指南

Interop 範例、Microsoft .NET Framework

Adam Nathan 的 部落格

Chris Brumme 的 部落格

附錄 3:詞彙詞彙

AppDomain (應用程式域) 應用程式域可以視為類似輕量型作業系統進程,並由 Common Language Runtime 管理。
CCW (COM 可呼叫包裝函式) CLR Interop 層針對從 COM 程式碼啟動的 Managed 物件所建立的特殊包裝函式。 CCW 藉由提供資料封送處理、存留期管理、身分識別管理、錯誤處理、更正 Apartment 和執行緒轉換等方式,隱藏 Managed 和 COM 物件模型之間的差異。 CCW 會以 COM 易記的方式公開 Managed 物件功能,而不需要 Managed 程式碼實作器知道有關 COM 管線的任何專案。
CLR Common Language Runtime。
COM Interop CLR Interop 層所提供的服務,可用來從 Managed 程式碼使用 COM API,或將受控 API 公開為 UNmanaged 用戶端。 COM Interop 適用于所有 Managed 語言。
C++ Interop C++ 語言編譯器和 CLR 所提供的服務,用於在相同的可執行檔中直接混合 Managed 和 Unmanaged 程式碼。 C++ Interop 通常涉及在 Unmanaged API 中包含標頭檔,並遵循特定程式碼撰寫規則。
複雜的一般 API 具有難以以 Managed 語言宣告之簽章的 API。 例如,具有可變大小結構參數的方法很難宣告,因為 Managed 類型系統中沒有相等的概念。
Interop 涵蓋 Managed 與 Unmanaged (之間任何類型的互通性的一般詞彙,也稱為「原生」) 程式碼。 Interop 是 CLR 所提供的許多服務之一。
Interop 元件 特殊型別的 Managed 元件,其中包含類型程式庫中所包含 COM 類型對等的 Managed 型別。 通常由在型別程式庫上執行型別程式庫匯入工具 (Tlbimp.exe) 來產生。
Managed 程式碼 CLR 控制項下執行的程式碼稱為 Managed 程式碼。 例如,以 C# 或 Visual Basic .NET 撰寫的任何程式碼都是 Managed 程式碼。
平台叫用 CLR Interop 層所提供的服務,可從 Managed 程式碼呼叫 Unmanaged 一般 API。 平台叫用適用于所有 Managed 語言。
RCW (執行時間可呼叫 wapper) CLR Interop 層針對從 Managed 程式碼啟動的 COM 物件所建立的特殊包裝函式。 RCW 藉由提供資料封送處理、存留期管理、身分識別管理、錯誤處理、更正 Apartment 和執行緒轉換等方式,隱藏 Managed 和 COM 物件模型之間的差異。
Unmanaged 程式碼 在 CLR 外部執行的程式碼稱為「Unmanaged 程式碼」。COM 元件、ActiveX 元件和 WIN32 API 函式是 Unmanaged 程式碼的範例。