Pull to refresh

Windows Runtime. Система типов и взаимодействие с CLR

Reading time 9 min
Views 16K
С выходом Windows 8 разработчикам стала доступна новая библиотека классов — Windows Runtime. Компоненты WinRT могут использоваться в приложениях Windows Store и настольных приложениях; в неуправляемом коде C/C++, в JavaScript, а также в C# и Visual Basic.

Метаданные Windows Runtime

На внутреннем уровне компоненты WinRT представляют собой компоненты COM (Component Object Model), для описания API которых теперь используются метаданные. Эти метаданные хранятся в файлах с расширением *.winmd и представляют собой обновленную версию метаданных .NET, которые кодируются в соответствие с правилами раздела №2 (Metadata Definition and Semantics) стандарта ECMA-335. Поскольку обычные сборки .NET Framework кодируются с помощью этого же стандарта, это говорит о том, что вы можете использовать знакомые средства (такие как ildasm.exe, Object Browser) для просмотра содержимого этих файлов.
По большей части, просмотр WinMD файла с помощью утилиты ildasm.exe очень похож на просмотр стандартной управляемой сборки. Есть несколько различий, которые могут быть видны — в первую очередь то, что WinMD файлы, в общем, не содержат никаких Intermediate Language (IL) инструкций. Вместо этого, эти файлы описывают API, предоставляемые Windows Runtime. Реализация этих интерфейсов может быть полностью отделена от их определения, и по сути, может быть записана в машинном коде. Тем не менее, для разработчиков управляемых приложений, детали реализации WinRT API не имеют значения, потому что управляемый код должен видеть только определения API, которые он вызывает. За кулисами, Common Language Runtime (CLR) и операционная система Windows соединяют для вас определения API и их реализации.

Например, в файле метаданных Windows.Foundation.winmd (находится в каталоге %WinDir%\System32\WinMetadata) вы можете обнаружить следующий тип Windows.Foundation.Collections.PropertySet, конструктор которого не содержит тела, потому что тип реализуется в native code.

image

Тем не менее, метаданные, которые описывают этот тип позволяют CLR получить экземпляр реализации при вызове конструктора класса.
При просмотре Windows Runtime метаданных можно также заметить, что определения типов и сборок используют новое ключевое слово WindowsRuntime.

image

Это ключевое слово является контекстно-зависимым и по разному интерпретируется в зависимости от того, где оно применяется. Например, если ключевым словом помечено определение типа (TypeDef), то этот тип подчиняется правилам системы типов Windows Runtime и вызов этого типа следует рассматривать как вызов WinRT API.

Взаимодействие CLR с компонентами WinRT

CRL поддерживает взаимодействие с COM-компонентами через обертки Runtime Callable Wrapper (RCW) и COM Callable Wrapper (CCW). Таким образом в CLR ссылка на WinRT объект представляет собой ссылку на RCW, которая в свою очередь содержит ссылку на WinRT объект. Соответственно управляемый код взаимодействует с RCW, который по сути является интерфейсом между вашим кодом и WinRT объектом.

image

Аналогичным образом в Windows Runtime ссылка на объект CLR представляет собой ссылку на CCW, которая в свою очередь содержит ссылку на CLR объект. Windows Runtime при этом взаимодействует с CCW для доступа к функциональности управляемого объекта.

WinRT типы и управляемый код

Несмотря на то, что система типов Windows Runtime схожа с системой типов CLR, при внимательном просмотре WinMD файлов можно заметить, что некоторые из типов, используемых в определениях API, не совпадают с типами, которые используются в управляемом коде. Для того, чтобы .NET разработчики могли создавать приложения с применением знакомых им технологий, CLR скрывает некоторые типы WinRT и предоставляет доступ к ним через другие. В общем, существует три категории типов в Windows Runtime и некоторые из них выглядят иначе в управляемом коде:
  • Основные типы данных, которые кодируются в метаданных с применением того же перечисления ELEMENT_TYPE что и в управляемых сборках. Исключением является тип SByte, который не поддерживается в WinRT.
  • Проецируемые типы (mapped types), которые кодируются в WinMD файлах как одни типы, но появляются в управляемом коде как их .NET эквиваленты. Например, когда CLR считывает в метаданных тип Windows.Foundation.Uri, вместо него она использует тип System.Uri. То есть CLR скрывает тип WinRT и предоставляет доступ к нему через другой тип. Кроме того CLR маршалирует тип между управляемым и неуправляемым кодом, что позволяет передавать объект System.Uri в Windows Runtime как Windows.Foundation.Uri. Полный список WinRT типов, которые CLR проецирует на типы FCL можно найти здесь.
  • Все остальные типы. Подавляющее большинство типов WinRT API предоставляются .NET разработчикам как есть. Например, если вы используете класс Windows.UI.Xaml.Controls.Button, CLR не предоставляет специальное представление или маршалинг это типа. В эту категорию также включаются типы, для которых среда CLR предоставляет методы расширения. Например, чтобы использовать объект, реализующий WinRT интерфейс Windows.Storage.IInputStream, с классом .NET Framework, которому необходим тип, производный от System.IO.Stream, следует задействовать методы расширения, которые определены в классе System.IO.WindowsRuntimeStreamExtensions сборки System.Runtime.WindowsRuntime.dll.

