2015 年 7 月

Volume 30 Number 7

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

Kenny Kerr | 2015 年 7 月

Kenny Kerrこれから数か月にわたって、Windows ランタイムの基礎について調べていく予定です。目的は、開発者がさまざまな言語プロジェクションやツールチェーンで使用する高度な抽象化を分析し、アプリケーション バイナリ インターフェイス (ABI) のレベルでの Windows ランタイムのしくみを調べることです。ABI は、アプリケーションと、開発者が OS サービスへのアクセスに利用するバイナリ コンポーネントとの境界にあたります。

ある意味、Windows ランタイムは COM が進化したものにすぎません。COM は、コードの再利用における事実上のバイナリ標準で、複雑なアプリケーションや OS のコンポーネントを構築する手段として今でもよく使われています。ただし、COM とは異なり、Windows ランタイムの対象はより限定的で、主に Windows API の基盤として使用されます。アプリケーションの開発者は、今後ますます OS コンポーネントのコンシューマーとして Windows ランタイムを使用するようになり、コンポーネント自体を作成することは少なくなります。とは言え、効率の高いアプリケーションを作成して相互運用性やパフォーマンスの問題を適切に診断できるようにするには、洗練されたさまざまな抽象化のすべてが、多数のプログラミング言語にどのように実装され、プロジェクションされるかを理解することが唯一の方法です。

Windows ランタイムのしくみについて (やや希薄なドキュメントの内容以上のことを) 理解している開発者がかなり少ない理由の 1 つは、ツールの利用や言語プロジェクションによって基盤となるプラットフォームの実情がわかりにくくなっているためです。このような状況は、C# 開発者にとっては自然なことかもしれませんが、実際に内部の処理を把握したいと考える C++ 開発者にとっては快適な状況とはいえません。そこで、Visual Studio 2015 開発者用コマンド プロンプトを使用して、シンプルな Windows ランタイム コンポーネントを標準 C++ で作成することから始めてみましょう。

まず、いくつかの関数をエクスポートする従来のシンプルな DLL から見ていきます。これからの説明に沿って作業を行う場合は、Sample フォルダーを作成し、そのフォルダーに Sample.cpp を始めとしていくつかのソース ファイルを作成します。

C:\Sample>notepad Sample.cpp

最初に、DLL をアンロードします。この DLL から作成するコンポーネントを呼び出す予定です。作成するコンポーネントでは、アンロード クエリをサポートします。それにはエクスポートした関数呼び出し (DllCanUnloadNow) を使用します。これは、CoFreeUnusedLibraries 関数によってアンロードを制御するアプリケーションです。この方法は、従来の COM におけるコンポーネントのアンロードと同じなので、この説明は手短に済ませます。コンポーネントは、(LIB ファイルなどを使って) アプリケーションに静的にリンクするのではなく、LoadLibrary 関数によって動的に読み込むことから、コンポーネントを最終的にアンロードする手段が必要になります。未解決の参照がいくつあるかを実際に把握しているのはこのコンポーネントだけなので、COM ランタイムは DllCanUnloadNow 関数を呼び出して、アンロードしても安全かどうかを判断します。アプリケーションは、CoFreeUnusedLibraries 関数または CoFreeUnusedLibrariesEx 関数を使用して、自身でこのハウスキーピングを実行することもできます。コンポーネントでの実装は簡単です。使用中のオブジェクトの数を追跡するロックが必要になるだけです。

static long s_lock;

各オブジェクトは、単純にこのロックをコンストラクターでインクリメントし、デストラクターでデクリメントします。複雑になるのを避けるため、今回は ComponentLock という小さなクラスを作成します。

struct ComponentLock
{
  ComponentLock() noexcept
  {
    InterlockedIncrement(&s_lock);
  }
  ~ComponentLock() noexcept
  {
    InterlockedDecrement(&s_lock);
  }
};

コンポーネントをアンロードしないオブジェクトは、ComponentLock をメンバー変数として埋め込むだけです。これで DllCanUnloadNow 関数はきわめてシンプルに実装されるようになります。

HRESULT __stdcall DllCanUnloadNow()
{
  return s_lock ? S_FALSE : S_OK;
}

