Windows 运行时和 CLR

深入了解 .NET 和 Windows 运行时

Shawn Farkas

 

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)。

The CLR Inserts a Metadata Adapter Between WinMD Files and the Public Metadata Interface
图 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 文件

Category 示例
标准 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 API­Info.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,它不会提供一套的扩展方法的 IRandom­AccessStream,它允许您的代码以处理这些 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)。

Using Wrappers for Windows Runtime and Managed Objects
图 3 为 Windows 运行库、 托管的对象使用包装

封送处理存根 (stub)

当托管的代码转换跨任何互操作的边界,包括 WinRT 的界限,必须发生几件事情:

  1. 将托管输入的参数转换成 WinRT 等效项,包括建立幼儿托管对象。
  2. 找到实现从 RCW 上被调用方法的调用 WinRT 方法的接口。
  3. 调用 WinRT 方法。
  4. WinRT 输出参数 (包括返回值) 转换为托管等效项。
  5. 从 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 调用的调用堆栈

Stack 描述
PropertySet::HasKey WinRT PropertySet 执行
HasKey_Stub 封送处理成 WinRT 调用转换词典存根 (stub) HasKey 调用的存根 (stub)
TryGetValue_Stub 存根实施的 IMap IDictionary
应用程序 托管应用程序代码调用 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 德里和张义