連載
.NET&Windows Vistaへ広がるDirectXの世界

第2回 DirectXマスターを目指すあなたが持つべき視点

NyaRuRu
Microsoft MVP Windows - DirectX(Jan 2004 - Dec 2006)
2006/07/22
Page1 Page2 Page3

2. COMと反復型開発

 ここまでの説明でご理解いただけるだろうが、DirectXを語る上でCOMを避けて通ることはできない。Direct3Dの10年は、COMを用いた反復型開発プロセスにより、新しいハードウェア機能をサポートしていくとともに、インターフェイス・セットを洗練させていった歴史である。そこで以降では、このCOMとそれによる反復型開発プロセスについてもう少し掘り下げて解説することにする。まずは、そもそもCOMとはどういうテクノロジなのかについて説明しよう。

COMとは?

 DirectXが登場する1995年までに、マイクロソフトはC++による開発を主眼に置いたコンポーネント技術に大きな投資を行っていた。それがCOMである。

 COMはプログラミング・モデルを重視する一方で、オブジェクト同士の契約にバイナリ規約を用いており、オーバーヘッドが非常に小さいという特徴があった*2。Windows 95では数多くのOS機能がCOMとして実装されている。パフォーマンスと生産性を両立させるために、COMテクノロジにより構築したコンポーネント(以降、COMコンポーネント)をC++で作成しVisual BasicなどによるRAD開発からそれを利用するというすみ分けが始まっていた。

*2 COMは呼び出し規約にいわゆるC++でいうところの“this-call”(=引数の数が変化しないC++メンバ関数に使用するデフォルトの呼び出し規約)を採用しており、実質的にC++の仮想関数呼び出しと同じコストしかかからない。COMの特徴は良くも悪くも、バイナリ結合に主眼を置いたことである。その見返りとしてパフォーマンスを得た代わりに、呼び出し先のコード内容を検証する手段がなく、コンポーネント単位で信頼するしかないという弱点もある。.NET Frameworkのコンポーネントはパフォーマンスと信頼性の両立を目指したものだが、コード実行の仕組みが非常に大がかりになり、専用のランタイム・ライブラリが必要になるため、COMのように従来のコード資産に手軽に導入するというわけにはいかなくなった。

 ここでCOMをベースにした基本的なプログラミング・モデルの特徴についておさらいをしておこう。

(1)CoCreateInstance APIで言語非依存なオブジェクト生成を行う。ファクトリ・パターンによって依存性の解決を実行時まで遅延させることができる。

(2)参照カウンタで言語非依存なオブジェクトの寿命管理とオブジェクト破棄を行う。

(3)メソッド呼び出し前後のメモリ・リソースなどの管理責任について、一貫したポリシーが存在する。従来のC/C++のポインタ渡しは、[In][Out][InOut]に分類され、より詳細に区別される。

(4)IUnknownインターフェイスのQueryInterface関数(=COMインターフェイスの関数)により、任意のインターフェイス間に言語非依存のキャスト操作が定義される。インターフェイスの型はGUIDによって、言語非依存な形で区別される。

(5)実装クラス「CMyService1」がIMyService1インターフェイスをサポートするとき、IMyService1インターフェイスとIMyService2インターフェイスをサポートする実装クラス「CMyService2」で、CMyService1クラスを置き換えることができる。これにより、従来のソフトウェア資産を生かしたまま、新しいサービス「IMyService2」を追加できる。

 このようなルールを守ったコンポーネント同士であれば、容易に実行時のバイナリ結合が可能になるというのがCOMのポイントである。

