Поделиться через



Август 2015

Том 30 выпуск 8

Windows и C++ - Компоненты Windows Runtime с MIDL

Кенни Керр

Kenny KerrВ прошлом выпуске этой рубрики (msdn.microsoft.com/magazine/mt238401), я рассказал о концепции компонентов Windows Runtime (WinRT) как продукта эволюции парадигмы программирования с применением COM. Если Win32 использует COM как гарнир, то Windows Runtime выводит COM на первый план. Windows Runtime является преемником Win32, где последний является собирательным термином для Windows API, так как он охватывает множество разных технологий и моделей программирования. Windows Runtime предоставляет согласованную и унифицированную модель программирования, но, чтобы Windows Runtime завоевала популярность, программистам как внутри, так и вне Microsoft нужны более качественные средства разработки WinRT-компонентов и возможность использования этих компонентов из приложений.

Основной инструмент, предоставляемый Windows SDK для удовлетворения этих потребностей, — компилятор MIDL. В прошлой статье я показал, как компилятор MIDL может создавать файл Windows Runtime Metadata (WINMD), необходимый большинству языковых проекций для использования WinRT-компонентов. Конечно, любой опытный разработчик на платформе Windows знает, что компилятор MIDL также создает код, который компилятор C или C++ способен использовать напрямую. По сути, сам MIDL ничего не знает о формате файла WINMD. В принципе, он занимается разбором IDL-файлов и генерацией кода для компиляторов C и C++, чтобы поддерживать разработку с применением COM и RPC (remote procedure call), а также создает прокси-DLL. Так сложилось исторически, что компилятор MIDL является критически важной частью всей системы, а потому инженеры, создававшие Windows Runtime, предпочли не рисковать с изменениями в этом компиляторе, а разработали «субкомпилятор», отвечающий только за Windows Runtime. Разработчики обычно не подозревают об этой уловке (и не должны подозревать), но это помогает объяснить, как на практике работает компилятор MIDL.

Давайте рассмотрим какой-нибудь исходный IDL-код и поглядим, что на самом деле творится в компиляторе MIDL. Вот исходный IDL-файл, в котором определяется классический COM-интерфейс:

C:\Sample>type Sample.idl
import "unknwn.idl";

[uuid(e21df825-937d-4b0b-862e-e411b57e280e)]
interface IHen : IUnknown
{
  HRESULT Cluck();
}

В классической COM нет жесткой концепции пространств имен, поэтому интерфейс IHen просто определяется на уровне файла. Кроме того, до его использования должно быть импортировано определение IUnknown. Тогда я смогу пропустить этот файл через компилятор MIDL, чтобы получить ряд артефактов:

C:\Sample>midl Sample.idl
C:\Sample>dir /b
dlldata.c
Sample.h
Sample.idl
Sample_i.c
Sample_p.c

Файл исходного кода dlldata.c содержит несколько макросов, которые реализуют необходимые экспорты для прокси-DLL. В файле Sample_i.c содержится GUID интерфейса IHen на случай, если вас угораздило пользоваться компилятором 25-летней давности, в котором отсутствует поддержка uuid __declspec, прикрепляющая GUID типам. В файле Sample_p.c содержатся инструкции маршалинга для прокси-DLL. Пока что я обойду молчанием эти файлы и вместо этого сосредоточусь на Sample.h, который хранит кое-что весьма полезное. Если вы просмотрите все эти кошмарные макросы, призванные помочь разработчикам на C использовать COM (ужас!), то найдете это:

MIDL_INTERFACE("e21df825-937d-4b0b-862e-e411b57e280e")
IHen : public IUnknown
{
public:
  virtual HRESULT STDMETHODCALLTYPE Cluck( void) = 0;

};

Это не элегантный C++, но после предобработки вы получаете C++-класс, который наследует от IUnknown и добавляет свою чисто виртуальную функцию. Это удобно, потому что вам не придется писать это вручную, возможно, вводя несоответствие между C++-определением интерфейса и исходным IDL-определением, которое могли бы использовать другие инструменты и языки. В этом суть того, что компилятор MIDL предоставляет разработчикам на C++, транслируя исходный IDL-код так, чтобы компилятор C++ мог напрямую использовать эти типы.

Теперь вернемся к Windows Runtime. Я слегка обновлю исходный IDL-код, чтобы он соответствовал более строгим требованиям для WinRT-типов:

C:\Sample>type Sample.idl
import "inspectable.idl";

namespace Sample
{
  [uuid(e21df825-937d-4b0b-862e-e411b57e280e)]
  [version(1)]
  interface IHen : IInspectable
  {
    HRESULT Cluck();
  }
}

WinRT-интерфейсы должны наследовать напрямую от IInspectable, а пространство имен используется отчасти для сопоставления типов с реализующим их компонентом. Если я попытаюсь скомпилировать свой код, как раньше, то получу проблему:

