UNIX 上での COM コンポーネントの作成
Christian Gross
euSoft
1998 年 7 月
はじめに
Microsoft Windows 上でのComponent Object Model(COM) コンポーネントの開発は、よく知られているプロセスです。これはわれわれが慣れ親しんでいるプロセスであるといってもよいでしょう。このプロセスは、COM コンポーネントそのものの複雑さに応じて、単純にも複雑にもなります。コンポーネント間での通信を可能にしているのは、COM ランタイムです。これは、スレッド間、プロセス間、およびマシン間でのコンポーネント通信を可能にする「ミドルウェア」です。言い換えると、いつでも、どこでも、どんな方法でもアクセスが可能なのです。これは天国のように思えるかもしれません。しかし、Microsoft Windows 上でしか使えないという問題があり、この世界は Microsoft Windows だけから構成されているわけではありません。この世界は、さまざまな機能を持つさまざまなタイプのコンピュータが存在する異種環境なのです。コンピュータは他のコンピュータと相互運用を行う能力を持っていなくてはなりません。
時は流れ、万物は流転します。これは人間界にも当てはまる普遍の法則です。しかし、変化にはお金がかかります。したがって、問題の本質は、変化に経済的に対応するにはどうすればいいかということです。その 1 つの方法は、段階的な進化をたどる方法です。これは物事のエキサイティングな運び方とはいえないかもしれませんが、いつの時点でも最高のテクノロジを利用できるという利点があります。そして、これこそが UNIXやその他のプラットフォーム上の分散 COM(DCOM) の目的なのです。これにより、開発者はアプリケーションまたはコンポーネントを、異なるプラットフォーム間で適宜移動することができます。しかも、この変化の進行中も、DCOM が各種のシステム上でミドルウェアとして機能しているため、システムは運用を続けることができます。このように、DCOM を使った段階的な進化は、その多様なプラットフォームとの相互運用性のために、きわめて経済的に実現することができます。
この記事の目的は、UNIX 上の DCOM について説明することです。他のプラットフォームにも他のインプリメンテーションが存在しますが、この記事では UNIX に焦点を当てます。この記事では、読者が COM に関してある程度の知識を持っているものと仮定します。知識がない方は、ドキュメントの最後に、COM と DCOM に関する基本的な知識を得ることができる URL のリストがあるので参考にしてください。この記事では、まず DCOM の全体的なアーキテクチャの中での位置づけについて説明します。次に、DCOM onUNIX に関するいくつかの疑問に対する答えを示します。次のセクションでは、DCOM on UNIX のアーキテクチャについて説明し、DCOM on UNIX がどのようにインプリメントされているのか、また個々の要素が何に対応しているのかを詳しく示します。最後のセクションでは、DCOM on UNIX コンポーネントをインプリメントする方法の概略を示します。このセクションの内容は、一般的な側面だけを取り出せば、任意のプラットフォームに適用することができます。
この記事で扱う COM はサーバー サイド COM です。グラフィカル ユーザー インターフェイス (GUI) を使用するクライアント サイド COM コンポーネントを作成することも可能ですが、開発者が作成するほとんどの COM コンポーネントはサーバー ベースになるはずなので、この記事ではあえて触れません。この記事で焦点を当てる COM は分散 COM、あるいは DCOM と呼ばれるものです。
この記事を読むプログラマは、Microsoft Visual C++、ActiveX Template Library(ATL)、COM、および UNIX に関する知識を持っている必要があります。この記事で扱うソリューションは Software AG と MainSoft のものです。ソリューションはこれ以外にも存在しますが、いずれもまだ完成していません。
DCOM と UNIX に関するいくつかの疑問
COM と DCOM の違いは何か? DCOM は、別のマシン上にある別の COM オブジェクトを呼び出す COM です。この点を除けば、両者に違いはありません。次の図を見てください。

