年 9 月 2015

ボリューム 30 番号 9

Windows と C++ - Windows ランタイムにおける洗練された型

Kenny Kerr | 年 9 月 2015

Kenny Kerrコンポーネントの開発者は、Windows ランタイム (WinRT) によって、洗練された型システムをアプリの開発者に提供できます。WinRT 全体が COM のインターフェイスを使って実装されることを考えると、C++ や従来の COM に詳しい開発者にはあり得ない話のように思えるかもしれません。突き詰めると、COM はクラスではなく、インターフェイスを中心とするプログラミング モデルです。COM のアクティベーション モデルにより、CoCreateInstance やその関連関数を使用してクラスを構築することはできます。しかしこれでは、既定のコンストラクターのようなものだけをもっともらしくサポートするにすぎません。こうした感覚は、WinRT RoActivateInstance でも変わりません。では、どういうしくみで型システムを提供できるのでしょう。

実際のしくみは、最初に受ける印象ほどつじつまの合わないものではありません。洗練された型システムをサポートするのに必要な基本的な抽象化は、COM アクティベーション モデルによって既に提供されています。この抽象化をコンシューマーに示すのに必要なメタデータが不足しているだけです。COM アクティベーション モデルは、クラスのオブジェクトとインスタンスを定義します。クラスのインスタンスをコンシューマーが直接作成しないクラスもあります。アプリ開発者が、CoCreateInstance を呼び出してクラスのインスタンスをいくつか作成しても、目的のインスタンスを取得するためには、依然として OS がそのクラス オブジェクトを取得する必要があります。

WinRT は、クラス オブジェクトを新しい名前で呼びますが、メカニズムは同じです。アクティベーション ファクトリは、コンシューマーがコンポーネントを呼び出す最初の部分です。コンポーネントは、DllGetClassObject を実装するのではなく、DllGetActivationFactory という関数をエクスポートします。クラスの識別方法は変化していますが、結果としては、特定のクラスに関連してさらにクエリできるオブジェクトにすぎません。従来のクラス オブジェクト (以下、「アクティベーション ファクトリ」) は IClassFactory インターフェイスを実装していました。呼び出し元はこのインターフェイスを使って、このクラスのインスタンスを作成できます。新しい方式では、WinRT ランタイムのアクティベーション ファクトリが、新しい IActivationFactory インターフェイスを実装して、事実上同じサービスを提供する必要があります。

前 2 回のコラムで説明したように、activatable 属性で修飾された WinRT ランタイム クラスは、メタデータを作成し、既定の構築を許可するように言語プロジェクションに指示します。前提として、属性を付けたクラスのアクティベーション ファクトリは、IActivationFactory の ActivateInstance メソッドを利用して、そのように既定で構築されたオブジェクトを提供します。IDL で記述した次のシンプルなランタイム クラスは、既定のコンストラクターを備えたクラスを表します。

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

パラメーターのセットが異なる追加のコンストラクターを表すメソッドのコレクションを収容するインターフェイスを使って、追加のコンストラクターを IDL で記述することもできます。IActivationFactory インターフェイスが既定の構築を定義するのと同様、コンポーネント固有のインターフェイスやクラス固有のインターフェイスがパラメーター化された構築を表します。特定の数の鳴き声 (clucks) を指定して Hen オブジェクトを作成するシンプルなファクトリ インターフェイスを以下に示します。

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

明確な理由はありませんが、IHenFactory インターフェイスは、IActivationFactory と同様、IInspectable から直接継承する必要があります。これにより、このファクトリ インターフェイスの各メソッドは、指定されたパラメーターを受け取る追加コンストラクターとしてプロジェクションされます。また、各メソッドの最後にクラスと同じ型を持つ論理戻り値を返す必要があります。その後、このファクトリ インターフェイスを、インターフェイス名を示す別の activatable 属性を使って、明示的にランタイム クラスと関連付けます。

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