.\Sample.idl(3) : error MIDL2025 : syntax error : expecting
an interface name or DispatchInterfaceName or CoclassName
or ModuleName or LibraryName or ContractName or a type
specification near "namespace"

Компилятор MIDL не распознает ключевое слово namespace и отказывается продолжать. Вот для чего нужен ключ /winrt в командной строке. Он сообщает компилятору MIDL напрямую передавать командную строку компилятору MIDLRT для предобработки исходного IDL-файла. Именно второй компилятор (MIDLRT) ожидает ключ /metadata_dir в командной строке, о котором я упоминал в прошлой статье:

C:\Sample>midl /winrt Sample.idl /metadata_dir
  "C:\Program Files (x86)\Windows Kits ..."

Чтобы получить еще одно доказательство, посмотрите повнимательнее на вывод компилятора MIDL и вы поймете, что я имею в виду:

C:\Sample>midl /winrt Sample.idl /metadata_dir "..."
Microsoft (R) 32b/64b MIDLRT Compiler Engine Version 8.00.0168
Copyright (c) Microsoft Corporation. All rights reserved.
MIDLRT Processing .\Sample.idl
.
.
.
Microsoft (R) 32b/64b MIDL Compiler Version 8.00.0603
Copyright (c) Microsoft Corporation. All rights reserved.
Processing C:\Users\Kenny\AppData\Local\Temp\Sample.idl-34587aaa
.
.
.

Я убрал часть обработки зависимостей, чтобы высветить ключевые моменты. Вызов исполняемого файла MIDL с ключами /winrt сразу же передает командную строку исполняемому файлу MIDLRT. MIDLRT разбирает IDL, чтобы сначала сгенерировать WINMD-файл, а потом создать другой временный IDL-файл. Этот временный IDL-файл является трансляцией исходного со всеми ключевыми словами, специфичными для WinRT, например определяющими пространства имен, но они заменены так, чтобы компилятор MIDL принял их. Затем MIDLRT снова вызывает MIDL но без ключа /winrt и с указанием пути к временному IDL-файлу, чтобы создать исходный набор заголовочных файлов и файлов исходного кода на C и C++, как раньше.

Пространство имен в исходном IDL-файле удаляется, а имя интерфейса IHen дополняется во временном IDL-файле следующим образом:

interface __x_Sample_CIHen : IInspectable
.
.
.

На самом деле это кодированная форма имени типа, которое интерпретируется компилятором MIDL с учетом ключа /gen_namespace командной строки, используемого MIDLRT при вызове MIDL с предварительно обработанным выводом. Затем компилятор MIDL может обрабатывать это напрямую без знания специфических деталей Windows Runtime. Это просто один из примеров, но он дает представление о том, как новый инструментарий выжимает максимум возможного из существующей технологии для выполнения своей работы. Если вам любопытно, как это работает, загляните во временную папку, создаваемую компилятором MIDL: вы обнаружите лишь то, что эти файлы (Sample.idl-34587aaa в предыдущем примере) отсутствуют. Исполняемый файл MIDLRT аккуратно подчищает все за собой, но, если указать ключ /savePP в командную строку, временные файлы препроцессора удаляться не будут. Так или иначе, выполняется дополнительная предобработка, и полученный Sample.h теперь содержит нечто, что даже компилятор C++ воспринимает как пространство имен:

namespace Sample {
  MIDL_INTERFACE("e21df825-937d-4b0b-862e-e411b57e280e")
  IHen : public IInspectable
  {
  public:
    virtual HRESULT STDMETHODCALLTYPE Cluck( void) = 0;
  };
}

После этого я могу реализовать этот интерфейс, как и раньше, будучи уверенным, что компилятор отловит любые расхождения между моей реализацией и оригинальными определениями, которые я кодировал в IDL. С другой стороны, если вам нужен MIDL только для создания WINMD-файла и не требуются остальные исходные файлы для компилятора C или C++, вы можете избавиться от всех дополнительных артефактов компиляции, указав в командной строке ключ /nomidl. Этот ключ наряду с остальными передается исполняемым файлом MIDL исполняемому файлу MIDLRT. Потом MIDLRT пропускает последний этап повторного вызова MIDL после создания WINMD-файла. Также общепринято при использовании Windows Runtime ABI, сгенерированного MIDL, включать /ns_prefix в командную строку, чтобы полученные типы и пространства имен охватывались пространством имен ABI:

namespace ABI {
  namespace Sample {
    MIDL_INTERFACE("e21df825-937d-4b0b-862e-e411b57e280e")
    IHen : public IInspectable
    {
    public:
      virtual HRESULT STDMETHODCALLTYPE Cluck( void) = 0;
    };
  }
}