この例には、任意のオペレーティング システム上のクライアントが含まれています。このクライアントは、ネットワーク経由で動作する COM を通して、Solaris サーバーに対して呼び出しを行います。クライアントにとって、この呼び出しは位置には依存しません。呼び出しを行えば、それに対する応答は自動的に処理されます。このプロセスは透過的に実行されます。クライアントにとって、コンポーネントが Windows NT、Solaris、あるいはその他のプラットフォームのどこに存在しているかは関係ありません。要するに、COM コンポーネントであればそれでいいのです。
相互運用性とクロスプラットフォームのどちらがいいのか?
Windows 以外のプラットフォーム上で DCOM コードを書くときには、2 つのプログラミング スタイルがあります。1 つは、Windows プラットフォーム上で作成し、後に UNIX プラットフォーム用に再コンパイルできるような真のクロスプラットフォーム アプリケーションを書くというものです。このソリューションは非常に有効
です。
しかし、多くのクライアントは、このようなニーズを持っていません。UNIX ステーションを持っている多くの企業は、UNIX に対して行った投資を今後も活用したいと思っています。これは、データへのアクセスという問題ではありません。この問題は、Open Database Connectivity(ODBC) と OLE DB ですでに解決されています。真の問題は、UNIX ステーション上にかなりのビジネス コードが存在し、これに対して、Windowsクライアントから、分散オブジェクト テクノロジを通してアクセスしなければならないということです。つまり、最優先事項は、多様なプラットフォームとの相互運用性なのです。このため、Windows 以外のプラットフォーム上で DCOM コードの設計と開発を行うときには、相互運用性を念頭に置く必要があります。
DCOM プロトコルに関する詳しい説明はどこで得られるか?
この記事の焦点は、DCOM プロトコルとその機能の説明にはありません。Microsoft Web サイト www.microsoft.com/com/default.mspx には、Windows NT と Windows NT の間での通信という文脈で、DCOM プロトコルについて詳しく説明した一連の記事があります。
インターフェイスについての知識はどのように入手されるのか?
Windows 上の COM コンポーネントが UNIX 上でCOM コンポーネントを呼び出すとき、そのインターフェイスについての知識はどうやって入手されるのでしょうか? UNIX 上でコンパイルされた COM コンポーネントでは、オブジェクトのレイアウトや、変数の格納と参照の方法は同一ではなくなります。このことはクロスプラットフォームの COM 呼び出しを難しいものにするでしょう。
この問題の解決は、COM の動作そのものにあります。COM には、インターフェイス定義と、そのインターフェイスのインプリメンテーションがあります。この COM の基本的な構成が、クロスプラットフォームでの動作を可能にしているのです。インターフェイス定義はクロスプラットフォームであり、個々のプラットフォームに合わせて実現される中立的な定義なので、どこでも動作します。一方、インプリメンテーションはプラットフォーム固有なものであってかまいません。しかし、ここではまずインターフェイス定義について少し詳しく見てみましょう。Microsoft Interface Definition Language(MIDL) 環境でコンパイルされるタイプ ライブラリを使えば、任意のカスタム インターフェイスを世界に対して公開することができます。これは、すべての COM コンポーネントが、ローカルには自動的に公開されるということを意味しています。COM コンポーネントをネットワークの他の部分に公開するときには、DCOM デーモンが解決の処理を担当します。
プラットフォーム A とプラットフォーム B でデータの扱い方が違うと何が起こるのか? また、それはどのように解消されるのか?
プラットフォーム A が 16 ビット、プラットフォーム B が 32 ビット、プラットフォームCが 64 ビットだったとすると、何が起こるのでしょうか? また、プラットフォーム D が 32 ビットだが、整数を B と C とは異なる方法で格納している場合には? いずれの問題も、高水準言語を使用している開発者に対しては透過的に処理されます。ただし、その高水準言語がコンパイルされるときには、強制的に1 つの仕様に従ってコンパイルされます。つまり、1 つのプラットフォーム上で整数を保存したとして、これを別のプラットフォーム上で簡単に読み込むことはできません。
DCOM on UNIX のようなテクノロジでは、このことが問題となります。これを解決してくれるのがリモート プロシージャ コール(RPC) です。DCOM は分散コンピューティング環境(DCE) RPCのインプリメンテーションを使用します。RPC はこの問題に対処できるように作られており、変換を自動的に処理します。インターフェイス定義やプログラミング言語で特別に指定を行う必要はありません。すべてが自動的に処理されます。
問題が生じるのは、クライアントがカスタム データをサーバーに送信するときだけです。このとき、RPC 層は送信された情報を BLOB として扱い、変換を行いません。データは 1 バイトずつサーバーに送られるので、サーバーは適宜データの処理を行わなくてはなりません。可能ならば、カスタム マーシャリングを使用するのは避けるようにしてください。
Windows NT は NetBEUI を使用し、UNIX は TCP/IP を使用するが、問題はないのか?
Windows NT は複数のプロトコルを使用することができます。よく使われるのは、NetBIOS UserEnhanced User Interface(NetBEUI)、Internetwork Packet Exchange/Sequenced PacketExchange(IPX/SPX)、および伝送制御プロトコル/インターネット プロトコル (TCP/IP) です。Windows NT 同士での DCOM は、これらの任意のプロトコル上で動作します。しかし、UNIX は TCP/IP しか使用しません。問題は、UNIX マシンが Windows NT マシンや Windows 9X マシンと通信するときに起こります。この場合、UNIX マシンは TCP/IP を使用しなければなりません。このため、Windows 9X またはWindowsNT マシン上には TCP/IPがインストールされている必要があります。複数のプロトコルが使用されているときには、UNIX の COM 呼び出しが最適化されるように、次の操作を行ってください。
次のレジストリ キー
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Rpc\DCOM Protocols
で、ncacn_ip_tcp が最初の項目になっていることを確認します。
Unixと Windows NT ではセキュリティ システムが異なるが、セキュリティはどのように確保されるのか?
Windows NT プラットフォームからWindows NT 以外のプラットフォームに対して DCOM 呼び出しが行われるときには、セキュリティ モデルが変わります。UNIX プラットフォームにはアクセス制御リスト (ACL) は存在しませんし、メインフレームでのモデルもまた異なります。つまり、セキュリティの処理は複雑になります。
このリリースの DCOM on UNIX では、セキュリティを提供していないベンダも数社ありますし、カスタム セキュリティを提供しているベンダも数社あります。つまり、セキュリティに関する適切な概念は存在しないといえます。唯一の例外は、Software AG によるメインフレーム ポートです。
これは、セキュリティ モデルがまったくないという意味なのでしょうか? Windows NT version 5.0がリリースされるまでは、そうです。Windows NT version 5.0 ではKerberos がインプリメントされますが、この時点ですべての DCOM インプリメンテーションがこれをネイティブにサポートすることになります。では、現段階で、DCOM コンポーネントはセキュアであると見なすことはできるのでしょうか? 現段階では、COM コンポーネントをパブリックに公開せず、Windows NT サーバーの背後に隠すようなアーキテクチャを採用するべきです。
ネットワーク上でファイルを共有することは可能か?
COMでは複合ドキュメントを作成することができます。複合ドキュメントとは、COM オブジェクトが永続化されたものです。さて、複合ドキュメントをあるプラットフォームから別のプラットフォームに移動したときに、その複合ドキュメントを使用できるだろうかという疑問が生じます。その答えは、可能性はあるというものです。
実際には、その複合ドキュメントが標準的な COM のデータ型を使って作成されていたかどうかに依存します。もしそうであれば、データの読み込みと操作は何の問題もなく行えます。しかし、ドキュメントが特殊なデータ型を含んでいる場合には、特殊なデータ型の変換が正しく行われない可能性があります。この問題はRPC データ マーシャリングの問題と同じものです。このため、すべてのデータ型を COM のデータ型にすることをお勧めします。
Windows NT ステーション上のトランザクションをUNIX に移行することは可能か?
この問題の範囲を完全に理解するためには、トランザクションの動作を理解する必要があります。一般に、Microsoft Transaction Server(MTS) はトランザクション サービスであると思われています。しかし、これは必ずしも正しくはありません。真のトランザクション コーディネータは分散トランザクション コーディネータ(DTC) と呼ばれているものです。これはコミットとアボートのプロセスを処理しています。DTC は OLE トランザクション プロトコルを使って、トランザクションに関与しているすべてのリソースの調整を行います。さて、この世には XA Transaction Protocolリソースというものが存在します。このプロトコルを使うと、Windows プラットフォーム以外のプラットフォームで実行されるトランザクションを調整することができます。ただし、より大きな問題があります。MTS を使用することの利点は、COMメソッドの呼び出しとトランザクションの調整が暗黙のうちに行われるという点にあります。これらは 1 つのステップとして自動的に処理されます。しかし、現時点では、DCOM on UNIX を使った場合、DCOM 呼び出しとトランザクションの調整は暗黙のうちには行われないので、プログラミング レベルで処理を行う必要があります。
ファット クライアントとシン クライアントのどちらが必要か? それとも、その中間のクライアントか?
メディアでは、この問題は大問題として広く論じられてきました。ファット クライアントは悪いものであり、シンクライアントは良いものであるというような扱いです。これは、シン クライアントの方が管理と制御が簡単であるという発想に基づく考えです。ファット クライアントは、手作業でインストールするしかない大きなクライアント プログラムを必要とするから悪いというわけです。これは過去においては正しい考え方だったかもしれません。しかし、次の図を考えてみてください。