作成するクラスが既定のコンストラクターを必要としない場合は、最初の activatable 属性を省略するだけで、そのクラスの言語プロジェクションも同様に既定のコンストラクターを省略します。あるバージョンの鶏コンポーネントを出荷後、大きなとさか (comb) を使って雌鶏 (hen) を作成できないことに気付いたら、どうすればよいでしょう。顧客に出荷してしまった 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);
}

次に、この 2 つ目のファクトリ インターフェイスを、以前のように Hen クラスに関連付けます。

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

統合は言語プロジェクションが担当するため、アプリ開発者には多数のオーバーロード コンストラクターが提示されるだけです (図 1 参照)。

IDL-Defined Factory Methods in C#
図 1 C# での IDL 定義のファクトリ メソッド

コンポーネント開発者は、こうしたファクトリ インターフェイスを、前提条件の "IActivationFactory" と併せて慎重に実装します。

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

ここでは、2014 年 12 月のコラム (msdn.microsoft.com/magazine/dn879357) で説明した Implements クラス テンプレートを再び使用しています。既定の構築を使用しない場合も、hen のアクティベーション ファクトリでは IActivationFactory インターフェイスの実装が必要です。これは、コンポーネントに実装する必要のある DllGetActivationFactory 関数が、IActivationFactory インターフェイスを返すようにハードコードされているためです。したがって、アプリケーションで既定以外の構築が必要な場合、言語プロジェクションは、結果のポインターの QueryInterface を最初に呼び出す必要があります。それでも、IActivationFactory の ActivateInstance 仮想関数を実装しなければなりませんが、何も操作を実行しない次のような関数で十分です。

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 クラスに、スローするコンストラクターがないことが前提となります。そうしたコンストラクターでは、clucks の C++ ベクトルを割り当てることが必要になるかもしれません。その場合は、シンプルな例外ハンドラーを使えば対処できます。

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

静的クラス メンバーはどうなるでしょう。以前にも説明したように、アクティベーション ファクトリでは静的クラス メンバーも COM インターフェイスを使用して実装します。IDL に戻って、任意の静的メンバーをすべて収容するインターフェイスを定義できます。庭にいる産卵鶏 (layer hen) の数をレポートするコードを作成してみます。

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

次は、このインターフェイスを、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# コンパイラと共通言語ランタイム (CLR) では数多くの操作が実行されます。実行時には、CLR が RoGetActivationFactory を呼び出します。RoGetActivationFactory は、LoadLibrary を呼び出し、その後 DllGetActivationFactory を呼び出して、Sample.Hen アクティベーション ファクトリを取得します。次に、RoGetActivationFactory は QueryInterface を呼び出してファクトリの IHenFactory インターフェイス実装を取得します。これで初めて、CreateHenWithClucks 仮想関数を呼び出して Hen オブジェクトを作成できるようになります。言うまでもありませんが、Hen オブジェクトのさまざまな属性やセマンティクスを特定するよう、CLR を入力する各オブジェクトに促すたびに、CLR によって QueryInterface の追加呼び出しが多数要求されます。

静的メンバーの呼び出しも、同じくシンプルに実行できます。

int layers = Sample.Hen.Layers;

しかしここでも、CLR では、アクティベーション ファクトリを取得する RoGetActivationFactory を呼び出した後、QueryInterface を呼び出して IHenStatics インターフェイス ポインターを取得する必要があります。これによって初めて、この静的プロパティの値を取得する get_Layers 仮想関数を呼び出せるようになります。ただし、CLR はアクティベーション ファクトリをキャッシュするため、こうした呼び出しを何度も実行するうちに負荷はいく分低下します。その反面、コンポーネント内でファクトリをキャッシュしようとすると、CLR により、操作がやや無意味で不必要に煩雑になります。これについてはまた別の機会に説明しましょう。来月は、引き続き C++ の Windows ランタイムについて説明します。ぜひご覧ください。


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


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