第 3 章: Windows 開発テクノロジの選択

アプリケーション開発用の最適なツールとテクノロジを選択するには、いくつか考慮すべき点があります。アプリケーションの要件を考慮するのは当然として、開発チームのスキルや、プロジェクトに与えられた期間も考慮する必要があります。また同じように重要なのが、アプリケーションの実装に使用するプログラミング言語、開発ツール、ライブラリの柔軟性と機能です。Hilo アプリケーションの設計基準は、軽量かつ高パフォーマンスでありながら、完全な機能を備えた魅力ある Windows 7 アプリケーションを、必要最小限のインストール要件で作成することです。この記事では、Hilo アプリケーションの実装に使用されている開発テクノロジがなぜ選ばれたのか、その根拠を示します。

最適な言語の選択

言語は非常にさまざまな種類が存在し、言語によって固有の長所と短所があります。まず判断しなければならないのは、.NET ベースの言語を使用するかどうかです。.NET ベースの言語は洗練されてきてはいますが、もっとパワーや柔軟性に富んだ言語が存在します。.NET ベースの言語で書かれたコードは、.NET Framework ライブラリ、あるいは .NET Platform Invoke や Component Object Model (COM) 相互運用レイヤーを通じて、オペレーティング システム機能にアクセスします。このライブラリや相互運用レイヤーは、可能な限り効率性を追求した設計になっているとはいえ、呼び出しのたびに余分なマシン サイクルが発生します。可能な限り最高のパフォーマンスが要求されるアプリケーションでは、こういった余分なレイヤーによって差が生じてしまいます。

Microsoft Windows プラットフォーム上で開発用にマネージ実行時環境を使用したくないのであれば、候補はそれほど多くありません。最良の候補は、C および C++ です。C は比較的シンプルで強力なプログラミング言語による開発エクスペリエンスを提供しますが、カプセル化、抽象化、継承といった、よりハイレベルな言語の持つメリットはありません。また、Windows 7 アプリケーション プログラミング インターフェイス (API) の大部分が、COM インターフェイス経由でしかアクセスできません。COM インターフェイスは本質的に仮想関数ポインター テーブルへのポインターであり、COM メソッドへのアクセスで必要となる二重の逆参照によって、C コードが複雑化し読みづらくなります。

したがって、Windows 7 ベースのアプリケーションを作成するにあたって、あらゆる面から考慮して優れた選択肢と言えるのは、C++ となります。C++ は、C 本来のパワーや柔軟性、そして C# の高度な言語機能を適切なバランスで備えています。コンパイル言語である C++ は、可能な限り Windows API に近い実行可能コードを作成します。実際、C++ コンパイラ オプティマイザーは、可能な限りコードを効率化します。C++ コンパイラには開発者が最適化のために利用できる多くのオプションがあり、マイクロソフトの C++ コンパイラは、小容量で高速なコードを作成するための最良の方法の 1 つと言えます。

このようなパワーと柔軟性を備えていても、当然のことながら潜在的な落とし穴はあります。C++ の最大の問題はメモリ管理ですが、成熟した言語であるため、たとえばスマート ポインターなど、標準的な解決策が用意されています。定着した標準ライブラリも数多く存在します。これらのライブラリがこれほど長い期間を経て成熟してきたことは、幅広く試用され、信頼性と安定性を備えるものとして確立したことを意味しています。

標準ライブラリを使用するメリットは、その使用方法を具体的に示すドキュメントやサンプルが豊富に存在することであり、これらを利用すれば目的を達成できるという安心感を持つことができます。たとえば、C++ の標準テンプレート ライブラリ (STL) は 20 年前から存在し、1994 年からは ANSI 規格となっています。

さらに、C++ は過去 20 年間にわたって Windows 開発で選ばれてきた言語です。そのため多くの企業が C++ ソース コードに莫大な投資をしてきました。そのうえ、この言語の持つ優れた柔軟性によって、20 年前のソース コードでも、Windows 7 の機能を利用する最新のコードに統合することが可能です。

最適な開発ツールの選択

