Windows 執行階段和 CLR
深入探討 .NET 與 Windows 執行階段
Windows 運行庫 (WinRT) 提供新的 Api 一大套 Windows 體驗開發人員。 CLR 4.5,哪些船舶作為 Microsoft.NET 框架 4.5 Windows 8 的一部分允許開發人員編寫託管代碼以自然的方式,使用的 Api,就好像它們是另一個類庫。 您可以添加到 Windows 的中繼資料 (WinMD) 檔,用於定義您要調用,然後只是因為同一個標準的託管 API 呼叫到他們的 Api 的引用。 Visual Studio 會自動添加到內置的 Windows 使用者介面的新專案,以便您的應用程式可以簡單地開始使用這個新的 API WinRT Api 集的引用。
下罩,CLR 提供託管代碼來使用 WinMD 檔和託管的代碼與 Windows 運行庫之間的過渡的基礎設施。 在本文中,我將展示一些這些細節。 你會走的更好地瞭解您的託管的程式調用 WinRT API 時的後臺。
從託管代碼的消費 WinMD 檔
在使用 ECMA 335 所述的檔案格式進行編碼的 WinMD 檔中定義 WinRT Api (bit.ly/sLILI)。 雖然 WinMD 檔和.NET Framework 程式集共用一種常見的編碼,他們就是不一樣。 在中繼資料中的主要區別之一源自 WinRT 類型系統是獨立于.NET 類型系統的事實。
C# 編譯器和 Visual Studio 等程式使用 CLR 中繼資料 (如 IMetaDataImport) Api 讀取.NET Framework 組件中繼資料,現在可以從 WinMD 檔以及讀取中繼資料。 因為中繼資料不完全是.NET 程式集相同,CLR 中繼資料讀取器插入中繼資料 Api 和正在讀取的 WinMD 檔之間的配接器。 這使 WinMD 檔讀取好像他們是.NET 程式集 (請參見圖 1)。
圖 1,CLR 將插入一個 WinMD 檔和中繼資料的公共介面之間的元資料配接器
運行 ILDasm,可説明您瞭解 CLR 元資料配接器執行上的 WinMD 檔的修改。 預設情況下,ILDasm 以其原始形式,顯示 WinMD 檔的內容,因為它在磁片上的 CLR 元資料配接器沒有編碼。 但是,如果您通過 ILDasm /project 命令列參數,它使元資料配接器,和您可以看到作為 CLR 中繼資料和託管的工具會閱讀它。
通過運行 ILDasm 並排的副本 — — 一個具有 /project 參數,一個沒有 — — 您可以輕鬆地流覽 CLR 元資料配接器作到 WinMD 檔中的更改。
Windows 運行庫和.NET 類型系統
元資料配接器執行的主要業務之一是要合併的 WinRT 和.NET 類型系統。 在高級別上,WinRT 類型的五個不同類別可以在 WinMD 檔中出現和需要考慮由 CLR。 [圖 2] 列出這些必要的參考。 讓我們看看每個類別中更多詳細資訊。
圖 2 WinRT 類型的 WinMD 檔
分類 | 範例 |
標準 WinRT 類型 | Windows.Foundation.Collections.PropertySet、 Windows.Networking.Sockets.DatagramSocket |
基本型別 | Int32,位元組字串物件 |
投影的類型 | Windows.Foundation.Uri、 Windows.Foundation.DateTime |
預計的介面 | Windows.Foundation.Collections.IVector <T> Windows.Foundation.Iclosable |
與.NET 傭工的類型 | Windows.Storage.Streams.IInputStream、 Windows.Foundation.IasyncInfo |
標準 WinRT 類型雖然 CLR 有許多類別的公開的 Windows 運行時類型特別支援,絕大多數的 WinRT 類型不在所有治療專門由 CLR。 相反,這些類型顯示給.NET 開發人員不被修改,以及它們可用於作為一大類圖書館啟用寫入 Windows 存儲的應用程式。
基元類型此基元類型的一組編碼為使用相同的 ELEMENT_TYPE 枚舉,.NET 程式集使用 WinMD 檔。 CLR 自動解釋這些基元類型,好像他們是.NET 等效項。
大多數情況下,WinRT 基元類型視為.NET 基元類型的工作。 例如,一個 32 位整數起相同的位模式在 Windows 運行時一樣在.NET 中,所以 CLR,可以將一個 WinRT dword 值視為.NET System.Int32 沒有任何麻煩。 但兩個明顯的例外是字串和物件。
在 Windows 運行時,字串表示與 HSTRING 的資料類型不是.NET System.String 相同。 同樣,ELEMENT_TYPE_OBJECT 意味著 System.Object 到.NET,雖然它意味著 IInspectable * 到 Windows 運行時。 對於字串和物件,CLR 需要封送物件在運行時類型的 WinRT 和.NET 的表示形式之間進行轉換。 您將看到這封送處理本文中稍後介紹的工作方式。
預計類型 WinRT 類型系統中具有等效項有些現有基本.NET 類型。 例如,Windows 運行時定義 TimeSpan 結構和一個 Uri 類,這兩個.NET 框架中有相應的類型。
若要避免迫使.NET 開發人員來來回回這些基本資料類型之間進行轉換,CLR 專案為它的等效.NET 的 WinRT 版本。 這些預測都有效地合併,CLR.NET 和 WinRT 之間插入類型系統的點。
例如,Windows 運行庫銀Client.RetrieveFeedAsync API 接受 WinRT Uri 作為其參數。 而不需要手動創建一個新的 Windows.Foundation.Uri 實例傳遞給此 API 的.NET 開發,CLR 專案類型為 System.Uri,.NET 開發人員可以使用他們比較熟悉的類型。
投影的另一個例子是 Windows.Founda吸附。HResult 結構,預計由 CLR 為 System.Exception 的類型。 在.NET 中,開發人員習慣于看到錯誤資訊轉達了異常,而不是一個失敗 HRESULT,所以有如 IAsycn WinRT APIInfo.ErrorCode 表示錯誤的資訊,因為 HResult 結構不會感覺自然。 相反,CLR 專案 HResult 到異常,這使得如 IAsyncInfo.ErrorCode WinRT API 為.NET 開發人員更便於使用。 這裡是編碼的 Windows.winmd IAsyncInfo 錯誤代碼屬性的一個示例:
.class interface public windowsruntime IAsyncInfo
{
.method public abstract virtual
instance valuetype Windows.Foundation.HResult
get_ErrorCode()
}
CLR 投影后這裡是 IAsyncInfo 錯誤代碼屬性:
.class interface public windowsruntime IAsyncInfo
{
.method public abstract virtual
instance class [System.Runtime]System.Exception
get_ErrorCode()
}
預計介面 Windows 運行庫還提供了一組有.NET 等效項的基本介面。 CLR 執行類型預測這些介面以及,再次合併這些基本要點的類型系統。
預計介面的最常見的例子是 WinRT 集合介面如 IVector <T> IIterable <T> 和 IMap < K、 V >。 使用.NET 開發人員熟悉集合介面如 IList <T> <T> IEnumerable 和 IDictionary < K、 V >。 CLR 專案的 WinRT 收集到它們的等效.NET 的介面和也會隱藏 WinRT 介面,開發人員不必處理兩個等同集的類型的做同樣的事情。
除了預測這些類型,它們顯示為參數和返回類型的方法時,CLR 還必須專案這些介面,當它們在某個類型的介面執行清單中出現。 例如,WinRT PropertySet 類型實現 WinRT IMap < 字串、 物件 > 介面。 然而,CLR,將工程實現 IDictionary < 物件,字串 > 類型 PropertySet。 當執行此推算,PropertySet 成員,用於執行 IMap < 字串、 物件 > 是隱藏的。 相反,.NET 開發人員通過相應 IDictionary < 物件,字串 > 訪問 PropertySet 方法。 這裡是 PropertySet 局部視圖,如在 Windows.winmd 中的編碼:
.class public windowsruntime PropertySet
implements IPropertySet,
class IMap`2<string,object>,
class IIterable`1<class IKeyValuePair`2<string,object> >
{
.method public instance uint32 get_Size()
{
.override method instance uint32 class
IMap`2<string,object>::get_Size()
}
}
在這裡 CLR 投影后是 PropertySet 的局部視圖:
.class public windowsruntime PropertySet
implements IPropertySet,
class IDictionary`2<string,object>,
class IEnumerable`1<class KeyValuePair`2<string,object> >
{
.method private instance uint32 get_Size()
{
}
}
請注意三種類型的預測會發生:IMap < 字串、 物件 > IDictionary < 字串、 物件 >、 IKeyValuePair < 字串、 物件 > < 物件,字串 > KeyValuePair 和 IIterable <IKeyValuePair> 到 IEnumerable <KeyValuePair>。 此外請注意隱藏的 get_Size 方法從 IMap。
與.NET 框架傭工類型 Windows 執行時間有幾種類型沒有點到.NET 類型系統完全合併,但有足夠重要,.NET 框架提供了與他們一起工作的説明器方法的大多數應用程式的類型。 兩個最好的例子是 WinRT 流和非同步介面。
雖然 CLR 不專案 Windows.Storage.Streams.IRandomAccess流到 System.Stream,它不會提供一套的擴充方法的 IRandomAccessStream,它允許您的代碼以處理這些 WinRT 流,好像他們是.NET 流。 例如,您輕鬆閱讀與.NET StreamReader WinRT 流通過調用 OpenStreamForReadAsync 擴充方法。
Windows 運行庫提供一組表示非同步作業,如 IAsyncInfo 介面的介面。 在.NET 框架 4.5,有內置的等待非同步作業,哪個開發人員要在他們的.NET Api 相同的方式使用 WinRT Api 與支援。
要啟用此功能,.NET 框架附帶一套 WinRT 非同步介面的 GetAwaiter 擴充方法。 由 C# 和 Visual Basic 編譯器,這些方法用於啟用等待 WinRT 非同步作業。 以下為範例:
private async Task<string> ReadFilesync(StorageFolder parentFolder,
string fileName)
{
using (Stream stream = await parentFolder.OpenStreamForReadAsync(fileName))
using (StreamReader reader = new StreamReader(stream))
{
return await reader.ReadToEndAsync();
}
}
.NET 框架之間轉換和運行時 theWindows 的 CLR 提供了託管代碼能夠無縫地調用 WinRT 的 Api,以及 Windows 運行庫回檔到託管代碼的機制。
在最低的水準,Windows 運行時是構建 COM 概念,因此,毫不奇怪,在調用 WinRT Api 的 CLR 支援建立在現有 COM 交互操作基礎結構之上。
WinRT 交互操作和 COM 交互操作的一個重要區別是如何更少配置,您必須在 Windows 運行時處理。 WinMD 檔具有豐富描述所有的 Api,他們公開其明確定義映射到.NET 類型系統,所以沒有必要在託管代碼中使用任何 MarshalAs 屬性的中繼資料。 同樣,因為 Windows 8 附帶其 WinRT api 的 WinMD 檔,你不需要有與您的應用程式捆綁在一起的主交互操作程式集。 相反,CLR 使用框中的 WinMD 檔,找出需要知道有關如何調用 WinRT Api 的所有東西。
這些 WinMD 檔提供的託管的類型定義在運行時用來允許訪問 Windows 運行庫的託管的商。 雖然 CLR 從 WinMD 檔中讀取的 Api 包含的格式設置為從託管代碼很容易使用的方法定義,底層的 WinRT API 使用不同的 API 簽名 (有時稱為應用程式二進位介面或 ABI,簽名)。 API 和 ABI 簽名之間的一個例子是差異的,像標準 COM WinRT Api 返回 HRESULT,和 WinRT API 的傳回值是差異的實際上的 ABI 簽名中的輸出參數。 我將展示如何託管的方法簽名轉換成 WinRT ABI 簽名時我看看如何 CLR 調用 WinRT API 在本文後面的示例。
運行庫可調用包裝和 COM 可調用包裝
當一個 WinRT 物件進入 CLR 時,它需要被調用好象它是.NET 物件。 要實現這一點,CLR 中運行庫可調用包裝 (RCW) 包裝每個 WinRT 物件。 Rcw 的功能是什麼託管的代碼進行交互,並且是您的代碼,並使用您的代碼的 WinRT 物件之間的介面。
相反,當從 Windows 運行時使用託管的物件時,他們需要把它們當作 WinRT 物件調用。 在這種情況下,託管的物件封裝在一個 COM 可調用包裝 (CCW) 時它們被發送到 Windows 運行時。 因為 Windows 運行時作為 COM 使用相同的基礎結構,它可以與幼兒交互對託管物件的訪問功能 (請參閱圖 3)。
圖 3 為 Windows 運行庫、 託管的物件使用包裝
封送處理存根 (stub)
當託管的代碼轉換跨任何交互操作的邊界,包括 WinRT 的界限,必須發生幾件事情:
- 將託管輸入的參數轉換成 WinRT 等效項,包括建立幼兒託管物件。
- 找到實現從 RCW 上被呼叫者法的調用 WinRT 方法的介面。
- 調用 WinRT 方法。
- WinRT 輸出參數 (包括傳回值) 轉換為託管等效項。
- 從 WinRT API 的任何失敗 HRESULT 轉換託管異常。
這些操作將在封送處理的存根,CLR 將生成您的程式的名義。 對 RCW 的封送處理 stub 是轉換成 WinRT API 之前實際調用的託管的代碼。 同樣,Windows 運行時調用到生成 CLR 封送處理存根上時它可以轉換為託管代碼的 CCW。
封送處理存根 (stub) 提供的橋,橫跨 Windows 運行庫和.NET 框架之間的差距。 瞭解他們的工作將説明您深入瞭解到 Windows 運行庫調用您的程式時,會發生什麼。
示例調用
想像一個 WinRT API,採用字串清單並將他們,每個元素之間的分隔符號字串連接在一起。 此 API 可能有一個託管的簽名:
public string Join(IEnumerable<string> list, string separator)
CLR 已為中 ABI 定義,因此它需要找出方法的 ABI 簽名調用該方法。 值得慶倖的是,可以應用一組的確定性轉換要毫不含糊地給予 API 簽名 ABI 簽名。 第一個轉型是投影的資料類型替換對應的 WinRT,它返回 API 定義的表單的它是在 WinMD 檔中的元資料配接器載入它之前。 在此情況下,IEnumerable <T> 是實際上的 IIterable <T> 投影,所以此函數的 WinMD 視圖實際上是:
public string Join(IIterable<string> list, string separator)
WinRT 字串存儲在 HSTRING 的資料類型,所以到 Windows 運行時,此函數實際上看上去像中:
public HSTRING Join(IIterable<HSTRING> list, HSTRING separator)
在 ABI 層,在調用實際發生,WinRT Api 具有傳回值,HRESULT 和來自其簽名的傳回值是一個輸出參數。 此外,物件是指標,所以此方法的 ABI 簽名會:
HRESULT Join(__in IIterable<HSTRING>* list, HSTRING separator, __out HSTRING* retval)
所有 WinRT 方法必須都是介面的物件實現的一部分。 例如,我們 Join 方法,可能是 StringUtilities 類支援 IConcatenation 介面的一部分。 調用 Join 方法之前,CLR 必須掌握的 IConcatenation 介面指標上進行調用。
封送處理的存根 (stub) 的工作是從原始轉換託管 WinRT 介面上的最後 WinRT 調用 RCW 上調用。 在這種情況下,封送處理的存根 (stub) 偽代碼可能看上去像圖 4 (與清理調用為清晰起見省略)。
圖 4 為 Windows 運行庫調用從 CLR 封送處理存根的示例
public string Join(IEnumerable<string> list, string separator)
{
// Convert the managed parameters to WinRT types
CCW ccwList = GetCCW(list);
IIterable<HSTRING>* pCcwIterable = ccwList.QueryInterface(IID_IIterable_HSTRING);
HSTRING hstringSeparator = StringToHString(separator);
// The object that managed code calls is actually an RCW
RCW rcw = this;
// You need to find the WinRT interface pointer for IConcatenation
// implemented by the RCW in order to call its Join method
IConcatination* pConcat = null;
HRESULT hrQI = rcw.QueryInterface(IID_ IConcatenation, out pConcat);
if (FAILED(hrQI))
{
// Most frequently this is an InvalidCastException due to the WinRT
// object returning E_NOINTERFACE for the interface that contains
// the method you're trying to call
Exception qiError = GetExceptionForHR(hrQI);
throw qiError;
}
// Call the real Join method
HSTRING returnValue;
HRESULT hrCall = pConcat->Join(pCcwIterable, hstringSeparator, &returnValue);
// If the WinRT method fails, convert that failure to an exception
if (FAILED(hrCall))
{
Exception callError = GetExceptionForHR(hrCall);
throw callError;
}
// Convert the output parameters from WinRT types to .NET types
return HStringToString(returnValue);
}
在此示例中,第一步是將託管的參數從其託管表示形式轉換為其 WinRT 表示形式。 在這種情況下,代碼創建特定常規武器公約 》 為清單中的參數,並將 System.String 參數轉換為 HSTRING。
下一步是找到用品執行聯接的 WinRT 介面。 通過發出對包裝的調用的託管的代碼加入對 RCW 的 WinRT 物件的 QueryInterface 調用出現這種情況。 InvalidCastException 獲取引發 WinRT 的方法調用的最常見原因是如果這 QueryInterface 調用失敗。 發生這種情況的原因之一是 WinRT 物件不實現調用方預期到的所有介面。
現在真正的行動發生 — — 交互操作的存根 (stub) 對 WinRT Join 方法,提供了一個位置,以存儲邏輯實際調用傳回值 HSTRING。 如果 WinRT 方法失敗,它指示這與失敗 HRESULT,其中交互操作的存根 (stub) 轉換為異常並引發。 這意味著如果您的託管的代碼看到 WinRT 的方法調用引發異常,則很可能被調用 WinRT 方法返回失敗 HRESULT 和 CLR 引發了異常,以指示該故障狀態到您的代碼。
最後一步是將輸出參數從其 WinRT 表示形式轉換為其.NET 形式。 在此示例中,邏輯的傳回值是聯接調用的輸出參數,需要從 HSTRING 轉換為.NET 的字串。 然後可以作為存根 (stub) 的結果返回此值。
從 Windows 運行時到託管代碼中調用
來自 Windows 運行庫和目標的調用託管代碼工作以類似的方式。 CLR 回應的 QueryInterface 調用 Windows 運行時元件對它與具有虛函數表的填寫與交互操作的存根 (stub) 方法的介面。 這些存根來執行我展示以前,但相反方向的一個相同的功能。
讓我們再次考慮加入 API 的情況,除了這次假定它託管代碼實現的並從 Windows 運行時元件到調用。 存根 (stub),它允許發生此轉換的偽代碼可能看上去像圖 5。
對於 CLR 從 Windows 運行時調用的封送處理存根圖 5 示例
HRESULT Join(__in IIterable<HSTRING>* list,
HSTRING separator, __out HSTRING* retval)
{
*retval = null;
// The object that native code is calling is actually a CCW
CCW ccw = GetCCWFromComPointer(this);
// Convert the WinRT parameters to managed types
RCW rcwList = GetRCW(list);
IEnumerable<string> managedList = (IEnumerable<string>)rcwList;
string managedSeparator = HStringToString(separator);
string result;
try
{
// Call the managed Join implementation
result = ccw.Join(managedList, managedSeparator);
}
catch (Exception e)
{
// The managed implementation threw an exception -
// return that as a failure HRESULT
return GetHRForException(e);
}
// Convert the output value from a managed type to a WinRT type
*retval = StringToHSTring(result);
return S_OK;
}
第一,此代碼轉換的輸入的參數及其 WinRT 資料類型從託管類型。 假設輸入的清單一個 WinRT 物件,存根 (stub) 必須獲得 RCW 來表示該物件允許託管的代碼來使用它。 字串值只是從 HSTRING 轉換為 System.String。
下一步,將調用製成常規武器的聯接方法的託管實現 如果此方法將引發異常,交互操作的存根 (stub) 捕獲它,並將其轉換為一個失敗則返回到 WinRT 調用方的 HRESULT。 這就解釋了為什麼一些從託管代碼調用的 Windows 運行時元件引發的異常不崩潰進程。 如果 Windows 運行時元件處理失敗 HRESULT,這就是有效地捕捉和處理引發的異常相同。
最後一步是將輸出參數從其.NET 資料類型轉換為等效的 WinRT 資料類型,在這種情況下將 System.String 轉換為 HSTRING。 傳回值然後放入輸出參數和返回 HRESULT 成功。
預計的介面
我剛才提到,CLR 將成等效.NET 介面專案有些 WinRT 介面。 例如,IMap < K、 V > 預計到 IDictionary < K、 V >。 這意味著任何 WinRT 地圖是可訪問的作為.NET 字典,反之亦然。 要啟用這一預測工作,另一組的存根 (stub) 需要實現 WinRT 介面的.NET 介面,預計到,反之亦然。 例如,IDictionary < K、 V > TryGetValue 方法,但 IMap < K、 V > 不包含此方法。 若要允許託管的調用方使用 TryGetValue,CLR 提供存根 (stub) 實現此方法的 IMap 不會具有的方法。 這可能看起來類似于圖 6。
圖 6 的 IMap IDictionary 概念的情況
bool TryGetValue(K key, out V value)
{
// "this" is the IMap RCW
IMap<K,V> rcw = this;
// IMap provides a HasKey and Lookup function, so you can
// implement TryGetValue in terms of those functions
if (!rcw.HasKey(key))
return false;
value = rcw.Lookup(key);
return true;
}
注意,要工作,此轉換存根 (stub) 幾個調用基礎的 IMap 實現。 例如,假設您寫的以下位的託管代碼,以查看是否一個 Windows.Foundation.Collections.PropertySet 物件包含"NYJ"鍵:
object value;
if (propertySet.TryGetValue("NYJ", out value))
{
// ...
}
TryGetValue 呼叫確定是否設置的屬性包含鍵,呼叫堆疊可能看上去像圖 7。
圖 7 TryGetValue 調用的呼叫堆疊
堆疊 | 描述 |
PropertySet::HasKey | WinRT PropertySet 執行 |
HasKey_Stub | 封送處理成 WinRT 調用轉換詞典存根 (stub) HasKey 調用的存根 (stub) |
TryGetValue_Stub | 存根實施的 IMap IDictionary |
Application | 託管應用程式代碼調用 PropertySet.TryGetValue |
總結
對 Windows 運行庫的 CLR 支援允許託管開發人員調用它們可以調用託管的 Api 中標準的.NET 程式集定義一樣很容易在 WinMD 檔中定義的 WinRT Api。 在幕後,CLR 使用元資料配接器執行説明合併 WinRT 型系統的.NET 類型系統的預測。 它還使用一組交互操作存根來允許.NET 代碼調用 WinRT 方法,反之亦然。 綜上所述,這些技術使託管開發人員要從其 Windows 存儲應用程式調用 WinRT Api 可以輕鬆。
刷新 在 CLR 上工作了 10 年,目前正在開發主管負責 CLR Windows 運行時投影和.NET 交互操作。在 Microsoft.NET 框架 4.5 之前, 他工作的 CLR 安全模型。他的博客可以發現在 blogs.msdn.com/shawnfa。
由於下面的技術專家對本文的審閱:賴恩拜因頓、 Layla 德里和張義