1 つのコンポーネント内に作成できるオブジェクトは、実際には 2 種類あります。1 つはアクティベーション ファクトリ (従来の COM ではクラス ファクトリと呼ばれていました)、もう 1 つは特定のクラスの実際のインスタンスです。今回はシンプルな "Hen" クラスを実装します。このクラスでは、雌鶏 (hen) が鳴ける (cluck) ように、IHen インターフェイスを定義します。

struct __declspec(uuid("28a414b9-7553-433f-aae6-a072afe5cebd")) __declspec(novtable)
IHen : IInspectable
{
  virtual HRESULT __stdcall Cluck() = 0;
};

これは、IUnknown から直接派生しないで IInspectable から派生しているだけで、通常の COM インターフェイスです。今回は、2014 年 12 月号 (msdn.com/magazine/dn879357) で説明した Implements クラス テンプレートを使ってこのインターフェイスを実装し、コンポーネント内で実際に Hen クラスを実装します。

struct Hen : Implements<IHen>
{
  ComponentLock m_lock;
  virtual HRESULT __stdcall Cluck() noexcept override
  {
    return S_OK;
  }
};

アクティベーション ファクトリは、IActivationFactory インターフェイスを実装する C++ クラスにすぎません。この IActivationFactory インターフェイスには、唯一のメソッドである ActivateInstance があります。これは、従来の COM の IClassFactory インターフェイスとその CreateInstance メソッドの関係に似ています。従来の COM インターフェイスは、呼び出し元が特定のインターフェイスを直接要求できるという点で Windows ランタイムよりもわずかに優れています。Windows ランタイムの IActivationFactory は IInspectable インターフェイス ポインターを返すだけです。したがって、アプリケーションが IUnknown QueryInterface メソッドの呼び出しを担当し、オブジェクトへのより有益なインターフェイスを取得することになります。いずれにせよ、これで ActivateInstance メソッドの実装がかなり簡素化されます。

struct HenFactory : Implements<IActivationFactory>
{
  ComponentLock m_lock;
  virtual HRESULT __stdcall ActivateInstance(IInspectable ** instance)
    noexcept override
  {
    *instance = new (std::nothrow) Hen;
    return *instance ? S_OK : E_OUTOFMEMORY;
  }
};

このコンポーネントは、DllGetActivationFactory という別の関数をエクスポートすることで、アプリケーションから特定のアクティベーション ファクトリを取得できるようにします。これも、COM のアクティベーション モデルをサポートする DllGetClassObject エクスポート関数に似ています。大きな違いは、目的のクラスが GUID ではなく文字列で指定されることです。

HRESULT __stdcall DllGetActivationFactory(HSTRING classId,
   IActivationFactory ** factory) noexcept
{
}

HSTRING は、変更不可の文字列値を表すハンドルです。クラス識別子 ("Sample.Hen" など) であり、返すアクティベーション ファクトリを指定します。ここで、いくつかの理由で DllGetActivationFactory の呼び出しが失敗する可能性があるため、最初に nullptr を使用してファクトリ変数をクリアします。

*factory = nullptr;

ここで、HSTRING クラス識別子のバッキング バッファーを取得する必要があります。

wchar_t const * const expected = WindowsGetStringRawBuffer(classId, nullptr);

その後、この値と、コンポーネントが実装するすべてのクラスを比較できます。現時点では、そのようなクラスは 1 つだけです。

if (0 == wcscmp(expected, L"Sample.Hen"))
{
  *factory = new (std::nothrow) HenFactory;
  return *factory ? S_OK : E_OUTOFMEMORY;
}

それ以外の場合は、要求されたクラスが使用不可であることを示す HRESULT を返します。

return CLASS_E_CLASSNOTAVAILABLE;

このシンプルなコンポーネントを実行できるようにするのに必要な C++ の作業は以上です。しかし、このコンポーネントの DLL を実際に作成し、その後、ヘッダー ファイルの解析方法を知らない厄介な C# コンパイラに DLL のことを説明するには、もう少し作業が必要です。DLL を作成するには、リンカーが必要です。具体的に言うと、DLL からエクスポートされる関数を定義するリンカーの機能が必要です。マイクロソフトのコンパイラ固有の dllexport __declspec 指定子を使用することも可能です。しかし、この場面はリンカーを直接操作し、エクスポートする一連の関数を指定したモジュール定義ファイルを提供する方が望ましい数少ない例の 1 つです。個人的には、このアプローチの方がエラーが発生しにくいと考えています。それでは、コンソールに戻って 2 つ目のソース ファイルを作成します。