Visual Studio は長年にわたり、Windows 開発者の間で高く評価されてきた開発ツールです。Visual C++ 2010 には、C++ コードの作成を大幅に簡易化、迅速化するための生産性ツールが追加されています。そうした新機能の 1 つに、有効なセマンティック エラーがあります。Microsoft Office のユーザーにとって親しみのある生産性ツールが、Visual C++ に新機能として追加されました。このコード エディターでは、コンパイラ品質の構文エラーおよびセマンティック エラーが入力されると、その箇所に波形の下線が付けられます。問題のあるコードをマウスでポイントすると、エラーを示すツールヒントが表示されます。

また Visual C++ 2010 には、C++0x 機能と呼ばれる標準の C++ 言語機能も含まれています。こうした C++ コードを読みやすくするための機能がある一方、使用するとコードに著しく影響を与える機能があります。たとえば、C++0x キーワードに nullptr があります。この値を使用してポインターに Null 値を代入すると、コードが少し読みやすくなります。一方、匿名関数オブジェクトを定義するラムダ式は、ネイティブの C++ に関数プログラミングのレベルを追加する強力な機能です。ラムダ式になじみのない開発者にとっては最初のうちはきわめて異質なものに感じられると思いますが、ラムダ式を使うことによってコードが冗長でなくなり、読みやすくなる可能性があります。

最適なグラフィックス ライブラリの選択

モニターに表示される画像は、コンピューターのグラフィックス カードによって生成されています。各ピクセルの色、そして重要なポイントであるピクセルの変化についてグラフィックス カードに指示する方法には、まったく異なる 2 つのテクノロジを使う方法があります。その 2 つのテクノロジとは、グラフィックス デバイス インターフェイス (GDI: Graphics Device Interface) と DirectX です。

GDI は Windows の最初のバージョンから組み込まれてきたテクノロジです。GDI の本質は、個々のピクセルや、線や塗りつぶし図形としてのピクセル集合の色を指示する、一連の C 関数です。GDI を使用すると、ダブル バッファー処理を使用して簡単なアニメーションを実行することができます。つまり、次のフレームをオフスクリーンのビットマップ バッファーに描画しておき、そのフレームを画面に次々とコピーします。この動作は、コピーを実行する関数 (BitBlt) の名前から、ビットブリットと呼ばれています。また、線、四角形、多角形、楕円形を線図形塗りつぶし図形の両方で描画することができます。これらは非常にプリミティブですが、使いやすい関数です。線を描くにはペンを選択して使い、面を塗りつぶすにはブラシを選択して使います。つまりペンまたはブラシを選択し、座標を指定するだけで、GDI によって該当するピクセルが変更されます。GDI は、テキストについても同様に、比較的シンプルなテキスト描画機能を提供しています。フォントを選択して文字間隔やポイント サイズなどの値を指定するだけで、GDI によって画面に非アンチエイリアスのテキストが描画されます。GDI の塗りつぶしルーチンを使うと単色を使用できるようになりますが、GDI ではグラデーション塗りつぶし機能を使えないほか、アルファ チャネルを使って不透明度の情報を提供する機能もありません (アルファ チャネル情報を使用する数少ない GDI の 1 つが AlphaBlend 関数です。これを BitBlt の代わりに使ってビットマップをコピーすることができます)。

.NET Framework の Windows フォームで使われているネイティブ コード グラフィックス エンジン GDI+ では、GDI が機能拡張され、アルファ ブレンドとグラデーション塗りつぶし機能が使用できるようになっており、また、アンチエイリアスされたテキストの描画も可能です。GDI+ は DLL からエクスポートされる C 関数として提供されていますが、GDI よりもはるかにオブジェクト ベースであり、Windows SDK for Windows 7 では、この GDI+ オブジェクトをカプセル化する C++ クラス ライブラリを提供しています。GDI+ は GDI の改良版ですが、実装に使用されているテクノロジは同じです。

GDI (および GDI+) の最大の問題点と思われるのは、これらの API が最新のグラフィックス プロセッシング ユニット (GPU) を前提に書かれていないため、GDI ラスター処理を、最新のグラフィックス プロセッサ固有のテクスチャおよびベクター ベースの処理に変換しなければならない点です。こういった処理は低レベル プリミティブのように思えますが、余分な変換レイヤーが存在する GDI は、GPU に直接働きかける API よりもパフォーマンスが低いということがわかります。