DirectXとCOM

 そして1995年、DirectXは登場当初からCOMコンポーネントとして提供されることとなった。しかし、Win16時代から徐々にマイクロソフトのプログラミング・モデルに親しんでいたデスクトップ・アプリケーション開発者とは異なり、DirectXのメイン・ターゲットであったゲーム開発者は、このころになってやっとWindows開発に移住してきた一団である。

 それまでハードウェアを完全に制御し、アセンブリ言語やC言語を好きに組み合わせて開発していた人々に対し、「これからWindows上でハードウェア機能を利用するには、カクカクシカジカのプログラミング・モデルに従ってください」という話をマイクロソフトは突き付けたわけだ。

 筆者もこのころの状況は当時のプログラミング雑誌や専門書などでしか知らないが、しばしばWindowsでのゲーム開発の解説の途中に「COMについて」というコラムでCOMの簡単な概要が別途紹介されていたのを覚えている。筆者の周囲では「DirectXはやたら難しいらしい」とか、「DirectXを使うにはC++の知識が必要らしい」といった会話が行われていたが、確かにいまになって初期のDirectXの仕様を見直してみると、COMというプログラミング・モデルにこだわらなければもっと平易なAPIセットになっていたのではなかろうかという気もしてくる。

 では、DirectXがCOMを採用したのは失敗だったかというと、筆者はそうは思わない。DirectXがCOMではなく従来のWin32的な関数ベースAPIであったなら、10年の間に8回もの頻繁なメジャー・バージョンアップはできなかったであろう。また、当初は複雑に入り組んでいたコンポーネント設計も、アップデートを繰り返すたびに着実に洗練されてきたことで、最近はコンポーネント開発の収穫期に入っている。

 というより、そもそも、DirectXがCOMを喰ってしまったようなのだ。

 実は、現在のDirectXのプログラミング・モデルは、COMのオリジナル・モデルとは異なっている。DirectXチームは、ある時点から、ゲーム開発に不要な部分をCOMから取り除き、標準のCOM APIへの依存をやめ独自実装で置き換えを行うようになり、まさに「COM for DirectX」とでもいうべきプログラミング・モデルを作り上げつつある。Direct3D 10でもこの方針は継続され、弱参照の導入などさらに若干の改良が行われる見込みである。以下、簡単にこの歴史を見てみよう。

「COMによるDirectX」から「DirectXのためのCOM」へ

 COMは登場後もさまざまな拡張が行われ、複数のスレッド・モデルのサポートやコンポーネントの集約、プロセス間通信や分散オブジェクトといった分野に適用範囲が拡大されてきたが、それとともに仕様が肥大化し、手続きが煩雑になることでかえって生産性を下げてしまう局面も見られるようになってきた。

 DirectXチームの面白いところは、汎用化・複雑化するCOM技術に対し、むしろにDirectX開発に不要な機能をどんどん削除していく方向にかじを切ったところにある。このことを最も端的に表す変化がDirectX 6〜8の間に起きている。

 次の表1は、Direct3Dオブジェクト(=Direct3Dに関係するすべてのオブジェクト生成の起点になるファクトリ・オブジェクト)がCoCreateInstance APIで作成可能かということと、生成されたオブジェクトがどのインターフェイスをサポートしているかを、DirectXのバージョンごとに示したものである。

表1 DirectXのバージョンと、CoCreateInstance APIの対応状況、インターフェイスの互換性
DirectX 7には、その新機能を使うためのコンポーネントと、DirectX 6以前をサポートするためのコンポーネントが含まれている。

 DirectX 2から6までは、上に挙げたCOMをベースにした基本的なプログラミング・モデルの特徴である(1)と(5)が強く意識されていることに気付くだろう。DirectXのバージョンが新しくなると、古いインターフェイスを継続サポートしつつ、新機能を含んだ新しいインターフェイスもサポートしたコンポーネントが導入されている。

 しかしDirectX 7では、それまでと異なり、DirectX 7の新機能を使うためのコンポーネントと、DirectX 6以前をサポートするためのコンポーネントが分離された。さらにDirectX 8では、DirectX 8のみをサポートするコンポーネントが追加されるとともに、プラットフォーム標準のCoCreateInstance APIではなく、DirectX DLLのエクスポート関数がインスタンス生成の唯一の手段となった。

 これこそが、COMという本来世界が持っていた可能性から、DirectXが行った取捨選択である。その結果、C++によるDirectX開発は次のようなプログラミング・モデルに変化した(太字は変化したプログラミング・モデルの特徴を示す)。

(I)C++標準のオブジェクト生成方式の代わりに、DirectXの世界で完結したオブジェクト生成の仕組みを導入する。

(II)参照カウントでオブジェクトの寿命管理とオブジェクト破棄を行う。

