2015 年 8 月

Volume 30 Number 8

Windows と C++ - Windows ランタイムのコンポーネントと MIDL

Kenny Kerr | 2015 年 8 月

Kenny Kerr2015 年 7 月号 (msdn.microsoft.com/magazine/mt238401) では、COM プログラミング パラダイムが進化したものとして、Windows ランタイム (WinRT) コンポーネントの考え方を紹介しました。COM は、Win32 では脇役でしたが、Windows RT では主役に据えられます。Windows RT は Win32 の後継となるものですが、Windows API の包括的用語となっていて、多種多様なテクノロジやプログラミング モデルを網羅しています。Windows RT は、一貫性のある統一プログラミング モデルを提供しますが、マイクロソフトの開発者だけでなく多くの開発者がこのプログラミング モデルを正しく使えるようになるには、WinRT コンポーネントを開発するために使用でき、アプリ内から WinRT コンポーネントを操作するために使用できる優れたツールが必要です。

このニーズを満たすために Windows SDK が提供する主要ツールが MIDL コンパイラです。7 月号では、Windows RT メタデータ (WINMD) ファイルを、MIDL コンパイラで生成する方法を紹介しました。このファイルは、ほとんどの言語プロジェクションから WinRT を使用するために必要になります。もちろん、Windows プラットフォームについて経験豊富な開発者なら、MIDL コンパイラは、C や C++ のコンパイラが直接利用できるコードも生成できることはご存じでしょう。実は、MIDL 自体は WINMD ファイル形式を理解しません。MIDL の主な目的は、COM やリモート プロシージャ コール (RPC) の開発やプロキシ DLL の生成をサポートするために、IDL ファイルを解析し、C と C++ コンパイラ向けにコードを生成することです。MIDL コンパイラは昔から非常に重要なメカニズムだったので、Windows RT を開発したエンジニアは、Windows RT が機能しなくなるリスクが生じないよう、Windows RT だけを担当する「サブ コンパイラ」を開発しています。通常、開発者はこのような隠し技があることを知りませんし、知る必要もありません。とは言え、MIDL コンパイラの実際のしくみを説明する場合にはこの知識が役に立ちます。

IDL のソース コードをいくつか見ながら、MIDL コンパイラによる実際の処理を見ていきます。以下は、従来の COM インターフェイスを定義する IDL ソース ファイルです。

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 に必要なエクスポートを実装するマクロがいくつか含まれています。25 年物のコンパイラを使用していて、型に GUID をアタッチする uuid __declspec をサポートしていない場合は、Sample_i.c に IHen インターフェイスの GUID が含まれます。Sample_p.c もあります。これには、プロキシ DLL 用のマーシャリング命令が含まれています。これらのファイルについてはひとまず横に置き、Sample.h を見てみます。これには、きわめて有用な機能が含まれています。COM を使用する C 開発者の支援を目的に用意されたすばらしいマクロを見落としてしまうと、後々大変なことになります。たとえば、以下のようなコードがあります。

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

C++ のようには洗練されていませんが、プリプロセッサで処理された後は、IUnknown から継承し、独自の純仮想関数を追加した C++ クラスになります。これは、手作業で作成する必要がないので便利です。コードを手作業で記述すると、インターフェイスの C++ 定義と、他のツールや言語が利用する元の IDL 定義との間に不一致が生じるおそれがあります。MIDL コンパイラが C++ 開発者向けに提供する機能の本質がここにあり、IDL ソースコードは C++ コンパイラが変換後の型を直接使用できる方法で変換されます。

ここで、Windows RT に話を戻します。WinRT 型に関するさらに厳しい要件に従うために、IDL ソース コードを少し更新します。

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 コマンド ライン オプションがあります。このオプションは、コマンド ラインを MIDLRT コンパイラに直接渡して、IDL ソース ファイルをプリプロセッサで処理するように MIDL コンパイラに指示します。この 2 つ目のコンパイラ (MIDLRT) は、7 月号で取り上げた /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
.
.
.

このコードでは、重要な点を理解しやすいように、依存関係の処理を一部省略しています。/winrt オプションを指定して MIDL 実行可能ファイルを呼び出すと、コマンド ラインをそのまま MIDLRT 実行可能ファイルに渡して終了します。MIDLRT は、まず IDL を解析して WINMD ファイルを生成しますが、同時に一時 IDL ファイルも生成します。この一時 IDL ファイルは、オリジナルの IDL ファイルを変換したもので、WinRT 固有キーワード (名前空間など) をすべて、MIDL コンパイラが受け入れ可能なかたちに置き換えています。次に、MIDLRT は、再び MIDL 実行可能ファイルを呼び出します。ただし、今回は /winrt オプションを指定せず、一時 IDL ファイルの場所を指定します。その結果、MIDL は C や C++ のヘッダーとソース ファイルの本来のセットを以前のように生成できるようになります。

一時 IDL ファイルでは、オリジナルの IDL ファイルにあった名前空間が削除され、IHen インターフェイスの名前が以下のように修飾されます。

interface __x_Sample_CIHen : IInspectable
.
.
.

実はこれが、/gen_namespace コマンド ライン オプションを指定した場合に MIDL が解釈するエンコード済み形式の型名です。MIDLRT は、プリプロセッサによる処理後の出力を指定して MIDL を呼び出すときにこのコマンド ライン オプションを指定します。これにより、本来の MIDL コンパイラは、Windows RT に関する具体的な知識がなくても直接このコードを処理できるようになります。これは単なる一例ですが、新しいツールが、既存のテクノロジを最大限に活用してその役割を果たすしくみをおわかりいただけるでしょう。このしくみを知りたい方は、MIDL コンパイラが追跡する一時フォルダーを確認してみてください。ただし、そのままでは一時 IDL ファイル (上記の例では Sample.idl-34587aaa) は見つかりません。MIDLRT 実行可能ファイルは自身の処理が完了すると、最後に丁寧なクリーン アップを行います。ただし、/savePP コマンド ライン オプションを指定すると、一時プリプロセッサ ファイルが削除されなくなります。いずれにせよ、さらにもう少しプリプロセッサによる処理が行われてから、C++ コンパイラが名前空間として認識できるコードが Sample.h に含まれるようになります。

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