DirectX テクノロジは、最新の GPU を前提に書かれています。そのため API が GPU の機能を反映し、DirectX のプログラミング パラダイムは GPU が使用するオブジェクトに可能な限り近い形で達成できます。DirectXで可能な限り多くのグラフィックス カードをプログラミングできるようにするために、基になるライブラリがカードの GPU の機能をチェックし、GPU がサポートしない機能がある場合、DirectX はソフトウェア実装を提供します。つまり、どのようなグラフィックス カードが使われても、開発者が書くコードは同じになります。DirectX アプリケーションをフル機能のグラフィック カードで実行する場合、ハードウェア アクセラレーションが使用されます。

GPU は 3D 処理に対応する設計となっているため、DirectX は COM に似たインターフェイスを通じてアクセスするオブジェクトのコレクションである Direct3D API を提供しています。

最適なアプリケーション フレームワークの選択

あらゆる機能を搭載した Windows ベースのアプリケーションは複雑すぎて手に負えなくなる可能性があるため、アプリケーション開発では多くの場合、何らかの形式のアプリケーション フレームワークを使用することが重要です。アプリケーション フレームワークの目的は、開発者が基にあるコードの接続に煩わされることなく、アプリケーションの機能に集中できるようにすることです。

ただし、アプリケーション フレームワークを選ぶときは、いくつかのトレードオフを考慮しなければなりません。たとえばフレームワークで提供される既製の実装をそのまま利用すれば、開発作業が大幅にスピードアップする可能性がありますが、その反面、フレームワークによって開発者の自由がきかなくなったり、必要な機能や動作を実装するためにはフレームワークの拡張や修正が必要なので余計に複雑になってしまったり、アプリケーションの全体的なサイズが大きく拡大してしまうといったことが生じることも考えられます。

Hilo の開発は Visual C++ 2010 Express で行われたため、このエディションにない MFC (および ATL) を利用するという選択肢は最初からありませんでした。また、WTL も使用しないことに決定しました。これは WTL のライブラリがサポートされていないためですが、いずれにせよ、Hilo プロジェクトの基本方針は軽量性と柔軟性であるため、Windows および Direct2d API の上位でシン ラッパーとして動作する小容量の共通ライブラリを作成することになりました。すべての Hilo アプリケーションはこのライブラリをベースとして作成されました。Hilo Common Library は、目標とするパフォーマンスやサイズ特性を維持しながら、Hilo アプリケーションの作成を迅速かつ容易にするさまざまな機能を提供しています。

Windows 7 対応の開発

Windows 7 には革新的で魅力的な機能が数多く備わっています。これらの機能はすべて、DLL からエクスポートされる C 関数、または COM インターフェイスのあるオブジェクトによって使用できるようになっています。また Windows 7 では DirectX が強化され、Windows 開発者に人気の API になっています。DirectX とその構成テクノロジである Direct2DDirectXDirectWrite は、いずれも COM に似たインターフェイスを使用します。魅力ある最新の Windows 7 アプリケーションを作成するには、COM インターフェイスと C ライブラリ関数に簡単にアクセスできる言語が必要であり、その言語こそが Visual C++ です。

Windows 7 では、タブレット機能にアクセスするための API が強化され、このためアプリケーションでリッチなユーザー エクスペリエンスを提供できるようになっています。タブレット PC における最も重要な機能は、タッチ対応スクリーンです。ユーザーは指のジェスチャとスタイラスの動きによってアプリケーションと対話します。指のジェスチャの自然な動きに合わせて、アプリケーションの反応も自然であることが要求されます。たとえば、画面を横切るように指でアイテムをドラッグする場合には、選択されたアイテムが指の動きに合わせて移動するようにする必要があります。また、指のジェスチャを使ってアイテムを “フリック” する場合には、そのアイテムが画面上で弾かれたように見せることが必要です。これを実現するためには、Windows 開発者はこれまでにないタイプのグラフィックス開発を行う必要があります。ユーザーは、アプリケーションのアイテムが自分のジェスチャに合った動作をすることを期待しています。開発者には、そのような動きを簡単に実現できるライブラリが必要になります。そこで登場するのが、Direct2D とアニメーション API です。