(III)メソッド呼び出し前後のメモリ・リソースなどの管理責任について、一貫したポリシーが存在する。従来のC/C++のポインタ渡しは、[In][Out][InOut]に分類され、より詳細に区別される。

(IV)インターフェイス指向のプログラミングを行い、基本的にインターフェイスは単継承とする(C++のアップキャストが使用できる)*3。ダウンキャストはC++のRTTI(=実行時型情報)ではなくIUnknown::QueryInterface関数を使用する。

(V)メジャー・アップデートでインターフェイスを一新する。複数のバージョンをサポートするオブジェクトを作らず、オブジェクト間のもつれ合いを少ない状態に保つことを優先する。

*3 ただしDirect3D 10では、IUnknown::QueryInterface関数を積極的に利用する方向に再び変化するようである。

 (I)は、実際にはDirectXランタイム・ライブラリDLLのエクスポート関数によって最初のファクトリ・クラスが生成され、そのほかのインスタンスはファクトリ・クラスを経由することで実現されている。そのため、このDLLを見掛け上置き換えたり、エクスポート関数呼び出しをフックしたりすることで、プロキシ・オブジェクトを返し、DirectXのAPI呼び出しをフックすることができる。この手法は、DirectX SDKに付属するプロファイラである「PIX for Windows」で使用されている。

 (II)(III)のように、従来のCOMテクノロジで標準的に使用されているオブジェクトの寿命管理方式を使用することで、各APIのドキュメントにこまごまとリソース破棄の責任について明記する必要がなくなっている。

 (IV)(V)は、インターフェイス・セットのシンプルさと関係している。一般に、新旧のインターフェイスを同時にサポートするような高機能なコンポーネントを作成しようとすると、新旧のインターフェイスを同時に使用した場合の動作仕様が複雑化しやすく、必然的に実装も難しくなる。

 無理をしてこのような互換性を維持しようとすると、新しいインターフェイス・セットは古いインターフェイスのモデリングを踏襲したものになりがちで、より明確な分類が後から分かってもそれを採用しにくい。そこでむしろ、アップデート時にインターフェイス・セットをシンプル化することを優先し、古いインターフェイス・セットとのソース・コード・レベルでの互換性を捨てたのが(IV)(V)の方針といえる。

 実際、メジャー・アップデート時のソース・コード・レベルでの互換性を捨てたDirectX 7以降、Direct3Dのインターフェイス・セットは急激に分かりやすく使いやすいものに変化している。

 最も変化が大きかったのはDirectX 7とDirectX 8の間であり、それまでDirectDrawのインターフェイス・セットを併用していたプログラミング・モデルを一新し、Direct3Dのインターフェイス・セットのみで3Dデバイスの列挙や初期化、描画処理が完結できるように変更が行われた。この変更は当時「DirectDrawとDirect3Dの融合」と伝えられ、コンポーネントの名称もDirectX Graphicsと改められた。*4

*4 ただし、インターフェイス名は、慣例に従いIDirect3D8となっていた。

 しかし、第1回の記事のコマンド・バッファの項で説明したとおり、DirectX 7とDirectX 8の間でドライバ・モデルは変化しておらず、従って内部実装のレベルでDirectDrawとDirect3Dが融合したという事実はない。

 これはつまり、DirectX 7以降のランタイム・ライブラリは内部でDirectDraw依存コードを隠ぺいし、Direct3Dの方法論でデザインされた新しいインターフェイス・セットに翻訳してくれているということを意味する。Direct3D 8やDirect3D 9では、このようにランタイム・ライブラリがドライバ層のレガシーな構造を隠ぺいしてくれるおかげで、DirectDrawの作法を意識する必要がなくなり、シンプルなAPIセットを使用してアプリケーション開発を行えるのだ。


 INDEX
  .NET&Windows Vistaへ広がるDirectXの世界
  第2回 DirectXマスターを目指すあなたが持つべき視点
    1.安定性に基づく分類
  2.COMと反復型開発
    3.DirectX開発の未来
 
インデックス・ページヘ  「.NET&Windows Vistaへ広がるDirectXの世界」


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Insider.NET 記事ランキング

本日 月間