C:\Sample>notepad Sample.def

この DEF ファイルに必要なのは、エクスポートする関数を一覧する EXPORTS というセクションだけです。

EXPORTS
DllCanUnloadNow         PRIVATE
DllGetActivationFactory PRIVATE

これで、このモジュール定義ファイルと一緒に C++ ソース ファイルをコンパイラとリンカーに渡して DLL を生成できます。次に、コンポーネントを作成する際の都合を考えて簡単なバッチ ファイルを使用し、すべてのビルド アーティファクトをサブフォルダーに置きます。

C:\Sample>type Build.bat
@md Build 2>nul
cl Sample.cpp /nologo /W4 /FoBuild\ /FeBuild\Sample.dll /link /dll /def:Sample.def

バッチ ファイル スクリプト言語という魔法の技術はさておき、Visual C++ コンパイラのオプションに注目します。/nologo オプションは、著作権バナーが表示されないようにします。このオプションはリンカーにも転送されます。欠かせない /W4 オプションは、一般的なコーディング バグに対して表示する警告を増やすようにコンパイラに指示します。/FoBuild というオプションはありません。コンパイラには、このように読みにくい名前表記があり、オプション (この例では /Fo) の後に出力パスを指定します。オプションと出力パスの間にスペースを置きません。いずれにせよ、/Fo オプションを使用して、コンパイラが Build サブフォルダー内のオブジェクト ファイルを除去するようにします。/Fo オプションは、/Fe オプションを使用して定義した実行可能ファイルと同じ出力フォルダーを既定で使用しない、唯一のビルド出力です。/link オプションは、後続の引数がリンカーによって解釈されることをコンパイラに通知します。これにより、第 2 の手順としてリンカーを呼び出す必要がなくなります。コンパイラとは異なり、リンカーのオプションでは大文字と小文字が区別されるため、オプション名と値との間にセパレーターを使用します。これは、使用するモジュール定義ファイルを指定する /def オプションと同様です。

これで、コンポーネントはかなり簡単にビルドできるようになります。結果の Build サブフォルダーには、いくつかのファイルが格納されますが、重要なのはそのうちの 1 ファイルだけです。当然それは、アプリケーションのアドレス空間に読み込むことのできる Sample.dll 実行可能ファイルです。しかし、このファイルだけでは不十分です。アプリケーション開発者は、コンポーネントの内容を把握する手段が必要です。C++ 開発者は、IHen インターフェイスが格納されたヘッダー ファイルで満足するかもしれませんが、このヘッダー ファイルもとりわけ便利なわけではありません。Windows ランタイムには、言語プロジェクションの概念が組み込まれています。これにより、コンポーネントをさまざまな言語で認識できるようになり、型がその言語のプログラミング モデルにプロジェクションされます。言語プロジェクションについては来月以降に説明しますが、それまでは最も説得力の高い C# アプリケーションでこのサンプルを操作してみます。既に説明したように、C# コンパイラには C++ ヘッダー ファイルの解析手段が用意されていません。したがって、C# コンパイラが正常に動作するようにいくつかのメタデータを提供する必要があります。コンポーネントを説明する CLR メタデータが格納された WINMD ファイルを生成します。これは簡単な作業ではありません。コンポーネントの ABI に使用するネイティブの型は、C# にプロジェクションされると外観が大きく変化するためです。さいわい、Microsoft IDL コンパイラは、いくつかの新しいキーワードを使用する IDL ファイルによって WINMD ファイルを生成するようになっています。それでは、コンソールに戻って 3 つ目のソース ファイルを作成します。

C:\Sample>notepad Sample.idl

まず、必須の IInspectable インターフェイスの定義をインポートする必要があります。

import "inspectable.idl";

次に、コンポーネントの型の名前空間を定義します。この名前空間は、コンポーネント自体の名前に一致している必要があります。

namespace Sample
{
}

次に、既に C++ で定義した IHen インターフェイスを定義する必要があります。ただし、今回は IDL インターフェイスとして定義します。