このインターフェイスを先ほどのように実装すると、実装したコードと IDL に記述したオリジナルの定義との相違がコンパイラによって検出されます。ですが、WINMD ファイルの生成のみを MIDL に求め、C/C++ コンパイラ用のソース ファイルを一切必要としない場合は、/nomidl コマンド ライン オプションを指定すると、他のビルド アーティファクトがすべて割愛されます。このオプションは他のオプションと一緒に、MIDL 実行可能ファイルにから MIDLRT 実行可能ファイルに渡されます。その結果、MIDLRT は、WINMD ファイルの生成後に MIDL を再び呼び出す最後の手順をスキップします。また、MIDL によって生成される Windows ランタイム ABI を使用するときは、/ns_prefix コマンド ラインを指定して、以下のように型と名前空間を "ABI" 名前空間にまとめます。

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

最後に、それだけでコンポーネントの型をすべて表す自己完結型の WINMD ファイルを生成する場合は、MIDL と MIDLRT だけでは足りません。外部の型 (通常は OS によって定義される型など) を参照する場合は、ここまで説明した処理で生成される WINMD ファイルを、ターゲットの Windows バージョンに対応したメイン メタデータ ファイルにマージする必要があります。この問題について説明しておきましょう。

まず、IHen インターフェイスと、このインターフェイスを実装したアクティブ化可能な Hen クラスの両方を表す IDL 名前空間を記述します (図 1 参照)。

図 1 IDL の Hen クラス

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

次に、7 月号で説明したテクニックを使用して、Hen クラスを実装します (ただし、MIDL コンパイラが生成した IHen の定義を利用できるという点は異なります)。これで、WinRT アプリ内部では、単純に Hen オブジェクトを作成して Cluck メソッドを呼び出すことができるようになります。以下に C# を使用して、これに相当するアプリ側のコードを示します。

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

SetWindow メソッドは、C# アプリによって提供される IFrameworkView 実装の一部です (IFrameworkView については、2013 年 8 月号 (msdn.microsoft.com/magazine/jj883951) で説明しました)。当然、このコードは機能します。C# は、コンポーネントを表す WINMD メタデータに全面的に依存します。一方、このメタデータにより、ネイティブ C++ コードを C# クライアントと簡単に共有できるようになります。必ずではありませんが、ほぼ可能です。前述のように、外部型を参照する場合は問題が生じます。Cluck メソッドを更新して、引数として CoreWindow を受け取るようにしてみます。CoreWindow は OS によって定義されるため、単純に IDL ソース ファイル内で定義することはできません。

まず、ICoreWindow インターフェイスの依存関係を持つように IDL を更新します。以下に示すように、単純にその定義をインポートします。

import "windows.ui.core.idl";

次に、ICoreWindow パラメーターを Cluck メソッドに追加します。

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

MIDL コンパイラは、生成するヘッダー windows.ui.core.h の #include に、この "import" を含めます。したがって、必要な作業は Hen クラスの実装の更新だけです。

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

これで、以前と同様にコンポーネントをコンパイルし、アプリ開発者に提供できるようになります。C# アプリの開発者は、以下に示すように、アプリの CoreWindow への参照を指定して Cluck メソッド呼び出すように更新するだけです。

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# コンパイラは、インターフェイスが同一だとは認めません。型名が一致するだけでは不十分で、同名の Windows 型には関連付けられません。C++ とは異なり、C# はドット部分をつなぐバイナリの型情報に大きく依存します。この問題を解決するには、Windows SDK が提供する別のツールを利用できます。このツールは、Windows OS のメタデータをコンポーネントのメタデータと合成またはマージして、ICoreWindow を OS のメイン メタデータ ファイルに正しく解決します。このツールが MDMERGE です。

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

MIDLRT 実行可能ファイルと MDMERGE 実行可能ファイルのコマンド ライン引数はかなり面倒です。うまく機能させるには、これらのオプションを正しく理解する必要があります。今回の例では、処理完了時に MDMERGE によって入力 WINMD ファイルが削除されるため、単純に /i (入力) オプションと /o (出力) オプションが同じフォルダーを指すように Sample.winmd を更新するだけでは機能しません。/partial オプションは、/metadata_dir オプションによって提供されるメタデータに未解決の ICoreWindow インターフェイスが含まれていないかどうかを確認するよう、MDMERGE に指示します。これを参照メタデータと呼びます。MDMERGE を使用して複数の WINMD ファイルをマージできますが、今回の例では、OS 型の参照を解決するためだけに MDMERGE を使用しています。

これで、ICoreWindow インターフェイスを参照する際、生成された Sample.winmd が Windows OS のメタデータを正しく指すようになります。C# コンパイラはエラーを発生させることなく、記述されているとおりにアプリをコンパイルします。来月は、引き続き C++ の Windows ランタイムについて説明します。ぜひご覧ください。


Kenny Kerr は、カナダを拠点とするコンピューター プログラマであり、Pluralsight の執筆者です。また、Microsoft MVP でもあります。彼のブログは kennykerr.ca (英語) で、Twitter は twitter.com/kennykerr (英語) でフォローできます。

この記事のレビューに強力してくれたマイクロソフト技術スタックの Larry Osterman に心より感謝いたします。