この図では、Internet Information Server(IIS) がActive Server Pages(ASP) を提供するために使用されています。ASP ページとは、ダイナミック HTML コードを含んでいるダイナミックなスクリプトです。ASP スクリプトからは、DCOM または COM を使ってコンポーネントを呼び出すことができます。この例では、コンポーネントは Microsoft Transaction Server コンテキストの中で動作するので、これらのビジネス オブジェクトが実行するアクションは、すべて MTS によって制御されます。コンポーネントからは、MicrosoftMessage Queue Server(MSMQ) やデータベースを呼び出すことができ、また DCOM を使って UNIX システム上のコンポーネントを呼び出すこともできます。アドオン ソフトウェアを使えば、UNIX コンポーネントは ODBC を通してOracleと通信したり、MSMQと通信したりすることができます。このように選択肢は無限に広がります。いずれにしても、このアーキテクチャを使えば、理想的な多層アプリケーションが作成できることは明らかでしょう。
ダイナミック HTML スクリプトを受け取るクライアントは、ユーザー インターフェイス体験を強化する COM コントロールへの参照を含むことができます。これらのコントロールは、クライアント側の機能に応じて、ファットであってもシンであってもかまいません。要点は、クライアントからはビジネス プロセス オブジェクト(Windows NT 上の COM と UNIX 上の COM のどちらでも可能)しか見えないようにアプリケーションを分割するということです。これこそが、UNIX 上に DCOM コンポーネントをインプリメントする理由なのです。
DCOM on UNIX の紹介
DCOM on UNIX を理解するための最初のステップは、ベンダが提供するドキュメントを読むことです。ドキュメントは各インプリメンテーションに固有の微妙な違いに触れているので、これらのドキュメントを読むこ
とが先決です。
DCOMアーキテクチャ
このセクションでは、DCOM の動作原理を示す基本的なアーキテクチャについて説明します。参考までに、他のプラットフォーム上での最初の DCOM インプリメンテーションは、Software AG によって提供されました。そのポートをもとに、Microsoft は他のさまざまなベンダによってインプリメントされたリファレンスを作成しました。しかし、いずれのバリエーションも、アーキテクチャ上の共通する部分を持っています。次の図を見てください。