Direct2D は、コンピューターのグラフィックス カードの GPU に密接した Direct3D よりも上位に位置します。Direct2D のパラダイムは、Windows GDI プログラミングとはまったく異なります。GDI プログラミングでは、画面上のピクセルをどのように変化させるかをプログラム コードが決定すると、コンピューターの CPU はそのピクセルを変更するための計算を実行すると同時に、表示をアニメーション化するための処理も実行します。Direct2D プログラミングでは、プログラムが表示をどのように変化させるかを指定し、グラフィックス カードの GPU に指示を出して、必要な計算を実行させ、表示を更新させます。これにより、コンピューターの CPU が解放されてデータ処理に集中することができ、より高速で応答性の高いアプリケーションが実現されます。グラフィックス カードの専用 GPU がグラフィックス計算を実行するので、より複雑で高パフォーマンスのアニメーションを作成できます。

Direct2D のプログラミング方法については、「C++ での Windows 対応プログラミングの学習: モジュール 3 Windows グラフィックス (英語)」を参照してください。

コンポーネント化

コンポーネント化とは、状態やコードをオブジェクトとしてカプセル化することを意味します。GDI は非オブジェクト API であり、コンポーネント化の機能はありません。GDI+ はオブジェクト ベースですが、コンポーネント化は C 関数によって提供されます (ただし Windows SDK for Windows 7 では、API をコンポーネント化する C++ クラス ライブラリが提供されています)。DirectX API はすべてオブジェクト ベースであり、COM に似たインターフェイスによって提供されます。

COM は、バイナリ ソフトウェア コンポーネントを作成するためのプラットフォームに依存しないオブジェクト指向型システムです。COM オブジェクトはさまざまなプログラミング言語で作成できます。このオブジェクトは、1 つのプロセス内、他のプロセス内、さらにはリモート コンピューター上に存在していても構いません。COM インフラストラクチャでスレッド化、再入、プロセス間コールが処理されるため、ロケーションの透過性が提供されます。つまり、COM オブジェクトの場所にかかわらず、クライアント コードは同じでよいということです。

COM の最も重要な側面は、インターフェイス プログラミングにあると考えらえます。インターフェイス プログラミングは多くの言語に共通の技法であり、それによってコードは "実装" **ではなくオブジェクトの "動作" **に集中できるようになります。インターフェイスは動作を表す関連メソッドの集合です。COM オブジェクトは複数のインターフェイスを実装できるので、さまざまなタイプの動作を使用できます。COM オブジェクトへのアクセスは、すべてインターフェイス ポインターを通じて行います。

すべての COM オブジェクトが IUnknown というインターフェイスを実装する必要があります。また、COM インターフェイスはすべて IUnknown から派生 (つまり IUnknown インターフェイスのメソッドを実装) する必要があります。IUnknown インターフェイスには 3 つのメソッドがあり、そのうち 2 つは IUnknown::AddRefIUnknown::Release メソッドであり、3 番目のメソッドは QueryInterface と呼ばれるものです。IUnknown::AddRef メソッドと IUnknown::Release メソッドは、参照カウントに使用されます。COM のルールでは、インターフェイス ポインターがコピーされるたびに (COM オブジェクトの起動時に初めてポインターが返される場合も含めて)、IUnknown::AddRef メソッドを呼び出し、参照カウントを増分する必要があります。またこのルールには、コードが COM インターフェイス ポインターを使い終わった時点で IUnknown::Release メソッドを呼び出し、参照カウントを減分する必要があることも規定されています。参照カウントがゼロに達すると、そのインターフェイス ポインターのユーザーはもう存在しないので、通常 IUnknown::Release メソッドで COM オブジェクトを削除します。開発者は IUnknown::AddRef メソッドと IUnknown::Release メソッドによる参照カウントを利用して、オブジェクトの寿命を定義し、そのオブジェクトが不要になった時点で正しくクリーンアップが実行されるようにすることができます。IUnknown::QueryInterface メソッドは、C++ の reinterpret_cast<> 演算子に対応する COM のメソッドです。IUnknown::QueryInterface メソッドは、要求されたインターフェイスの COM 名 (128 ビット識別子) を取得し、オブジェクトがそのインターフェイスを実装している場合に、インターフェイスにポインターを返します。