[version(1)]
[uuid(28a414b9-7553-433f-aae6-a072afe5cebd)]
interface IHen : IInspectable
{
  HRESULT Cluck();
}

これは古き良き IDL です。IDL を使用して COM コンポーネントを定義した経験がある方にとっては、驚くような点は何もありません。しかし、Windows ランタイムのすべての型では、version 属性を定義する必要があります。この処理は、以前は省略することができました。インターフェイスも、すべて IInspectable から直接派生する必要があります。Windows ランタイムには、インターフェイスの継承は事実上ありません。その結果、好ましくない事態が発生することがありますが、これについては来月以降に説明します。

最後に、新しい runtimeclass キーワードを使用して Hen クラス自体を定義する必要があります。

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

ここでも version 属性が必要になります。activatable 属性は、必須ではありませんが、このクラスがアクティブ化できることを示します。この例の activatable 属性は、IActivationFactory ActivateInstance メソッドによって既定のアクティブ化がサポートされることを示します。言語プロジェクションは、C++ や C# の既定のコンストラクターとして、または特定の言語にとって意味のあるものとして使用すべきです。最後に、interface キーワードの前にある default 属性は、IHen インターフェイスが Hen クラスの既定のインターフェイスであることを示します。既定のインターフェイスは、戻り値の型によってクラス自体が指定される場合に、パラメーターと戻り値の型の代わりに機能するインターフェイスです。ABI が機能するのは COM インターフェイスだけであり、Hen クラス自体はインターフェイスではないことから、既定のインターフェイスは ABI レベルで表されます。

ここで説明できることは他にもいろいろありますが、差し当たりはこれで十分でしょう。これで、バッチ ファイルを更新し、コンポーネントを説明する WINMD ファイルを生成できます。

@md Build 2>nul
cl Sample.cpp /nologo /W4 /FoBuild\ /FeBuild\Sample.dll /link /dll /def:Sample.def
"C:\Program Files (x86)\Windows Kits\10\bin\x86\midl.exe" /nologo /winrt /out %~dp0Build /metadata_dir "c:\Program Files (x86)\Windows Kits\10\References\Windows.Foundation.FoundationContract\1.0.0.0" Sample.idl

ここでもバッチ ファイル内の魔法のような処理については目をつぶり、MIDL コンパイラ オプションの新機能に注目します。鍵となるのは /winrt オプションです。このオプションは、IDL ファイルに、従来の COM または RPC スタイルのインターフェイス定義ではなく Windows ランタイム型が格納されていることを示します。/out オプションは、C# ツールチェーンの要件に適合するように、DLL と同じフォルダーに WINMD ファイルが格納されていることを確認します。/metadata_dir オプションは、OS の構築に使用されたメタデータの場所をコンパイラに伝えます。本稿執筆時点では、Windows SDK for Windows 10 がまだ定着していないため、Visual Studio ツール コマンド プロンプトに表示されるパスの MIDL コンパイラではなく、Windows SDK に同梱される MIDL コンパイラを呼び出すようにします。

これで、バッチ ファイルを実行すると Sample.dll と Sample.winmd が生成されるようになります。この 2 つのファイルは、C# Windows ユニバーサル アプリから参照し、別の CLR ライブラリ プロジェクトであるかのように Hen クラスを使用できます。

Sample.Hen h = new Sample.Hen();
h.Cluck();

Windows ランタイムは、COM と標準 C++ を基盤に構築されています。CLR をサポートすれば、相互運用コンポーネントがなくても、C# 開発者が非常に簡単に Windows API を使用できるように調整されています。Windows ランタイムは、Windows API の将来の姿です。

今回は、このテクノロジの起源を理解するために、従来の COM と C++ コンパイラの COM のルーツという観点から、Windows ランタイム コンポーネントの開発について具体的に紹介しました。しかし、少し時間がたてば、このアプローチも実用的とは言えなくなります。MIDL コンパイラには、実際は単なる WINMD ファイルをはるかに超える機能が用意されています。この機能は、特に標準バージョンの IHen インターフェイスを C++ で生成するために、実際に使用できます。来月は、Windows ランタイム コンポーネントを作成する場合のさらに信用度の高いワークフローについて説明し、その過程で相互運用性の問題をいくつか解決していきます。来月号もお読みいただければさいわいです。


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

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