.NET and Metro: The Windows Runtime and the CLR on Windows 8
Many developers tend to look at Windows 8 as a completely new platform and even question whether it heralded the imminent demise of managed code. After spending many months digging into the Windows Runtime (WinRT), Metro style or “tailored” applications, and exploring how they related to the .NET Framework, I’ve come to the conclusion that the two work very closely together and in fact are engineered to integrate. That means good news for managed developers. The .NET Framework 4.5 is very much “Metro-aware” while the Windows Runtime knows how to shake hands with the CLR. The purpose of this post is to elaborate on this a bit more than what I covered in my Portable Class Library series.
The first thing to note is that WinRT components, regardless of what language they were written in, share the same metadata format as .NET Framework assemblies. This is what allows you to navigate to the metadata folder at c:\windows\system32\WinMetadata and be able to use ildasm.exe to expose the definitions of WinRT components that are part of the Windows 8 operating system. The standard is documented as ECMA-335 and you can check this link to learn more.
As part of this metadata, types and assemblies can be tagged with a new Intermediate Language (IL) keyword called windowsruntime. This keyword tags the type as a WinRT component. The .NET Framework uses this to know when to marshal parameters because the WinRT type system is not the same as the one supported by the CLR. Fortunately, this is supported “under the covers” as you’ll see in the next section. When an assembly is tagged with the keyword, it instructs the .NET Framework to use a special APIs like the one called RoResolveNamespace to load references. This is a way that all supported WinRT languages can access underlying components.
As I mentioned, the .NET Framework does some magic with type conversion. This happens in both directions (calls into and values received from WinRT components). This is not new – this has been supported for awhile and you can see a list of Default Marshaling for Value Types in the MSDN documentation. A System.Int32 becomes a ELEMENT_TYPE_I4 and vice versa. These are what are referred to as “fundamental types.”
In addition, however, the CLR performs implicit mapping to more complex types. If you’ve been doing any Windows 8 development, you are probably familiar with conversions that look like this:
Windows.Foundation.Collections.IVector<T> <= => System.Collections.Generic.IList<T>
You can read the list of WinRT collection types, then drill into the detail to see the corresponding .NET map.
Here is a list of commonly mapped types:
|Windows Runtime||.NET Framework|
|IMap<K, V>||IDictionary<TKey, TValue>|
|IMapView<K, V>||IReadOnlyDictionary<TKey, TValue>|
There are other mapped types that you may not be as familiar with, such as the map from a Windows.Foundation.Uri to a System.Uri.While this is done “automatically” it does have side effects. The WinRT Uri does not support anything but absolute Uris, so any attempt to pass a relative Uri into a WinRT component will fail.
Any other types are projected automatically without conversion. A good example of this is the built-in XAML controls for building Metro applications. Windows.UI.Xaml.Controls.Button appears as a CLR class to your .NET code, but it also appears as a C++ class to developers using the C++ option. There is no map or conversion, the type remains the same but the projection provides all of the plumbing necessary for it to act and feel like a type native to the environment you are working in. Any necessary wrappers are created behind the scenes – you’ll learn more about that later.
Because special types can have side effects, it’s important to know what they are and what restrictions exist. Here is a short list:
- Array – If you still use this construct for collections it’s important to note that WinRT does not support the class being passed by reference as in the CLR. Arrays can either be provided as input parameters to a WinRT component, where they can be referenced but not modified, or as output parameters, where what you pass in is really just regarded as a buffer and not inspected but populated by the component.
- DateTimeOffset – in the Windows Runtime, this becomes a Windows.Foundation.DateTime value which does not contain time zone information. The CLR will convert your time to UTC before passing it to the WinRT component, and will convert any result from UTC to local time. To resolve this, you should be prepared to convert the value to local time before passing it to a WinRT component and back from local time to the target time zone when you receive values back.
- String – this becomes a HSTRING which does not support null values. You should not pass a null string into a WinRT component, nor should you ever see a null value returned back. Instead, use String.Empty.
- Uri – this is converted to Windows.Foundation.Uri which only accepts absolute Uris. Attempting to pass a relative Uri will result in an exception.
In addition, it’s important to note some WinRT types and how they impact the CLR. Understanding these types is more important if you plan to write WinRT components in C++ for consumption by the .NET Framework – otherwise most of this happens behind the scenes for you.
- ELEMENT_TYPE_OBJECT – on the surface, this is mapped as System.Object in the .NET Framework. It is used to pass back other types from APIs. What’s important to note is how it is mapped on the .NET side. This type represents a pointer to the COM IInspectable interface. It is responsible for projecting WinRT components into all supported languages and all WinRT interface types must derive from it. One important method that it declares is GetRuntimeClassName. If this passes back a value, the CLR will create a strongly-typed Runtime Callable Wrapper (RCW) for the projection. Otherwise, it will create a weakly-typed wrapper that simply resolves to System.__ComObject regardless of the actual backing type.
- IReference<T> – this is an interesting type because it handles two features in the Windows Runtime. First, it is a way of providing a nullable type. While it maps loosely to Nullable<T> the mechanism is slightly different. The pointer itself will be null if the value passed is null, otherwise it will point to an implementation that returns the actual value when the get_Value method is called. Conversely, a null pointer results in a Nullable<T> instance created with HasValue set to false. The type also facilitates boxing. Instead of returning a direct value, a WinRT component can return an ELEMENT_TYPE_OBJECT (System.Object to the CLR) and implement IInspectable to return IReference<Int32> as the type. When that happens, the CLR will call the method to obtain the value, then box it on the managed heap and pass the reference back into the managed stack.
In the CLR, managed events can have multiple delegates assigned and removed. Removing an event is as simply as using the operator to remove it and passing the delegate that handles the event callbacks. In WinRT, the event system is token-based. When you add a delegate to handle an event, you receive a unique token. To remove your delegate from the event, you call a method to remove and pass in the token, not the delegate.
Events are mapped between the CLR and WinRT by the compiler. If you decompile your code you will see this in action. The CLR uses the helper classes:
The compiler will emit code that maps the events through these classes. The classes will maintain the dictionary of tokens and delegates, so you can code your familiar remove operation and have it automatically pass the correct token behind the scenes.
For the most part, you don’t have to worry about any of the magic happening behind the scenes because it is done for you. However, the purpose of this was to show how much thought and engineering went into actually working with both .NET and WinRT to ensure the two could work closely together. This to me is evidence that .NET is not a second-rate citizen in Windows 8, but an important factor that was carefully considered as part of the overall strategy to create a flexible new platform with reach.