上の図の多層構造は、DCOM のインプリメンテーションの仕組みを示しています。これらの部分には古いもの (midl) と新しいもの (libmutant) があります。移植を開始した際には、いくつかの点を検討する必要がありました。その移植は、UNIX をMicrosoft Win32に見せかけるようなものなのか、UNIX に COM の機能を与えるためのものなのか、ということです。最終的には、可能な場合には、UNIX を Win32 のように見せかけるという方針がとられました。これは、完全なクロスプラットフォーム性を実現するのではなく、すべてのシステム間である程度の類似性を持たせることを目標としたためです。
この図の下の方の部分については、特に詳しい説明は行いません。一番下にはオペレーティング システムがあります。これは、UNIX の任意のバージョン (Solaris、Digital UNIX、HP/UX など) でも、他の任意のオペレーティング システムであってもかまいません。
その次のレベルの右の方には、Software AG のソリューションに固有な 2 つのボックスがあります。Paulad(Private Authentication Layer) は、Windows 以外のマシンと Windows NT ドメインの間でのセキュリティを担当するデーモンです。誰かがコンポーネントを使用しようとすると、このデーモンはWindowsNT ドメインと通信を行い、使用を許可していいかどうかを確認します。この通信は暗号化され、WindowsNT プライマリ ドメイン コントローラ (PDC) に対して行われなくてはなりません。RPC プロセスはリモート プロシージャ コールを担当します。
libmutant と ntd
左側には "libmutant" と "ntd" という言葉を含んでいるボックスがあり、これがWin32 と等価であることを示しています。libmutantは、UNIX 上にWin32 を再現するUNIX 固有のライブラリです。このライブラリの関数は、スレッド処理やファイル操作など、視覚的な効果を持たないものに限定されており、ユーザー インターフェイスなしの Windows アプリケーションを作成することが可能です。UNIX 上で Windows アプリケーションを作成したい場合には、BristolかMainSoft のプロダクトを使用する方がいいでしょう。ntd は Win32(libmutant) サービスを提供するデーモンです。このデーモンは、Win32 を使って書かれたアプリケーションの前に起動しておく必要があります。このデーモンを起動するには、次のコマンドを入力します。
ntwopper ntd
次の問題は、この層が他のベンダのインプリメンテーションにも存在しているかということです。これは、そのベンダがすでに Win32 インプリメンテーションを持っていたかどうかに依存します。たとえば、MainSoft はすでに Win32 層を持っていたので、上記の層を統合せず、libmutant を独自のソフトウェアに置き換えました。このため、プログラマから見た違いはありません。また、ntwopper を起動する必要もないということになります。
RPCSS
Win32 層の上には、3 つのシステム共有ライブラリがあります。これらのライブラリは COM の基盤を構成しています。
RPCSS は、COM クラス キャッシュと ROT(Running Object Table) の処理を担当するライブラリです。ROT を使用すると、クライアントを、すでに実行中のオブジェクト インスタンスにバインドすることが可能になります。これは一般に効率性を高めるための手段です。呼び出しが着信すると、RPCSS はその呼び出しを受け取り、オブジェクトに変換します。一般には、レジストリを参照して、どのような処理を行うかを判断します。このデーモンのもう1 つの機能は、DCOM pinging サービスの提供です。たとえば、次のようなシナリオを考えてみましょう。クライアントがサーバー上にコンポーネントを作成します。クライアントは参照を保持し、しばらくした後に消滅します。サーバーはクライアントが消滅したことに気づかず、まだ参照が存在しているものと考えます。この時点で、サーバーは使用されていないのに、不要なリソースが消費されています。DCOM pingingサービスは、サーバーとクライアントの両方が存在していることを確認することで、このような事態を防ぎます。このデーモンが提供するもう1 つの機能は、OXID (オブジェクト エクスポータ ID) リゾルバ サービスです。OXID サービスの目的は、pinging サービスを処理し、OXID からローカル マシン上の文字列マッピングへのマッピングを提供することです。
このデーモンを起動するためには、rootになる必要があります。RPCSS はサービスを提供するためのポートを開くので、root 特権で実行しなければなりません。次のコマンドを入力します。
su
その後、パスワードを指定します。
ntwopper -nowait rpcss
他のベンダのインプリメンテーションでも、RPCSS はそれほど変更されていません。MainSoft のインプリメンテーションのケースでは、DCOM 呼び出しを受け取るために RPCSS デーモンを起動しておく必要があ
ります。
OLE32 とOLEAUT32
OLE32 とOLEAUT32 は、同名のダイナミック リンク ライブラリ (DLL) が、UNIX に共有オブジェクト ライブラリとして移植されたものです。これらは UNIX マシン上の COM インフラストラクチャを構成します。OLEAUT32 はオートメーション機能を提供します。
DCOM on UNIX のプログラミング
ここまでで、読者は DCOM on UNIX の動作と、これを使って行えることについて基本的な知識を得られたでしょう。次のステップでは、実際にアプリケーションを作成します。アプリケーションの作成そのものはそれほど難しくなく、問題はどのような手順を踏むかということです。DCOM on UNIX のソフトウェア開発キット (SDK) には、いくつかのヘッダーが含まれています。基本的なヘッダーは COM と Windows32 をベースにしたものですが、ATL 用に修正されたディストリビューションもあります。
筆者は 2 種類の UNIX インプリメンテーションで仕事をしたことがありますが、この 2 つのプラットフォームでは、C++ のインプリメンテーションがかなり異なるということに気づきました。Solaris SDK のドキュメントには、C++ コンパイラの制限のために、ATL を変更する必要があるという注意書きがあります。このことから、筆者は真のクロスプラットフォーム機能を実現することは不可能であることに気づきました。問題は、ATL でのテンプレートの使われ方から発しています。ATL はきわめて高度なテンプレートをいくつか含んでおり、このためにコンパイラが異常な動作を起こすのです。しかし、われわれは相互運用性の問題に取り組んでいるのに過ぎないので、この点は問題にはなりません。その他にも、ウィザードなしでの ATL の開発がいくぶん複雑になるという問題がありますが、この点はツール ベンダがこの種のウィザードの開発を進めているので、やがては解決されるでしょう。
そういうわけで、この記事の残りの部分では、UNIX 上の既存のアプリケーションを変換し、COM に対応させるという問題に焦点を当てます。対象とするアプリケーションは、Web 上で検索して見つけたフリーウェアです。アプリケーションの選択の際に、ソース コードを見ることはしませんでした。ソース コードの内容いかんによって、移植の容易さが変わるからです。オリジナルのアプリケーションは、コンソール用のメニュー形式の経理アプリケーションです。これはターミナル ドリブン アプリケーションに似ています。現在、世の中にはターミナル アプリケーションは多数存在していますが、ここでの主眼は、これらのアプリケーションを拡張する方法を示すことです。アプリケーションを実行した様子を次の図に示します。