Наконец, следует упомянуть, что и MIDL, и MIDLRT мало для создания самодостаточного WINMD-файла, который исчерпывающе описывает типы компонента. Если вы ссылаетесь на внешние типы, обычно определенные в ОС, то WINMD-файл, полученный в описанном процессе, все равно должен быть объединен с основным файлом метаданных для той версии Windows, на которую вы ориентируетесь. Позвольте проиллюстрировать эту проблему на примере.

Я начну с пространства имен в IDL, описывающего как интерфейс IHen, так и активируемый класс Hen, реализующий этот интерфейс (рис. 1).

Рис. 1. Класс Hen в IDL

namespace Sample
{
  [uuid(e21df825-937d-4b0b-862e-e411b57e280e)]
  [version(1)]
  interface IHen : IInspectable
  {
    HRESULT Cluck();
  }
  [version(1)]
  [activatable(1)]
  runtimeclass Hen
  {
    [default] interface IHen;
  }
}

Затем я реализую его, используя тот же способ, который я описывал в прошлой статье, с тем исключением, что теперь я могу полагаться на определение IHen, предоставляемое компилятором MIDL. Далее в WinRT-приложении можно просто создать объект Hen и вызвать метод Cluck. Я воспользуюсь C# для иллюстрации этого уравнения на стороне приложения:

public void SetWindow(CoreWindow window)   
{
  Sample.IHen hen = new Sample.Hen();
  hen.Cluck();
}

Метод SetWindow является частью реализации IFrameworkView, предоставляемой C#-приложением. (Я рассказывал о IFrameworkView в своей рубрике за август 2013 года, которую вы можете найти по ссылке msdn.microsoft.com/magazine/jj883951.) И, конечно, это работает. C# полностью зависим от метаданных WINMD, описывающих компонент. С другой стороны, это резко упрощает использование неуправляемого C++-кода клиентами на C#. Как минимум, в большинстве случаев. Но, если вы ссылаетесь на внешние типы, как я упоминал только что, возникает одна проблема. Давайте обновим метод Cluck так, чтобы он требовал передачи CoreWindow в качестве аргумента. CoreWindow определен ОС, поэтому я не могу просто взять и определить его в своем файле исходного IDL-кода.

Сначала я обновлю IDL с учетом зависимости от интерфейса ICoreWindow. Я просто импортирую его определение так:

import "windows.ui.core.idl";

А затем добавлю параметр ICoreWindow в метод Cluck:

HRESULT Cluck([in] Windows.UI.Core.ICoreWindow * window);

Компилятор MIDL превратит этот import в #include "windows.ui.core.h" внутри заголовочного файла, который он генерирует, поэтому мне остается лишь обновить реализацию класса Hen:

virtual HRESULT __stdcall Cluck(ABI::Windows::UI::Core::ICoreWindow *) 
  noexcept override
{
  return S_OK;
}

Теперь можно компилировать компонент, как раньше, и предоставлять его разработчику C#-приложения. Тот послушно обновляет вызов метода Cluck ссылкой на CoreWindow приложения следующим образом:

public void SetWindow(CoreWindow window)
{
  Sample.IHen hen = new Sample.Hen();
  hen.Cluck(window);
}

К сожалению, теперь будет недоволен компилятор C#:

error CS0012: The type 'ICoreWindow' is defined in an assembly
  that is not referenced.

Видите ли, компилятор C# не распознает интерфейсы как одинаковые. Компилятор C# не удовлетворяется совпадением лишь имени типа и не в состоянии связать его с Windows-типом, имеющим то же имя. C# — в отличие от C++ — очень сильно зависим от информации о двоичных типах. Чтобы решить эту проблему, можно воспользоваться другой утилитой из Windows SDK, которая объединит метаданные из Windows с метаданными моего компонента, корректно разрешая ICoreWindow в основной файл метаданных для ОС. Эта утилита называется MDMERGE:

c:\Sample>mdmerge /i . /o output /partial /metadata_dir "..."

Исполняемые файлы MIDLRT и MDMERGE весьма требовательны к аргументам в своих командных строках. Вы должны правильно указывать их, чтобы они работали. В данном случае я не могу просто обновить Sample.winmd по месту, указав ключи /i (input) и /o (output), которые ссылаются на одну и ту же папку, так как MDMERGE удаляет входной WINMD-файл по окончании своей работы. Ключ /partial сообщает MDMERGE искать неразрешенный интерфейс ICoreWindow в метаданных, предоставленных ключом /metadata_dir. Это называют ссылочными метаданными (reference metadata). Благодаря им MDMERGE можно использовать для объединения нескольких WINMD-файлов, но в данном случае я просто разрешаю ссылки на типы, определенные в ОС.

К этому моменту полученный Sample.winmd корректно указывает на метаданные из Windows при ссылке на интерфейс ICoreWindow, и компилятор C# успешно скомпилирует приложение. В следующем выпуске этой рубрики я продолжу исследовать Windows Runtime из C++.


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

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