つまり、COM オブジェクトは COM アパートメント内で実行する必要があります。アパートメントは、COM がスレッド化や再入などの側面を管理するメカニズムです。一部の Windows API には IUnknown インターフェイスから派生する COM に似たインターフェイスが備わっていますが、アクティブ化や COM アパートメント内での実行といった COM 機能は提供しません。DirectX オブジェクトや Windows シェル オブジェクトがこれに該当します。インターフェイス プログラミングは強力な技法であり、開発者が自身のアプリケーションで利用することができます。インターフェイス プログラミングを使用するために IUnknown インターフェイスを必ず使用しなければならないということはありませんが、一般的な標準のインターフェイスなので出発点として使用するのに適しています。

C++ での COM オブジェクトへのアクセス方法の詳細については、「C++ での Windows 対応プログラミングの学習: モジュール 2 Windows プログラムでの COM の使用 (英語)」を参照してください。

C++ COM キーワードの使用

COM オブジェクトにはインターフェイス ポインターを通じてアクセスします。インターフェイス ポインターは、メソッド ポインターのテーブルへのポインターです。したがって、インターフェイスの形式は C++ クラスの仮想メソッド テーブルと同じです。C++ を使ってインターフェイスを実装し、インターフェイス ポインターを使ってインターフェイスにアクセスするのは非常に理にかなった方法であるため、これは好都合です。開発者は一般的に、純粋仮想メソッドを使ってインターフェイスの抽象クラスを作成し、そのインターフェイス クラスからオブジェクト クラスを派生し、インターフェイス メソッドを実装します。Visual C++ では __interface キーワードが提供されています。これは基本的にすべてのメンバーが純粋仮想であるという動作を追加した structtypedef です。

IUnknown::QueryInterface のように、インターフェイス ポインターを返すメソッドは、オブジェクト ポインターをインターフェイス クラスへのポインターにキャストすることにより、適切なメソッド ポインターへのポインターを返すことができます。返されるポインターが仮想メソッド関数ポインター テーブルへのポインターであることが、C++ コンパイラによって保証されます。オブジェクトが複数のインターフェイスを実装する場合、複数の抽象インターフェイス クラスからクラスを派生することができます。

COM API (CoCreateInstanceEx) を使用して COM オブジェクトを作成する場合、オブジェクトとそのオブジェクトのインターフェイス名には COM 名、すなわち 128 ビットのグローバル一意識別子 (GUID) を使用する必要があります。GUID は不恰好な構造なので、Visual C++ コンパイラは、コードの読み書きを容易にする演算子とクラス修飾子を提供しています。 __declspec(uuid) クラス修飾子を使用すると、GUID を一種のタグとしてクラスに添付することができ、後から __uuidof 演算子を使って読み取ることができます。C++ コンパイラも COM 関数も、オブジェクトからこの GUID タグを自動的に読み取ることはできませんが、コードからは可能です。ただし、__declspec(uuid) を使用して C++ クラスに GUID を添付しても、C++ クラスが魔法のように COM オブジェクトになるわけではなく、さらに作業が必要になります。

リスト 1 Visual C++ キーワードによる IUnknown インターフェイスの定義

__declspec(uuid(“00000000-0000-0000-C000-000000000046”))
IUnknown
{
HRESULT QueryInterface(REFIID riid, void **ppvObject);
ULONG AddRef(void);
ULONG Release(void);
};

リスト 1 は、Visual C++ キーワードを使用した IUnknown インターフェイスの定義を示しています。リスト 2 は、標準の C++ キーワードを使用した同じコードであり、__interface が純粋仮想メソッドを使用する struct であることを具体的に示しています。

リスト 2 標準の C++ キーワードによる IUnknown インターフェイスの定義

struct IUnknown
{
virtual HRESULT QueryInterface(REFIID riid, void **ppvObject) = 0;
virtual ULONG AddRef(void) = 0;
virtual ULONG Release(void) = 0;
};

C++ での COM の使用

Hilo は COM サーバーではないため、このプロジェクトで COM 起動オブジェクトを実装する必要はありません。ただし、Hilo は DirectX オブジェクト上の COM に似たインターフェイスにアクセスします。COM インターフェイスを使用する際の注意点の 1 つは、参照カウントが確実に正しく処理されるようにすることです。前述したとおり、インターフェイス ポインターがコピーされるたびに必ず、IUnknown::AddRef を呼び出して、オブジェクトの参照カウントを増分しなければなりません。また、コードはインターフェイス ポインターを使い終わった時点で、IUnknown::Release を呼び出して参照カウントを減分する必要もあります。こうした参照カウントのルールが正しく守られないと、リソースが早期に解放されず、サーバーにメモリ リークの問題が起きることになります。