このアプリケーションのユーザーは、メニュー上の特定の項目にナビゲートすることで、目的のロジックにアクセスします。
ステップ 1: どのような種類の相互運用性を実現するのか?
どのような種類の相互運用性が必要なのかを考えることが重要です。相互運用性には次の 3 つのタイプがあります。
- 同期。オブジェクトが呼び出されるとき、情報を要求したクライアントは、答えが返されるまで待機します。
- 非同期。オブジェクトが呼び出されるとき、クライアントは要求をサブミットした後に、以前のタスクに戻ります。求めている応答の種類に応じて、クライアントは答えをポーリングするか、何らかの非同期的なコールバックを起動します。
- 一括データ転送。クライアントが情報を要求するとき、情報の要求は一括フォーマットで行われます。クライアントには一括データが返され、クライアントからの要求はコアのデータではなく一括データに対して行われます。
この開発では DCOM on UNIX を使用するので、選択肢としては同期方式と一括データ転送方式の 2 つしかありません。この例ではどちらの方式も使用できるでしょう。実際には、スケーラビリティ、コスト、およびデータベースのクリープ並列性を考慮して、どちらが適しているかを決めることになります。同期方式は、アプリケーションのスケーラビリティが適切である場合に使用されます。また、コストが重要であるときにも一般に使用されます。同期的な COM コンポーネントは、UNIX マシン上にロジックが存在しており、その COM インターフェイスを「叩く」ようにして使用します。データが UNIX マシン上に置かれており、他の場所に移動することができない場合には、必ず同期方式を使用しなくてはなりません。
一括データ転送方式では、データは 1 つのマシンから別のマシンに、または複数のマシンに移動されます。この方式を採用するのは、既存のマシンに新しいコンポーネントを追加するのがきわめて複雑である場合、スケーラビリティに問題が生じる場合、データベースのクリープが小さい場合、またはバッチ化が可能である場合です。データベースのクリープとは、ビジネス プロセス全体を通して変化するデータの量のことです。並列性が高く、データが頻繁に変更される場合には、一括転送は適していません。
このサンプル プログラムでは、同期方式を使用しています。
ステップ 2: インターフェイスの決定
インターフェイスの定義は、おそらく開発者が下さなければならない決定のうちでも最も複雑なものです。ここでは、問題の迅速な解決と、将来も使用できるようなインターフェイスの設計の間でトレードオフが行われます。筆者の持論は、インターフェイスの設計に十分な焦点が当てられていないことが多いというものです。クラスの設計に関する本や理論はいくつもありますが、これらも十分ではありません。その理由は簡単です。レガシー アプリケーションは長年にわたって存続します。一方、現在のところ、何年にもわたって存在するオブジェクトはありません。オブジェクト自体が生まれて間もないということもあります。今日設計されているようなオブジェクトが、20 年後も使われているとしたらどうなるでしょうか? 何年にもわたって存在し続けるオブジェクトの設計に関して論じている書籍はめったにありません。
万能の答えはない
しかし、万能の答えはありません。そのときどきで最善の決定があるに過ぎません。筆者は、コンサルタントの仕事をしていて、この決定において「正解」がないことに気づくことがよくあります。問題はあるかもしれませんが、それが普通なのです。「簡単なことならば、すべての人がやっているだろう」という諺が頭に浮かびます。たとえば、いまもよく行われる議論の 1 つに、インターフェイスをどのように設計すべきかというものがあります。インターフェイスは、目前の問題を解決できるように設計すべきなのか、それとも将来に起こる状況にも対応できるように設計すべきなのか。その答えは簡単です。目前の問題を解決できるように設計し、同時に段階的な変更が可能なようにしておきます。その後、しばらく経ったら、それまでにわかった新たな知識をすべて取り入れて、リエンジニアリングを行います。ここでの鍵は、それが書き換えを意味するとしても、新たに学んだ知識をつねに活用するということです。これは簡単に聞こえるかもしれませんが、実際には難しいことです。そこで、段階的な変更を容易にしてくれる 3 つの技法、デカップリング、ヘルパオブジェクト、およびオブジェクト ラッパについて説明します。
デカップリング
COM は、その性質からいって、デカップリングを行うためのきわめて優れたメカニズムです。デカップリングとは、インターフェイスと何らかのインプリメンテーションを別々に定義するプロセスのことです。コンシューマは、オブジェクトを使用するときには、インプリメンテーションではなくインターフェイスを見ます。デカップリングの利点は、コンシューマがインプリメンテーションの種類を動的に決定できるという点にあります。このように、このステップは COM を使うようにした時点で自動的に実現されています。たとえば、現在はインターフェイスが UNIX マシン上で実行されているとしても、別のインプリメンテーションを Windows NT 上に置くことが可能です。
ヘルパ オブジェクト
class Person {
public:
Person();
void makePhoneCall( const Person &inpPerson);
private:
vector< PhoneCall> m_calls;
char m_name[ 255];
long m_age;
};
このコードは Person クラスを定義しており、この人は電話をかける能力を持っています。このクラスは、通話の相手を唯一のパラメータとして取ります。このクラス定義は、通話と人間を結び付けているという点で問題を含んでいます。通話の定義が更新されたら、Person クラスも更新する必要があるからです。では、次の改良された例を見てください。
class Person {
public:
Person();
private:
char m_name[ 255];
long m_age;
};
// Other file
class Person;
class PhoneCall {
public:
void makeCall( Person *p1, Person *p2);
private:
Person *m_caller;
Person *m_called;
};
この例では、通話を更新して、その通話に関与している 2 人の人間を参照するようにしています。PhoneCall は、実際にはアクションなので、普通はメソッドとして実現されるものであり、正統的なクラスではありません。この奇妙なクラスはヘルパ オブジェクトで、Person をポインタとして参照し、決してメソッドを参照しないので、クラスとしては軽量です。一方のクラスを再コンパイルしても、もう一方のクラスを再コンパイルする必要はありません。
オブジェクト ラッパ
このテクニックはレガシー アプリケーションが関与する状況でのみ有効です。オブジェクト ラッパの目的は、レガシー システムと多層システム間でオブジェクト指向 (OO) の層を作成することです。オブジェクト ラッパは有効なテクニックとして受け入れられています。しかし、ラッパを開発するときには、インターフェイスの設計を適切に行わなくてはなりません。
このインターフェイスは、オブジェクト ラッパの概念に基づいて設計されることになります。オブジェクトは長期にわたって存在し続ける可能性があるので、インターフェイスはヘルパ オブジェクトのデザインに基づいて設計されます。ここでの鍵は、下位のデータ格納テクニックが変わっても操作をカプセル化できるような、優れたビジネス オブジェクト インターフェイスを設計することです。最初のビジネス オブジェクトがどのようにインスタンス作成され、そのデータベースが何であるかは、ヘルパ オブジェクトの機能の一部として処理されます。このヘルパ オブジェクトは将来のいつかの時点で古くなり、新たなデータベースをベースにした新しいヘルパ オブジェクトがインプリメントされるということが最初から前提となっています。
優れたオブジェクト指向デザイナーは、データ オブジェクトにアクセスするための適切な階層を作りたいという誘惑にかられるでしょう。これは、既存のデータ アクセス階層が古くなっており、またデータ アクセスはほぼ確実に Structured Query Language(SQL) を使って行われるからです。そして、SQL 用のオブジェクト階層の設計方法は周知の事柄になっています。この場合には、階層を部分的にインプリメントしておいて、将来のいずれかの時点でメソッドをインプリメントすることになるでしょう。この方法の利点は、この階層に依存しているアプリケーションを変更する必要がまったくないということです。しかし、問題が1 つあります。変化はいつ起こるのでしょうか? また、次世代のデータベースがSQLをベースにするという保証はあるのでしょうか? オブジェクト指向データベースや Extensible MarkupLanguage(XML) データベースが使われるという可能性はないのでしょうか? これらの疑問に対する確固たる答えはありません。そのため、すでにわかっているものについては定義を行い、そうでないものについては一時的なソリューションを提供するというアプローチの方が良いのです。
IDL インターフェイスの定義
次のステップではインターフェイスを定義します。このステップは、これまで過小評価されてきました。ほとんどの人は、ウィザードを通してインターフェイスを定義し、それと同時にインプリメンテーションを定義することに慣れています。この種の解決策はインプリメンテーションが1 つしかない単純なコンポーネントでは問題ありませんが、クロスプラットフォーム シナリオやエンタプライズ シナリオには適していません。現時点では、インターフェイスの定義はインターフェイス定義言語 (IDL) によって行われます。次にインターフェイスの定義を示します。
[
object,
uuid(5644D34B-C939-11d1-948A-00A0247D759A),
pointer_default(unique),
local,
version(1.0)
]
interface IUserRecords : IUnknown
{
void getValue( BSTR column, [out, retval]BSTR *output);
void setValue( BSTR column, BSTR input);
void addRecord( int year, int month, int day, BSTR description,
double cashValue, double creditCard, double income);
void setCurrent( int record);
void searchDate( BSTR startDate, BSTR endDate);
void searchText( BSTR text);
}
[
object,
uuid(5644D34C-C939-11d1-948A-00A0247D759A),
pointer_default(unique),
local,
version(1.0)
]
interface IHelperDatabase : IUnknown
{
HRESULT saveRecords();
HRESULT loadRecords([out, retval]IUnknown *retRecords);
HRESULT database([out, retval]BSTR *value);
HRESULT descriptions([out, retval]BSTR *value);
HRESULT numDescriptions([out, retval]long *value);
}
IUserRecords は、レガシー情報にアクセスするために使用される基本的なビジネス オブジェクトを定義するインターフェイスです。クライアントは、このインターフェイスを使用するときには、データのコレクションが存在するという仮定を行います。コレクションにデータがどのように追加されたかは、このインターフェイスの関知するところではありません。このインターフェイスは、コンソール アプリケーションが公開している機能を模倣しています。この機能はこれだけで十分なので、これ以上の拡張は行いませんでした。
ヘルパ オブジェクトは IHelperDatabase です。これは、ファイル ベースのメカニズムに対するレガシー インターフェイスです。このオブジェクトは、アプリケーションが必要とするコレクションの依存関係を処理します。IUserRecords はヘルパ オブジェクトに依存していないことに注意してください。また、ヘルパ オブジェクトも、IUnknownインターフェイスを使用しているので、IUserRecords には依存していません。これにより個々の要素のデカップリングが行われ、スクリプティング言語はコンポーネントを動的にバインドできるようになります。
ステップ3: サーバー コンポーネントのインプリメント
これは最後の、最も重要なステップです。前のステップでは、インターフェイスにレガシー アプリケーションに対するオブジェクト ラッパとしての役割を果たさせることに決めました。この決定の理由は、ソース コードを見るのが一番わかりやすいでしょう。次の main 関数を見てください。
void main(int argc, char *argv[])
{
opt_unit main_opt;
int menu_choice;
if(argc>1) strcpy(main_opt.options_file,argv[1]);
else strcpy(main_opt.options_file,".options");
if(!(main_opt.items_list=(char **)malloc(100)))
{printf("main_opt.items_list malloc error\n");exit(-1);}
read_options(main_opt.options_file,&main_opt);
menu_choice=main_menu();
while(menu_choice<8)
{
switch (menu_choice)
{
case 1:
new_data(&main_opt);
break;
case 2:
load_data(&main_opt);
break;
case 3:
save_data(&main_opt);
break;
case 4:
view_data(&main_opt);
break;
case 5:
edit_data(&main_opt);
break;
case 6:
options(&main_opt);
break;
case 7:
if(!(exit_check()))
{
printf("Bye for now :-)\n");
exit(0);
}
else break;
default:
break;
}
menu_choice=main_menu();
}
}
この main 関数ブロックは、すべての機能を一連の関数に委譲しているので、単純な内容になっています。
このため、環境にオブジェクト ラッパを簡単に追加することができます。オブジェクト メソッドは、自分の要求を個々のメソッドに委譲することができます。
シングルトン
この時点で、状況は難しくなることも、単純になることもあります。たとえば、上のアプリケーションで、情報を読み込み、グローバル変数に格納している場合を考えてみてください。これらのグローバル変数はアプリケーション全体から参照されています。このような場合には、シングルトンを作成する必要が生じます。シングルトンとは、1 つのインスタンスしか存在できないようなクラスのことです。これは一種のグローバル クラスということができるでしょう。シングルトンは、グローバル データを保持したいと考えるアプリケーションで必要となります。シングルトンの作成には2 つの方法があります。1 つは、その COM コンポーネントが1回しか実行されないことを指示するというものです。この方法でも問題はありませんが、これよりも優れた方法があります。
IHelperDatabase インターフェイスには、loadRecords という名前のメソッドがあります。このメソッドはIUserRecords インターフェイスを表現するインターフェイスを取得します。このメソッドに、呼び出されたときに、IUserRecords インターフェイスがまだインスタンス作成されていないことを確認するグローバルなチェックを行わせます。もしIUserRecords インターフェイスのインスタンスがすでに作成されていれば、そのアクティブ インターフェイスを返します。そうでなければ、インスタンスを新しく作成します。
IHelperDatabase インターフェイスはこのインターフェイスを作成する責任を負うので、インターフェイスの破棄についても責任を負わなくてはなりません。このため、一種のスーパーAddRefカウンタをインプリメントしています。IHelperDatabase インターフェイスの最後のインスタンスが破棄されると、グローバルなインスタンスであるIUserRecordsも破棄されます。このコンセプトをフリースレッド モデルでも機能させるためには、IUserRecords が IHelperDatabase の外ではインスタンス作成されないようにすることが重要です。このインプリメンテーションの具体的な詳細は、この記事の範囲を超えているので、ここでは扱いません。
総称的な COM インターフェイスに向けて
これで、インターフェイスが定義されました。UNIX MIDL でこれをコンパイルし、ヘッダー ファイルとIIDインプリメンテーション ファイルを作成することができます。次に、IUnknown インターフェイスについて、NT 上のMIDLが生成するものと、UNIX 上の MIDL が生成するものを少し詳しく見てみましょう。
MIDL_INTERFACE("00000000-0000-0000-C000-000000000046")
IUnknown
{
public:
BEGIN_INTERFACE
virtual HRESULT STDMETHODCALLTYPE QueryInterface(
/* [in] */ REFIID riid,
/* [iid_is][out] */ void __RPC_FAR *__RPC_FAR *ppvObject) = 0;
virtual ULONG STDMETHODCALLTYPE AddRef( void) = 0;
virtual ULONG STDMETHODCALLTYPE Release( void) = 0;
END_INTERFACE
};
NT 上のMIDL
DECLARE_INTERFACE( IUnknown )
{
INTERFACE_PROLOGUE( IUnknown )
STDMETHODEX_ ( HRESULT , QueryInterface, ( THIS_
/* [in] */ REFIID riid,
/* [iid_is][out] */ void **ppvObject) )
STDMETHODEX_ ( ULONG , AddRef, ( THIS) )
STDMETHODEX_ ( ULONG , Release, ( THIS) )
INTERFACE_EPILOGUE( IUnknown ) };
UNIX 上の MIDL
これらの定義を見ると、両者の間には大きな違いがあり、両者の間でインターフェイスを移動するのはきわめて難しいように思えるかもしれません。しかし、実際には、UNIX 上のマクロは NT 上と同じように解決されます。対応するものがない唯一の情報は、MIDL_INTERFACE マクロです。このマクロは、UNIX に対しては空として定義することができます。しかし、両方のプラットフォーム上のインターフェイスが基本的に同じものなのであれば、なぜマクロが必要なのでしょうか? その答えは、C言語のプログラマがMIDL for NT と MIDL for UNIX を比較すると、大きな違いがあるということにあります。C++コンパイラが生成する vtable は同じものですが、整数のサイズや順序に違いがあると、同一の vtable 表現を作成するのは困難です。こうした違いを、マクロがすべて吸収しているのです。
IClassFactory の定義
UNIX 上のすべての COM コンポーネントは実行可能ファイルとしてインプリメントされます。このため、起動された実行可能ファイルは、最初のステップとして、COM 層を初期化し、これに IClassFactoryを渡します。コードは直接 COM に対して書かれるので、個々のインターフェイスについて IClassFactory を定義する必要があります。もちろん、これはコンポーネントを外部的に作成できるという前提に立っています。これができない場合は、状況を単純にするために、IClassFactory を作成し、これを COM 層に登録するようにしてください。IClassFactory インプリメンテーションは総称的なものなので、次のテンプレートに置き換えることができます。
template< class impl>
class ClassFactoryImpl : public IClassFactory
{
…
// IClassFactory
STDMETHODIMP CreateInstance (LPUNKNOWN punkOuter, REFIID iid, void
**ppv) {
LPUNKNOWN punk;
HRESULT hr;
*ppv = NULL;
if (punkOuter != NULL)
return CLASS_E_NOAGGREGATION;
punk = new impl;
if (punk == NULL)
return E_OUTOFMEMORY;
hr = punk->QueryInterface(riid, ppv);
punk->Release();
return hr;
}
STDMETHODIMP LockServer (BOOL fLock) {
if (fLock)
SvcLock();
else
SvcUnlock();
return S_OK;
}
}
読みやすいように、テンプレートの一部を削除しています。このテンプレートには、テンプレートが作成すべきCOM コンポーネントが渡されます。CreateInstance が呼び出されると、テンプレートは正しいオブジェクトを作成します。LockServer メソッドでは SvcLock と SvcUnlock に対する呼び出しがあります。これ
らのメソッドは、それぞれ InterlockedIncrement と InterlockedDecrement を呼び出します。さて、Win32 アプリケーション プログラミング インターフェイス(API) を使用する開発者は、UNIX API も同時に使用できるのでしょうか? その答えは、制限付きで使用できるというものです。問題となるのはプロセス間通信 (IPC) とプロセス呼び出しで、COM とコンポーネントが互いに干渉する危険があります。
COM インプリメンテーションの定義
最後のステップである COM インプリメンテーションの定義は、最も単純なステップの 1 つです。インプリメントする必要があるインターフェイスは IUnknown の 1 つだけです。次にサンプルの IUnknown インプリメンテーションを示します。
STDMETHODIMP QueryInterface (REFIID iid, void **ppv) {
if (ppv == NULL)
return E_INVALIDARG;
if (riid == IID_IUnknown || riid == IID_IUserRecords) {
*ppv = (IUnknown *) this;
AddRef();
return S_OK;
}
*ppv = NULL;
return E_NOINTERFACE;
}
STDMETHODIMP_(ULONG) AddRef(void) {
return m_cRef++;
}
STDMETHODIMP_(ULONG) Release(void) {
if (--m_cRef == 0) {
delete this;
return 0;
}
return 1;
}
インプリメンテーションの残りの部分は、オブジェクト ラッパからの個々のインターフェイスの呼び出しです。
詳細については、付属のソース コードを参照してください。
ヒント
UNIX プラットフォームの各種のインプリメンテーションを使用するときには、いくつかの点に注意する必要があります。
多くのインプリメンテーションは、UNIX をターゲットとした開発に関して、それぞれ異なるアプローチを採用しています。Software AG のアプローチは、大部分の開発が UNIX 上で行われると仮定します。つまり、UNIX に移植できるコードはほんの一部に過ぎません。一方、MainSoft はこれとは違うアプローチを取っています。MainSoft は、中心的な開発環境が Windows NT であり、そこで開発されたアプリケーションをUNIX に移植するという前提に立ちます。このアプローチの違いは、全体的な開発アーキテクチャを定義します。では、個々の開発者はどちらのアプローチを採用すればいいのでしょうか? 答えは簡単で、既存のコードがどれほどあるかによって決まります。Windows NT 用に変換するまでの間、アプリケーションのためのブリッジを提供するのに過ぎないのであれば、Software AG のアプローチが適しています。しかし、多様なプラットフォームに分散しなければならないアプリケーションや COM コンポーネントを開発したい場合には、MainSoft のアプローチが適しています。
開発ツールは原始的なもので、すべての処理を明示的に行う必要があります。メイクファイルとエディタを、COM開発の方法論に合わせてカスタマイズしなければならないでしょう。オーストリアの Take Five という
会社は、Sniff という名前の開発ツールを作成しました。この開発環境は、いくぶん手を入れる必要がありますが、UNIX 環境ではきわめて有用です。このツールによって開発作業が単純になります。しかし、本当に快適に COM の開発を行える開発環境は、Windows NT 以外にはありません。その場合には、MainSoft のソリューションを検討することをお勧めします。
個々の環境を理解するようにしてください。他のプラットフォーム上でもかなりの開発作業を行う必要があるので、プラットフォームを理解しておく必要があります。1 ステップでコンパイルが行える MainSoft のソリューションでも、プラットフォームに関する知識が要求されます。この知識は、すべてがうまく行っているときには不要ですが、いったん何かがおかしくなったときに必要となります。すべてのテクノロジが、オペレーティング システム上に置かれた層であることを忘れないようにしてください。オペレーティング システムがスレッドをネイティブにサポートしていなければ、サンキング層が開発されます。たとえば、Linux の pthread のケースを思い出してください。pthreadはプロセスをforkして、スレッドのように見せかけます。このため、いくつかのプログラミング テクニックは使用できず、それを理解するためにはプラットフォームそのものを理解する必要があります。筆者の経験では、プログラミング レベルでの問題が最も少なかったのは Solaris でした。その他のプラットフォームは、それぞれ何らかの問題を抱えています。
コンパイラを理解するようにしてください。ATL はさまざまなプラットフォームに移植されましたが、問題も数多く存在します。ATL はきわめて高度な高性能テンプレート ライブラリです。C++ テンプレートを標準的に使用しているのにもかかわらず、各種のプラットフォーム上の多くのコンパイラは、この複雑さに対応することができません。このため、多くの DCOM インプリメンテーションは、その機能に制限を課しています。この点が問題になるのであれば、プレーンなCOMコードを使用することをお勧めします。特定のプラットフォームに依存する特殊なトリックを使用している COM コンポーネントは、後々のトラブルの原因になることがあ
ります。
まとめ
これで、Windows 以外のプラットフォーム上での COM コンポーネントの開発に関する説明は終わりです。この作業は、一定の範囲内で行っているうちは単純です。しかし境界を越えたとたんに、物事はきわめて複雑になり、さまざまな問題が発生する可能性があります。しかし、NT オペレーティング システムとそのサービスが改良されていくにつれ、Windows と他のプラットフォームの間の相互運用は、より簡単に実現できるようになるでしょう。
関連情報
COM:
- http://www.microsoft.com/com/
- http://www.microsoft.com/oledev/
- http://msdn.microsoft.com/
Software AG: http://www.sagus.com/ (インストレーション方法とヒント/トリックについては、EntireXDCOM, Dcomdoc.txt を参照してください)。
MainSoft: http://www.MainSoft.com/ (MainSoft は、UNIX 上の Windows のインプリメンテーションを専門としています。同社のライブラリは Microsoft Internet Explorer の移植に使用されました)。