Сентябрь 2015

ТОМ 30, НОМЕР 9

Windows и C++ - Подобные классам типы в Windows Runtime

Кенни Керр | Сентябрь 2015

Кенни КеррWindows Runtime (WinRT) позволяет разработчикам компонентов предоставлять разработчикам приложений систему подобных классам типов (classy type system). Учитывая, что Windows Runtime полностью реализована на COM-интерфейсах, это может показаться весьма абсурдным любому, кто знает C++ и классическую COM. В конце концов, COM — модель программирования, ориентированная на интерфейсы, а не на классы. Модель активации COM дает возможность конструировать классы с помощью CoCreateInstance и ей подобных, но они нормально поддерживают лишь нечто вроде конструктора по умолчанию. И WinRT RoActivateInstance ничего не меняет в таком восприятии. Как же это работает?

На самом деле это не столь несочетаемо, как может показаться на первый взгляд. Модель активации COM уже предоставляет фундаментальные абстракции для поддержки системы подобных классам типов. В ней просто отсутствуют метаданные, необходимые для ее описания потребителям. В модели активации COM определяются объекты и экземпляры классов. Экземпляр какого-либо класса никогда не создается потребителем напрямую. Даже если разработчик приложения вызывает CoCreateInstance, чтобы создать экземпляр некоего класса, ОС все равно должна получить объект класса, чтобы извлечь нужный экземпляр.

В Windows Runtime объекты классов названы по-новому, но механика все та же. Фабрика активации является для потребителя первым портом вызова компонента. Вместо реализации DllGetClassObject компонент экспортирует функцию DllGetActivationFactory. Способ идентификации классов изменился, но результат — просто объект, который можно дополнительно запрашивать в связи с данным классом. Традиционный объект класса, в дальнейшем именуемый фабрикой активации, мог бы реализовать интерфейс IClassFactory, позволяющий вызвавшему коду создавать экземпляры данного класса. В новом мире Windows Runtime фабрики активации должны реализовать новый интерфейс IActivationFactory, который в конечном счете обеспечивает тот же сервис.

По результатам моих исследований в последних двух выпусках этой рубрики, класс периода выполнения (runtime class) в Windows Runtime, дополненный атрибутом activatable, генерирует метаданные, которые инструктируют языковую проекцию разрешать конструирование по умолчанию. Допущение заключается в том, что фабрика активации для класса с атрибутом обеспечит такой конструируемый по умолчанию объект через метод ActivateInstance интерфейса IActivationFactory. Вот простой runtimeclass в IDL, представляющий класс с конструктором по умолчанию:

[version(1)]
[activatable(1)]
runtimeclass Hen
{
  [default] interface IHen;
}

Дополнительные конструкторы тоже можно описать в IDL с помощью, как вы догадались, интерфейсов, содержащих наборы методов, представляющих эти конструкторы с разными наборами параметров. По аналогии с тем, как интерфейс IActivationFactory определяет конструирование по умолчанию, интерфейсы, специфичные для компонента и класса, могут описывать параметризованное конструирование. Вот простой интерфейс фабрики для создания объектов Hen с учетом заданного количества clucks:

runtimeclass Hen;

[version(1)]
[uuid(4fa3a693-6284-4359-802c-5c05afa6e65d)]
interface IHenFactory : IInspectable
{
  HRESULT CreateHenWithClucks([in] int clucks,
                              [out,retval] Hen ** hen);
}

Как и IActivationFactory, интерфейс IHenFactory по какой-то неясной причине должен наследовать напрямую от IInspectable. Каждый из методов этого интерфейса фабрики затем проецируется как дополнительный конструктор с заданным числом параметров, и каждый должен возвращать логическое значение с типом, соответствующим этому классу. Потом этот интерфейс фабрики явным образом сопоставляется с runtimeclass, используя другой атрибут activatable, который указывает имя интерфейса:

[version(1)]
[activatable(1)]
[activatable(IHenFactory, 1)]
runtimeclass Hen
{
  [default] interface IHen;
}

Если конструктор по умолчанию не имеет смысла для вашего класса, тогда просто опустите исходный атрибут activatable и в языковых проекциях вашего класса этого конструктора не будет. А как быть, если я поставляю какую-то версию своего «птицеводческого» компонента, а затем осознаю, что ему не хватает возможности создавать куриц с высокими гребешками (combs)? Теперь, когда я поставил этот компонент своим пользователям, я уже не могу изменить интерфейс IHenFactory, поскольку COM-интерфейсы должны быть неизменяемыми на двоичном и семантическом уровнях. Но я могу без проблем определить другой интерфейс, содержащий любые дополнительные конструкторы:

[version(2)]
[uuid(9fc40b45-784b-4961-bc6b-0f5802a4a86d)]
interface IHenFactory2 : IInspectable
{
  HRESULT CreateHenWithLargeComb([in] float width,
                                 [in] float height,
                                 [out, retval] Hen ** hen);
}

После этого можно сопоставить второй интерфейс фабрики с классом Hen, как и раньше:

[version(1)]
[activatable(1)]
[activatable(IHenFactory, 1)]
[activatable(IHenFactory2, 2)]
runtimeclass Hen
{
  [default] interface IHen;
}

Языковые проекции берут на себя объединение интерфейсов, и разработчик приложения просто увидит ряд перегрузок конструктора, как показано на рис. 1.

Так в C# показываются методы фабрики, определенные в IDL
Рис. 1. Так в C# показываются методы фабрики, определенные в IDL

Разработчик компонента реализует интерфейсы этих фабрик наряду с обязательным интерфейсом IActivationFactory:

struct HenFactory : Implements<IActivationFactory,
                               ABI::Sample::IHenFactory,
                               ABI::Sample::IHenFactory2>
{
};

Здесь я вновь полагаюсь на шаблон класса Implements, который был описан в моей рубрике за декабрь 2014 года (msdn.microsoft.com/magazine/dn879357). Даже если бы я выбрал запрет конструирования по умолчанию, фабрика активации hen все равно должна была бы реализовать интерфейс IActivationFactory, так как функция DllGetActivationFactory, которую обязаны реализовать компоненты, «жестко запрограммирована» на возврат интерфейса IActivationFactory. Это означает, что языковая проекция должна сначала вызвать QueryInterface по полученному указателю, если приложению требуется что-то, отличное от конструирования по умолчанию. Тем не менее, реализация виртуальной функции ActivateInstance интерфейса IActivationFactory обязательна, но холостой команды (no-op) вроде показанной ниже, будет достаточно:

virtual HRESULT __stdcall ActivateInstance(
  IInspectable ** instance) noexcept override
{
  *instance = nullptr;
  return E_NOTIMPL;
}

Дополнительные методы конструирования можно реализовать любым способом, имеющим смысл для данной реализации, но самым простым решением была бы пересылка аргументов самому внутреннему классу — примерно так:

virtual HRESULT __stdcall CreateHenWithClucks(int clucks,
  ABI::Sample::IHen ** hen) noexcept override
{
  *hen = new (std::nothrow) Hen(clucks);
  return *hen ? S_OK : E_OUTOFMEMORY;
}

Это предполагает, что у C++-класса Hen нет конструктора, генерирующего исключения (throwing constructor). Мне может понадобиться создание C++-вектора clucks в этом конструкторе. В таком случае будет достаточно простого обработчика исключений:

try
{
  *hen = new Hen(clucks);
  return S_OK;
}
catch (std::bad_alloc const &)
{
  *hen = nullptr;
  return E_OUTOFMEMORY;
}

Как насчет статических членов класса? Вы не должны удивляться тому, что и это реализуется с помощью COM-интерфейсов в фабрике активации. Вернувшись в IDL, можно определить интерфейс, который будет содержать все статические члены. Посмотрим для примера, как сообщить о количестве куриц-несушек (layer hens) на заднем дворе:

[version(1)]
[uuid(60086441-fcbb-4c42-b775-88832cb19954)]
interface IHenStatics : IInspectable
{
  [propget] HRESULT Layers([out, retval] int * count);
}

Затем я должен сопоставить этот интерфейс со своим классом периода выполнения (runtime class), указав атрибут static:

[version(1)]
[activatable(1)]
[activatable(IHenFactory, 1)]
[activatable(IHenFactory2, 2)]
[static(IHenStatics, 1)]
runtimeclass Hen
{
  [default] interface IHen;
}

Реализация на C++ столь же прямолинейна. Я обновлю вариативный шаблон Implements следующим образом:

struct HenFactory : Implements<IActivationFactory,
                               ABI::Sample::IHenFactory,
                               ABI::Sample::IHenFactory2,
                               ABI::Sample::IHenStatics>
{
};

И предоставлю подходящую реализацию виртуальной функции get_Layers:

virtual HRESULT __stdcall get_Layers(
  int * count) noexcept override
{
  *count = 123;
  return S_OK;
}

Более точный подсчет голов, то бишь куриц, я оставлю вам в качестве упражнения.

На стороне потребителя, внутри приложения все выглядит очень просто и ясно. Как было показано на рис. 1, IntelliSense оказывает существенную помощь. Я могу использовать конструктор CreateHenWithClucks так, будто это еще один конструктор в C#-классе:

Sample.Hen hen = new Sample.Hen(123); // наседки (clucks)

Конечно, компилятору C# и Common Language Runtime (CLR) придется проделать немалую работу, чтобы это запустить. В период выполнения CLR вызывает RoGetActivationFactory, та — LoadLibrary, а она в свою очередь вызывает DllGetActivationFactory, чтобы получить фабрику активации Sample.Hen. Затем вызывается QueryInterface, чтобы получить реализацию интерфейса IHenFactory фабрики, и лишь после этого можно вызвать виртуальную функцию CreateHenWithClucks для создания объекта Hen. Я уж не говорю о множестве дополнительных вызовов QueryInterface, необходимых CLR по мере того, как она создает каждый объект; это требуется CLR для обнаружения различных атрибутов и семантики данных объектов.

Вызов статического члена тоже выглядит простым:

int layers = Sample.Hen.Layers;

Но и в этом случае CLR вновь должна вызвать RoGetActivationFactory для получения фабрики активации, потом — QueryInterface для получения указателя на интерфейс IHenStatics. Только после этого она может вызвать виртуальную функцию get_Layers, чтобы получить значение этого статического свойства. Однако CLR кеширует фабрики активации, поэтому издержки в какой-то мере амортизируются при множестве таких вызовов. С другой стороны, это также делает бессмысленными и неоправданно сложными различные попытки кеширования фабрик внутри компонента. Но эту тему мы оставим на потом. Присоединяйтесь ко мне в следующем месяце, и мы вместе продолжим исследовать Windows Runtime из C++.


Кенни Керр (Kenny Kerr) — высококвалифицированный программист. Живет в Канаде. Автор учебных курсов для Pluralsight, обладатель звания Microsoft MVP. Ведет блог kennykerr.ca. Кроме того, читайте его заметки в twitter.com/kennykerr.


Выражаю благодарность за рецензирование статьи эксперту Microsoft Ларри Остерману (Larry Osterman).