Интересно, что есть еще небольшая категория типов, которые появляются в несущественных местах в кодировках метаданных Windows. Это .NET Framework типы, которые используются просто для того, чтобы описать WinRT типы. Например, делегаты Windows Runtime кодируются с базовым типом System.MulticastDelegate, но это не означает, что все делегаты Windows Runtime получены из управляемого кода. Вместо этого базовый тип просто используется в качестве маркера метаданных, чтобы указать, что тип является типом делегата.

image

Проецирование типов

Когда среда CLR проецирует (отображает) WinRT типы, она делает две операции:
  • CLR определяет Windows Runtime тип как частный (private), а не общедоступный (public). Это препятствует тому, чтобы исходный тип был виден управляемому коду. Т.е. с точки зрения управляемого кода единственный тип, который существует — это .NET Framework тип, который является целью отображения.
  • Все ссылки на исходный тип преобразуются в ссылки на целевой .NET Framework тип.

Так, среда CLR представляет интерфейс IVector<T> как IList<T>, определение типа IVector<T> CLR рассматривает как определение типа с частной (private) областью видимости, а не с общедоступной (public). Аналогично тип Windows.UI.Xaml.Controls.UIElementCollection, который реализует IVector<UIElement> обновлен средой CLR. Реализация интерфейса IVector<T> перенаправляется на IList<T> так, что UIElementCollection в управляемом коде реализует интерфейс IList<UIElement>.
Обычно ildasm.exe показывает необработанное представление файла WinMD без включения любых перенаправлений. Чтобы просмотреть содержимое файла WinMD с включенными перенаправлениями, следует указать параметр /project. Эта опция позволяет увидеть, как CLR отображает метаданные на диске.

Базовый тип

Компоненты WinRT не имеют общего базового класса, однако все классы среды выполнения Windows должны реализовывать интерфейс IInspectable, который в свою очередь наследует от интерфейса IUnknown (что не удивительно). Однако, для .NET разработчиков все WinRT типы выглядят как типы производные от System.Object и соответственно наследуют такие методы как Equals, GetHashCode и т.д. Это становится возможным благодаря тому, что CLR осуществляет маршалинг объектов во время выполнения для преобразования типов между WinRT и .NET представлениями.

Структуры

Структуры WinRT, в отличие от значимых типов CLR, могут содержать только открытые поля одного из базовых типов или же являться другой структурой WinRT. Таким образом, следующий код выдаст ошибку на этапе компиляции:

public struct MyStruct
{
    // 'MyStruct' contains non-public field 'MyStruct.i'.
    // Windows Runtime structures can contain only public fields.
    private Int32 i;

    //Windows Runtime structures can contain only fields.
    public MyStruct(Int32 i)
    {
        this.i = i;
    }

    //Windows Runtime structures can contain only fields.
    public void MyFunc() { }
}

К тому же, структуры WinRT не могут определять конструкторы или содержать вспомогательные методы. Однако, некоторые структуры CLR, для удобства, проецирует на свои собственные, тем самым предоставляя разработчикам вспомогательные методы и конструкторы. К таким относятся, например, структура Windows.Foundation.Point, Windows.Foundation.Size и Windows.Foundation.Rect.

Строки

Тип System.String в WinRT представляется как HSTRING. Когда вы вызываете метод среды выполнения Windows, CLR преобразовывает любую .NET Framework строку в HSTRING перед вызовом метода. Аналогично, CLR преобразовывает любые строки, возвращенные из метода среды выполнения в тип System.String. Есть еще одна особенность — система типов WinRT не разрешает строкам принимать значение null. Вместо null для передачи пустой строки следует использовать String.Empty. При попытке передать null в качестве строки в WinRT функцию, CLR выдаст исключение ArgumentNullException. Точно так же вы никогда не увидите, что WinRT функция может вернуть null-строку, это может быть только пустая строка.

Null-совместимые типы

В WinRT API для определения null-совместимого значимого типа используется интерфейс Windows.Foundation.IReference<T>, который CLR проецирует на свой собственный System.Nullable<T>. Например, если метод в файле WinMD имеет следующую сигнатуру:

IReference<bool> Method(IReference<int> i);

то в управляемом коде этот метод будет выглядеть следующим образом:

Nullable<bool> Method(Nullable<int> i);


Делегаты

В качестве типа параметра или возвращаемого значения делегата WinRT могут использовать только WinRT-совместимые типы. Так же делегаты с глобальной (public) областью видимости не могут быть объявлены как вложенные (на самом деле это общие правила для среды выполнения Windows в целом). Когда вы передаете объект делегата компоненту Windows Runtime, этот объект упаковывается в обертку CCW, которая не уничтожается сборщиком мусора до тех пор, пока она не будет освобождена компонентом, который ее использует. Интересен так же тот факт, что делегаты WinRT не имеют методов BeginInvoke и EndInvoke.

События