リソースのリーク問題への解決策として使用できるのが、スマート ポインターです。スマート ポインター クラスはインターフェイス ポインターをカプセル化し、–> 演算子を上書きして、インターフェイス ポインターへのアクセスを提供します。そのため、スマート ポインター オブジェクトはインターフェイス ポインターのように使用することができます。ただし、スマート ポインター クラスで最も重要な細部は、スマート ポインターがスコープ外になった時点で、インターフェイス ポインターに対する IUnknown::Release を呼び出すデストラクターがある点です。つまり、オブジェクトの参照カウントは、スマート ポインター オブジェクトの寿命によって制御されるということです。そのため、たとえばスタック変数としてスマート ポインターを作成した場合、COM オブジェクトの寿命は、その関数が実行されている期間に自動的に制限されます。return の呼び出し、例外、あるいは単に関数の終わりなど、どのような方法で関数が終了するかにかかわらず、カプセル化されたインターフェイス ポインターの IUnknown::Release が呼び出されます。

Hilo アプリケーションは、ComPtr<> というスマート ポインター クラスを提供しています。このクラスは、COM オブジェクトのインターフェイスか否かにかかわらず、IUnknown から派生する任意のインターフェイス ポインターに使用できます。リスト 3 は、Hilo Browser アプリケーションのエントリポイント関数を示しています。BrowserApplication クラス (この後の章で詳しく説明) は、アプリケーションのメイン ウィンドウを提供し、IWindowApplication というインターフェイスを実装します。これは COM インターフェイスではありませんが、IUnknown インターフェイスから派生するので、BrowserApplication クラスのインスタンスについては参照カウントを使用して寿命を管理できます。

リスト 3 で呼び出されるテンプレート メソッド SharedObject<>::Create は、new 演算子を使用して C++ ヒープに BrowserApplication クラスのインスタンスを作成し、パラメータで表されるインターフェイス用に、このオブジェクトに対して IUnknown::QueryInterface を呼び出します。

リスト 3 Hilo Browser アプリケーションのエントリポイント関数

int APIENTRY _tWinMain(HINSTANCE, HINSTANCE, LPTSTR, int)
{
ComPtr<IWindowApplication> mainApp;
HRESULT hr = SharedObject<BrowserApplication>::Create(&mainApp);
if (SUCCEEDED(hr))
   {
hr = mainApp->RunMessageLoop();
   }

return 0;
}

スタックで ComPtr<> 変数を宣言しているので、変数がスコープ外になった時点 (return ステートメントが呼び出された後) でデストラクターが呼び出され、それによってインターフェイス ポインターに対する IUnknown::Release が呼び出されます。さらに、それによって BrowserApplication オブジェクトのポインターに delete 演算子が呼び出されます。このコードから、IUnknown メソッドの実装は、COM オブジェクトを作成するコード (この場合、SharedObject<>::Create) の実装と密接に連携していることがわかります。これはオブジェクトの削除に使用するコードが、オブジェクトの作成に使用したコードに対応している必要があるためです。

IWindowApplication インターフェイスには RunMessageLoop というメソッドが含まれており、BrowserApplication クラスでのこのメソッドの実装では、これを使って標準の Windows メッセージ ループを提供しています。リスト 3 では、インターフェイス ポインターに RunMessageLoop メソッドを呼び出しているように見えますが、それは一面的な見方です。mainAppComPtr<> オブジェクトであるため、リスト 3 のコードは実際にはスマート ポインター クラスに対して演算子 –> を呼び出しており、それによってカプセル化されたインターフェイス ポインターに対してメソッドが呼び出されています。

まとめ

この記事では、Hilo アプリケーションの実装に使用したテクノロジが選ばれた根拠について説明しました。次の記事では、Hilo Browser アプリケーションにおけるユーザー エクスペリエンスの設計プロセスについて説明します。さらにその後の記事では、Hilo Common Library の設計と実装について説明します。

前へ | 次へ | ホーム