Компоненты WinRT могут определять события, используя только типы делегатов WinRT. Существует тип делегата Windows.Foundation.EventHandler<T>, который CLR проецирует на тип делегата .NET Framework System.EventHandler<TEventArgs>. Когда вы определяете член-событие:

public event EventHandler<RoutedEventArgs> MyEvent;

то при компиляции этой строки кода, компилятор превращает ее в следующие инструкции:

private EventRegistrationTokenTable<EventHandler<RoutedEventArgs>> MyEvent;

public EventRegistrationToken add_MyEvent(EventHandler<RoutedEventArgs> handler)
{
    return EventRegistrationTokenTable<EventHandler<RoutedEventArgs>>
                .GetOrCreateEventRegistrationTokenTable(ref MyEvent)
                .AddEventHandler(handler);
}

public void remove_MyEvent(EventRegistrationToken token)
{
    EventRegistrationTokenTable<EventHandler<RoutedEventArgs>>
                .GetOrCreateEventRegistrationTokenTable(ref MyEvent)
                .RemoveEventHandler(token);
}

Как и прежде, компилятор создает закрытое поле и два метода-аксессора для регистрации и отказа от подписки на событие. Однако тип поля и содержание этих методов отличается от того, к чему мы привыкли (Delegate.Combine и Delegate.Remove). В качестве типа поля используется обобщенный класс EventRegistrationTokenTable<T>, аргументом типа которого является соответствующий тип делегата. Этот класс отвечает за хранения цепочки делегатов, которые представляют обработчики события. При добавлении нового обработчика, возвращается токен EventRegistrationToken, который может использоваться в дальнейшем для удаления обработчика события.

public void RaiseEvent()
{
    var list = EventRegistrationTokenTable<EventHandler<RoutedEventArgs>>
                .GetOrCreateEventRegistrationTokenTable(ref MyEvent)
                .InvocationList;
    if (list != null)
        list(this, new RoutedEventArgs());
}


public void Main()
{
    var myClass = new MyClass();
    var token = myClass.add_MyEvent(Handler);
    myClass.RaiseEvent();
    myClass.remove_MyEvent(token);
    myClass.RaiseEvent();
}

private void Handler(object sender, RoutedEventArgs args)
{
    Debug.WriteLine("event handling");
}

Для того, чтобы вызывать событие, следует воспользоваться свойством InvocationList, возвращающее делегат, список вызовов которого включает в себя все делегаты, добавленные в качестве обработчиков события.

Время и дата

В WinRT время и дата представляются в формате UTC структурой Windows.Foundation.DateTime. CLR проецирует данный тип на структуру System.DateTimeOffset, а не на System.DateTime. Стоит заметить, что DateTime не содержит информацию о часовом поясе. Поэтому дата и время, возвращаемые функциями WinRT в формате UTC, CLR преобразует в локальное время. И наоборот, при передаче структуры DateTimeOffset в WinRT функцию, дата и время преобразуются в UTC формат.

Массивы

WinRT API поддерживает только одномерные массивы. Соответственно следующий код вызовет ошибку времени компиляции:
// Arrays in Windows Runtime method signatures must be one dimensional.
public int[,] MyFunc()
{
    return new int[5, 5];
}

В управляемом коде массивы передаются по ссылке, при этом изменения элементов массива будут видны любому коду, который имеет ссылку на экземпляр этого массива. Однако, для WinRT это не всегда так, потому что содержимое массива маршалируется только в направлении, которое API определяет в своей сигнатуре, используя System.Runtime.InteropServices.InAttribute и System.Runtime.InteropServices.OutAttribute. Оба атрибута применяются к параметрам метода или возвращаемым значениям и определяют направление маршалинга между управляемой и неуправляемой памятью во время выполнения. В Windows Runtime параметры могут быть либо только для чтения [InAttribute], либо только для записи [OutAttribute] и не могут быть отмечены для чтения и записи одновременно [InAttribute], [OutAttribute]. Это означает, что содержимое массива, передаваемого методу, а также сам массив должны быть предназначены для чтения или для записи. Так, содержимое массива, который помечен атрибутом [InAttribute], копируется в вызываемый метод, поэтому все изменения, которые метод применяет к массиву, не видны вызывающему объекту. Аналогично, содержимое массива, который помечен атрибутом [OutAttribute], устанавливается вызываемым методом и копируется в вызывающий объект, поэтому вызываемый метод не должен делать каких-либо предположений о содержимом исходного массива.

Коллекции

При передачи коллекции CLR упаковывает объект коллекции в обертку CCW и передает ссылку на нее в WinRT API. При этом вызовы через обертку пересекают границу взаимодействия, что отрицательно сказывается на производительности. Однако, в отличие от массивов, возможно выполнение операций без копирования элементов.

Заключение

Подводя итоги, отмечу, что благодаря изменениям в CLR, разработчики управляемого кода могут легко адаптироваться к новому Windows Runtime API, используя знакомые им технологии. В данной статье я описал далеко не все подробности взаимодействия WinRT и CLR. Однако, это может послужить основой для дальнейшего изучения и более глубоко понимания Windows Runtime.
Tags:
Hubs:
+26
Comments 4
Comments Comments 4

Articles