Dr. GUI と COM オートメーション、第 1 部

1999 年 2 月 10 日

Contents

Dr. GUI のビットとバイト
今までの復習、これからの予定
オートメーション:今度は何かまったく違うもの
やってみよう!
今までの復習、これからの予定

Dr. GUI のビットとバイト

新年だというのに抱負も予測もない

Dr. GUI はここで、来たるべき年の抱負や予測を提示して新しい年を始めるという、コラムニストの尊ぶべき伝統とはきっぱり縁を切ることにします。

名医は「高値すぎる」という理由で Amazon.com を 100 ドルでは絶対に買わないと言いましたが、その後、同社の株は 3 分割された後なのにもかかわらず、200 ドル近くにまでなっています(分割していなければほぼ 600 ドルということになります!)。そんなミスを犯してしまった後では、名医はどんな予測も抱負も語る気が起きません。

Dr. GUI はまだ Amazon の株価には不安感を抱いていますが、誤解しないように。Amazon はすばらしい会社です。彼らの Web サイトには目を見張るものがあります。彼らが提供する顧客サービスは、Nordstrom のサービスさえ無愛想に見えてしまうほどのもので、全員が力を合わせる方法をよく知っています。同社の CEO である Jeff Bezos を含む全員が、このクリスマス商戦を乗り切るために倉庫で注文品のパック詰めをしていました。そこで結局 Dr. GUI は、1 つだけ予測をすることにしました。Amazon.com は大成功するでしょう。彼らは私たちの多くが学ぶところのある会社だと思います。

Visual C++ 6.0 は最高

Dr. GUI は自慢げにするのは嫌いですが(そんなことはしませんけどね)、PC Magazine の 1 月 19 日号(http://www.zdnet.com/pcmag/pctech/content/18/02/tf1802.001.html)の Richard V. Dragan が書いた Visual C++ 6.0 のレビューをぜひ読んでほしいと思います。まだ VC++ 6.0 を使っていないなら、このレビューを読んだ後にはよだれを垂らしてほしがること請け合いです(お願いですからナプキンを忘れないで!)。

Visual Studio をすでに持っている方は、現在 Service Pack 2 を https://msdn.microsoft.com/vstudio/sp/vs6sp3/ から入手できます。ここから直接ダウンロードするか、CD-ROM で入手できます。自動的に送られてきている人もいると思います(Web サイトでご確認ください)。

C++ をもっと上手に使いたい?

Dr. GUI は、Scott Meyers による『Effective C++』と『More Effective C++』が現在、『Effective C++ CD』という単独の CD-ROM タイトルとして入手できることを知りました。もちろん、これを読むにはコンピュータ(Mac、UNIX、さらに Windows 上でさえも動作します)が必要ですが、このすばらしい情報が全部をオンラインにあるので、コードをカット&ペーストして機能を簡単に試すことができます!また CD-ROM の参照も簡単です。Scott はリンクできるすべての段落にアンカーを追加したのです。最後に、価格も妥当です。29.95 ドルで、紙製の本のどちらか一方を買うよりも安いのです(でも、読むのには紙製の本もほしい、でしょう?)。さらに http://meyerscd.awl.com/ にはデモもあります。

2000 年問題の総括

Dr. GUI は 2000 年問題について書くという過ちを一度犯しました。一度あることは二度、三度…。

いずれにしても、名医はみなさんが読みたいであろう、いくつかの 2000 年問題に関する話題を見つけました。

Dave Barry の 2000 年問題に対する見方

Dr. GUI は、今だに Dave Barry のコンピュータとコンピューティングの捉え方に驚かされ、楽しまされ続けています。笑いが実際にすべての悩みに対する最良の薬だと知っている名医は、Miami Herald の 2000 年問題についてのコラム「Come the Millennium, Use the Stairs」(http://www.herald.com/archive/barry/1999/docs/jan3.htm)での、Dave の考えを心からお薦めします。Dave が今回、何を「地獄の邪悪な悪魔の卵」と呼んでいるか確かめましょう(ヒント:2000 年問題について書いている Dr. GUI のことではありません)。そして 2000 年 1 月 1 日の深夜のエレベータで何が起きるのかを知りましょう。

Microsoft の Year 2000 Resource Center

しかし私たちの中にはエレベータではなくソフトウェアを対象に働く人たちがいて、解決すべき本当の 2000 年問題を抱えています。もしあなたがその 1 人ならば、Microsoft の Year 2000 Resource Center(https://www.microsoft.com/technet/year2k/product/product.asp)をぜひご覧ください。DOS、Access、Excel から Actimates Barney、Arthur、DW まで、Microsoft 製品の 2000 年問題対応に関する情報が見つかります。このサイトには、対応状況を確認するための製品ごとの指示や、ホワイトペーパー、FAQ、ツール類などが含まれています。

ところで、Year 2000 Resource Center は CD-ROM でも入手できます。入手方法については、上記の URL をご覧ください。

VC++ の夏時間は 2001 年に遅れがでる

VC++ は最高かもしれませんが、完全ではありません。localtime() ルーチンは、2001 年の夏時間が 4 月 1 日ではなく 4 月 8 日から始まると考えています。つまり、このルーチンを使う時刻計算は、1 週間のあいだは 1 時間遅れているということです(Dr. GUI としては、ほかの機会にこれを遅刻の言い訳に使えれば、と思うのですが)。

自分のコードがこのバグに引っかかるかどうかは、localtime の呼び出しを検索すればすぐにわかります。時間に関係する Win32 API は影響ありません。MFC の時間関連ルーチンも、Win32 API の時間計算を使っているので影響はありません。しかし標準ライブラリ関数を使っているのなら(たとえば、UNIX システムからプログラムを移植した場合)、チェックして影響がないことを確認しましょう。

この春には、ライブラリを修正する Visual Studio Service Pack と、ライブラリの DLL である msvcrt.dll を修正する Windows のアップデートが提供される予定です。

Visual C++ の詳細については https://msdn.microsoft.com/visualc/ のサイトをご覧ください。

今までの復習、これからの予定

前回、ほかのインターフェイスから継承をするインターフェイスなど、複数のインターフェイスを持つさらに複雑な ATL オブジェクトを探求し、そのオブジェクトをさまざまな言語から使いました。今回と次回は、COM オートメーションの世界に深くもぐりこみます。名医はこの話題については、短いコラム 1 つで IDispatch したい(さっさと終らせたい)と思っていたのですが、どうやら少なくとも 2 回はかかりそうです。でも安心してください。Brockschmidt はこの話題に 100 ページ以上も費やしましたが、私たちは、そのはるか以前に終っているはずです。今回は、オートメーション(IDispatch)呼び出しを行う方法と、オートメーション オブジェクトがそれらを処理するために何をするべきかについてお話しします。次回は、オートメーションに使われる特別な COM データ型についてお話し、デュアル インターフェイスを探求します。

オートメーション:今度は何かまったく違うもの

オートメーション(従来、OLE オートメーションとして知られていました)は、これまで見てきた標準の COM 関数テーブル インターフェイスとはまったく違う方法で、クライアントからサーバーを呼び出します。

オートメーションは標準の COM インターフェイスである IDispatch を使って、オブジェクトのオートメーション インターフェイスにアクセスします。したがって、IDispatch を実装するオブジェクトはすべてオートメーションを実装すると言えます。前回登場した 2 つの ATL オブジェクトはどちらもデュアル インターフェイスを使っています。カスタム インターフェイスに対応する IDispatch インターフェイスが ATL と COM によって提供されました。オートメーション インターフェイスの実装は、ATL によって簡単になりましたが(正しいラジオ ボタンを選ぶことほど簡単なことはありませんよね?)、それでも背後で何が行われているのかを理解するのはよいことです。そして、それこそが今回の記事の内容なのです。

なぜオートメーションなのか?

オートメーションはもともと、アプリケーション(Word や Excel など)がスクリプティング言語を含むほかのアプリケーションに機能を公開するための方法として開発されました。その目的は、オートメーション クライアントにできるだけ負担をかけずに、プロパティにアクセスしたりメソッドを呼び出したりするシンプルな手段を提供することと、アクセス対象オブジェクトの型に関する情報がなくても呼び出しを行えるようにすることでした。

インターフェイス用の C++ ヘッダで型情報を調べたり、関数テーブルのメソッドのオフセットを調べたり、そして特に、メソッドを正しく呼び出せるように正しい C++ スタック フレームを設定したりするのは簡単なことではありません。これらはすべて、テキスト ベースのインタープリタ言語ではとりわけ難しいのです。

すべてのスクリプティング言語でこの難しいプログラミングをしなければならなかったとしたら、わずかな人しか COM オブジェクトにアクセスできなかったでしょう。しかしオートメーションによって、オブジェクトは単純化されたオートメーション インターフェイスを提供できるので、スクリプティング言語を使う開発者は IDispatch といくつかの COM API を覚えるだけで済むのです。

Visual Basic の最初の 32 ビットバージョンはオートメーションを使って、16 ビットの Visual Basic の VBX コントロールを置き換えた OLE コントロール(今は ActiveX コントロールと呼ばれています)にアクセスしました。Visual Basic は今でもオートメーションを使ってコントロールのプロパティやメソッドにアクセスできますが、もっと最近のバージョンでは標準の COM 関数テーブル インターフェイスも使えます。以前のコラムで作成したサンプルはすべて事前バインドされた関数テーブル インターフェイスを使っていて、オートメーション インターフェイスよりも高いパフォーマンスを提供します。今回作成するサンプルではオートメーション インターフェイスを使います。

Visual Basic for Applications、VBScript、J/Script のようなスクリプティング言語は、オートメーションだけを使います。したがって、オブジェクトをスクリプティング言語からも使えるようにしたい場合は、オートメーション インターフェイスを実装する必要があります。

オブジェクトとプロパティとメソッド、ああ!

オートメーションの世界には 3 つの主要な概念があります。オブジェクトはもっとも重要な概念です。各オブジェクトはプロパティとメソッドを公開します。

図 1:オートメーション オブジェクトのプロパティとメソッド

これを、オブジェクトでなくインターフェイスが主体で、プロパティが存在せず、複数のメソッドを含む複数のインターフェイスを各オブジェクトが持てる、より複雑な COM の世界と比べてみてください。

図 2:COM オブジェクト、インターフェイス、メソッド(ラベルなしの IUnknown を含む)

オートメーションのプロパティは C++ のデータ メンバかインスタンス データ(属性とも呼ばれます)に相当し、メソッドは C++ のメンバ関数に相当します。インターフェイスという独立の概念がありません。オブジェクトにあるオートメーション インターフェイスは 1 つだけです。COM インターフェイスにはプロパティの概念もないことに注意してください。あるのはメソッドだけです(ただし get/set メソッドの組み合わせでプロパティをシミュレートできます)。

オートメーション オブジェクトはどのように作られるのか?

オートメーション オブジェクトは簡単な操作で作成できます。ここでは例として Visual Basic を使いますが、オートメーション対応のどの言語でも手順はほとんど同じです。

Visual Basic では、まず Object 変数を作ります。

  Dim Beeper as Object

それからそれが特定のオブジェクトを参照するように設定します。

  Set Beeper = CreateObject("BeepCntMod.BeepCnt")

このケースでは、BeepCnt オブジェクトを作成しました(最初のATLの記事を参照してください)。

このようにしたら、すぐ後で紹介するように、オブジェクトのメソッドを呼び出してそのプロパティを操作できます。

しかしその前に、Visual Basic(に限らず、ほかのどんなオートメーション クライアントも)が見えないところで実際何をしなければならないのかについてお話ししましょう。

私たちはすでに、IDispatch 標準 COM インターフェイスを通してオートメーション オブジェクトにアクセスすることを知っています。したがって、上で示した DIM ステートメントは、まもなく作成するオブジェクトのための IDispatch ポインタを Visual Basic が格納できるように十分なメモリを確保します。

問題なのは CreateObject 呼び出しです。第一、GUID はどこでしょう?どうしたら CLSID 用の GUID なしでオブジェクトが作れるのでしょう?

オブジェクトの ProgID を使ってオブジェクトの型を参照できることを思い出してください。ProgID をキー名として使って、レジストリにキーを登録したことも思い出してください。このキーはサブキーとして CLSID を持っています。

COM には、渡された ProgID に基づいて CLSID を探し出す CLSIDFromProgID という関数があります。Visual Basic は、CreateObject に渡した文字列を使ってこの関数を呼び出します。このケースでは、Visual Basic は「BeepCntMod.BeepCnt」を渡します。CLSIDFromProgID はそのキーを探し出して、関連付けられている CLSID を返します(ところで、ProgID の最初の部分はモジュール名またはアプリケーション名で、2 番目の部分はそのモジュールまたはアプリケーション内のオブジェクトの名前です)。

この時点で Visual Basic は IDispatch インターフェイスを返してもらうために、おなじみの CoCreateInstanceEx を呼び出して CLSID を渡します。CoCreateInstanceEx が成功すると、VB は CoCreateInstanceEx から受け取った IDispatch ポインタを含んだオブジェクト変数を作り、それを私たちのオブジェクト変数に代入します。

オブジェクトが存在しない、あるいは IDispatch を実装していないなど、何らかの理由で作成が失敗すると、CreateObject 呼び出しが失敗します。

ご覧のように、Visual Basic(に限らず、ほかのどのオートメーション クライアントでも)のオーバーヘッドは最小です。オブジェクトを作成するために知っておくべきことは、2 つのシンプルな COM 関数だけです。

ではオートメーション プロパティとメソッドにはどのようにアクセスするのか?

オブジェクトにアクセスする Visual Basic のソース コードは以下のようになるでしょう。

  BC = Beeper.Count
Beeper.Count = 5
Beeper.Beep

この 3 つのステートメント、つまりプロパティのアクセス、プロパティの設定、メソッドの呼び出しは、IDispatch の次のたった 2 つのメソッドを使うだけで実現されています。GetIDsOfNamesInvoke の 2 つです。IDispatch::GetIDsOfNames は、メソッドまたはプロパティのテキストでの名前に結び付いている整数の ID を取得します。Visual Basic はこれを呼び出して、「Beep」に ID 1 が、「Count」に ID 2 が対応することを発見します。dispid と呼ばれるこれらの ID は、IDispatch::Invoke を呼び出すときに必要になります。

実際のオートメーションのプロパティとメソッドへのアクセスはすべて IDispatch::Invoke への呼び出しを通じて行われます。言いかえると、オートメーション オブジェクトにアクセスするには、オートメーション クライアントはいくつかのシンプルな COM 呼び出しを知っていればよいということです。実装に使う言語が C または C++ 以外ならば、ランタイム用にこれらの呼び出しをしてくれるヘルパーを書けばよいでしょう。したがって、どんなプログラムからでもオートメーションを簡単に使えます。

簡単かもしれませんが、重要なことです。IDispatch::Invoke は一群のパラメータを受け取りますが、それらをすべて正しく設定しなければなりません。もっとも重要なものを以下に示します。

  • dispid と呼ばれる整数の ID。アクセス対象プロパティまたはメソッドを指定します(プロパティまたはメソッドの名前を含む文字列を指定して GetIDsOfNames を呼び出すことでこれを取得します)。

  • パラメータの配列へのポインタを含む構造体(各パラメータは種別を表すタグとバリアントと呼ばれる共用体を含む構造体に格納されています)。

  • プロパティまたはメソッドをどうするか(プロパティの場合は、設定、取得、参照による設定、メソッドの場合は、呼び出し)を示すフラグ。

  • プロパティ取得とメソッドの戻り値用のパラメータです。これもバリアントです。

ああ、それから、メソッド、プロパティ、名前付きパラメータの名前、パラメータ値をローカライズしたい場合に備えて、InvokeGetIDsOfNames は両方ともロケール ID を受け取ります。

Invoke にはほかにも、オートメーション クライアントにエラー情報を返すためのパラメータが 2 つあります。完全に調べたことにして、今のところはこれらをスキップしましょう。

バリアントは 16 バイトで保存されます。最初の 2 バイトはタグで、バリアントの種別を表す数値です。次の 6 バイトはパディングです。最後の 8 バイトはバリアントの値です。値の形式はタグの値によります。C/C++ では、バリアントの値を共用体で表します。バリアントはほとんどの C++ のデータ型に加えて、ポインタ、配列、文字列、データ、通貨型のオブジェクトを保持できます。バリアントを含む COM のデータ型の完全な処置は、次回行います。

ではこれがカスタム インターフェイスよりも簡単なのはなぜなのか?

IDispatch::Invoke を通して呼び出しを行うために必要のない事柄に注目してください。

  • 関数テーブルのオフセットが不要—dispid を使います。これはオブジェクト自身に依頼して取得します。

  • C/C++ のパラメータ リストと呼び出し規約が不要—バリアントの配列を使います。

  • 上記の情報を提供する C/C++ のヘッダ ファイルが不要—ただしタイプ ライブラリはオプションです。

C/C++ を完全に除外できるということではありません。明らかに、4 つの呼び出しは C/C++ の呼び出し規約に従って行う必要があります。しかしオートメーション クライアントでは、心配する必要があるところはここだけです。

したがって、呼び出しを行うために必要なのは、オブジェクトへの IDispatch ポインタ、アクセス対象プロパティまたはメソッドの名前、そしてパラメータのリストだけです。

同じ理由で、スクリプティング言語を使って COM オブジェクトを書きたい場合、その言語のランタイムにとっては IDispatch だけ(ああ、それとオブジェクトの作成方法と)を実装する方が、無限に多様なカスタム インターフェイスの無数の細かい事柄を扱うよりも簡単です。

COM インターフェイスとオートメーションとの違い

前述のことだけからも、すぐにオートメーションが COM インターフェイスと異なる点がわかります。

  • オートメーション インターフェイスは、クライアントが dispid をキャッシュできるので動作中には変えるべきではありませんが、必ずしも不変ではありません。オートメーション インターフェイスは、特にメソッドが追加されるなど、オブジェクトのバージョンごとに変わるのがふつうです(メソッドを削除したりパラメータを変更したりすると、クライアントの既存のコードが動作しなくなることがあります)。

  • オートメーションのメソッド(およびプロパティ)は、さまざまなタイプを格納した可変長のパラメータ リストを受け取ることができます。実行時にパラメータを解析して、必要な型変換を行うのは IDispatch::Invoke の実装の仕事です (パラメータの変換が不可能な場合、オブジェクトの IDispatch::Invoke の実装は HRESULT エラーを返します)。

  • オートメーションのメソッドとプロパティのアクセスは実行時にバインドされます。つまり、実際にアクセスされるメソッドまたはプロパティの決定は、呼び出し時まで延期されます。

  • こうした実行時バインディングのおかげで、オートメーションのメソッドとプロパティは C++ よりも Smalltalk に近い形でポリモーフィックです。どのオブジェクトのどのメソッドまたはプロパティでもアクセスできます。不正な呼び出しを拒否するのはオブジェクトの責任です。たとえば、任意のオブジェクトの Print メソッドを呼び出すとします。Print という名前のメソッドを実装しているオブジェクトなら値を出力するでしょう。実装していないオブジェクトの呼び出しは失敗します。C++ では、Print を呼び出せるのは、オブジェクトのクラスが Print メソッドを定義している場合だけです。名前とパラメータの検査は実行時ではなくコンパイル時に行われるのです。

では、サンプルをいくつかいかが?

簡単な例として、今見てきた 3 つの呼び出しがどのように行われるのかを見てみましょう。

メソッドの呼び出し

Beep メソッドには渡すパラメータも戻り値もないので、まずこれを呼び出しましょう。呼び出しは以下のように書いたことを思い出してください。

  Beeper.Beep

最初に知る必要があることの 1 つは、Invoke に渡されるパラメータのポインタは、実際には DISPPARAMS 構造体へのポインタであるということです。この構造体は以下のように定義されています。

  typedef struct FARSTRUCT tagDISPPARAMS{
// Pointer to array of arguments, named and unnamed
VARIANTARG FAR* rgvarg;
// Array of Dispatch IDs of named arguments
DISPID FAR* rgdispidNamedArgs;
// Total number of arguments, named and unnamed
unsigned int cArgs;
// Number of named arguments.
unsigned int cNamedArgs;
} DISPPARAMS;

名前付き引数は使わないので、rgdispidNamedArgs は NULL で cNamedArgs はゼロになります。

このシンプルな呼び出しを行うコードは以下のようになります。

  DISPID dispid;
OLECHAR * szMember = "Beep";
// No parameters, so no array
DISPPARAMS dispparamsNoArgs = {NULL, NULL, 0, 0};

// pdisp is an IDispatch pointer
// to the Beeper object
hresult = pdisp->GetIDsOfNames(IID_NULL, &szMember, 1,
LOCALE_USER_DEFAULT, &dispid);
hresult = pdisp->Invoke(
dispid,
IID_NULL,
LOCALE_USER_DEFAULT,
DISPATCH_METHOD,
&dispparamsNoArgs, NULL, NULL, NULL);

まず GetIDsOfNames を呼び出して dispid を取得します。両方の呼び出しで指定している IID_NULL 値は、予約済みパラメータの値です。char ポインタの配列(この場合は、char ポインタ 1 つだけ)へのポインタ、配列内のポインタの数、ロケール(名前をローカライズしたい場合)、dispid の配列(これもポインタ 1 つだけ)へのポインタを渡します。呼び出しが戻ると、dispid には「Beep」に対応する dispid が入っています。

「Beep」の dispid を覚えておくようにすれば、GetIDsOfNames を 1 度呼び出すだけで済みます。

dispid を取得できたら Invoke を呼び出せます。

パラメータ リストが空であっても、最小限の DISPPARAMS 構造体を用意する必要があります。また、DISPATCH_METHOD を渡してメソッド呼び出しを行いたいことを指定しなければなりません。

これは単純に Beep() を呼び出すだけよりもずっと複雑です。オートメーションは C/C++ 以外のクライアントが使うには簡単ですが、直接呼び出しを行うよりも必ず遅くなります。しかし、サーバーが別のプロセスの中にあったり別のマシン上にあったりする場合には、オートメーション呼び出しを設定(そして実行)するためにかかる時間は些細なものになります。

プロパティの値の取得

プロパティの取得はメソッドの呼び出しとよく似ています。唯一の違いは、呼び出しが戻ってきたときに戻り値に注目するということです。

Visual Basic でプロパティの値を取得するコードは以下のようになります。

  BC = Beeper.Count

Invoke を呼び出す C++ のコードは以下のようになります。

  VARIANT varResult;
// No parameters, so no array
DISPPARAMS dispparamsNoArgs = {NULL, NULL, 0, 0};

// dispid set by call to GetIDsOfNames
// (omitted for brevity)

hresult = pdisp->Invoke(
dispid,
IID_NULL,
LOCALE_USER_DEFAULT,
DISPATCH_PROPERTYGET,
&dispparamsNoArgs, &varResult, NULL, NULL);

// Property's value stored in varResult

プロパティの取得(と設定)時に、インデックスや検索キーなどのパラメータを渡してパラメータ化されたプロパティ(もしくは「プロパティ配列」)を持つのは簡単です。

Visual Basic の文法では、プロパティの取得と、パラメータを受け取らないが戻り値のあるメソッドの呼び出しとの区別がつかないことに注目してください。つまり、BC = Beeper.Count という呼び出しが、Count というプロパティへのアクセスなのか、Count というメソッドの呼び出しなのかを、文法から見分けるのは不可能だということです。

その結果、オートメーション クライアントによっては、プロパティ アクセスのときに両方のフラグを渡すことがあります。たとえば、プロパティを取得する場合、Visual Basic はプロパティ アクセスまたはメソッド呼び出しのどちらをするのか区別できないため、DISPATCH_PROPERTYGET | DISPATCH_METHOD を渡します。このような場合も、オートメーション オブジェクトは、dispid がプロパティとメソッドのどちらを参照しているかに応じて正しい処理をしなければなりません。

プロパティの値の設定

プロパティの設定は、プロパティの取得と 3 つの点で異なります。

  • プロパティに設定する新しい値用のパラメータがあります。

  • このパラメータは DISPID_PROPERTYPUT という dispid を使って名前が付けられます

  • 戻り値のパラメータは無視されます。

プロパティの設定はメソッドの呼び出しとは異なり、特別な dispid を使って、プロパティの値として設定するパラメータの名前を付けます。

なぜこんな面倒なことをしなければならないのでしょう。プロパティをパラメータ化できることを思い出してください。プロパティの値のための特別な名前が決まっていれば、オートメーション オブジェクトはプロパティの値として設定するパラメータを簡単に判別できるようになるのです。

(名医は本当は名前付きパラメータの使用を避けたいのですが、これは絶対に必要なケースなのです)

Beeper オブジェクトの Count プロパティを設定する VB のコードを思い出してください。

  Beeper.Count = 5

Invoke を呼び出す C++ のコードは以下のようになります。

  // parameter structure
DISPPARAMS dispparams;
// one-element array of parameter names
DISPID mydispid[1] = { DISP_PROPERTYPUT };
// one-element array of parameters
VARIANTARG vararg[1];

dispparams.rgvarg = vararg; // 1-element array
VariantInit(&rgvarg[0]);
dispparams.rgvarg[0].vt = VT_I4;   // 32-bit integer
dispparams.rgvarg[0].iVal = 5;   // here's our 5!
dispparams.rgdispidNamedArgs = mydispid; // name array
dispparams.cArgs = 1;      // total args
dispparams.cNamedArgs = 1;   // named args

// dispid set by call to GetIDsOfNames
// (omitted for brevity)

hresult = pdisp->Invoke(
dispid,
IID_NULL,
LOCALE_USER_DEFAULT,
DISPATCH_PROPERTYPUT,
&dispparams, NULL, NULL, NULL);

このすべてのコードが、たった 1 つの小さな呼び出しのためにあるのです!

オートメーション呼び出しを行うのに必要なコードの量と、オートメーションが遅い理由がわかってきたと思います。パラメータの数がもっとあったとしたら、各バリアントを設定する以下の 3 行が、パラメータの数だけ繰り返されます。

  VariantInit(&rgvarg[0]);
dispparams.rgvarg[0].vt = VT_I4;   // 32-bit integer
dispparams.rgvarg[0].iVal = 5;   // here's our 5!

ステートメントの 1 つはバリアントを初期化し、1 つはその型を設定し、もう 1 つはその値を設定します。パラメータが名前付きならば、各パラメータの名前を rgdispidNamedArgs に設定するためのステートメントを追加する必要があります。そして数も正しく設定しなければなりません。

つまり、オートメーションは呼び出しのコード サイズを犠牲にして柔軟性を実現しているのです。犠牲にしているものがもう 1 つあります。渡すことのできるパラメータは、バリアントで表せるものだけなのです。この中でも最大の問題は、バリアントでは struct を表せないという点です。

バリアントについてはすぐ後で取り上げます。オートメーションにおける引数の引渡しの詳細については、自分のお気に入りの COM のリファレンスを参照してください。

独自に IDispatch::Invoke 呼び出しを書くって?ではこれを読んで…

自分で独自のスクリプティング言語を書いているか、オートメーション専用オブジェクトとやり取りをする C++ で書いたクライアントから直接 IDispatch::Invoke を呼び出すのでない限り、ここで取り上げる問題にぶつかることはないでしょう。

省略可能引数について読んでいたときに、Dr. GUI は不可解な状況を発見しました。IDispatch::Invoke を解説する COM のドキュメントには、Brockschmidt も言うように、省略する名前なしの引数ごとに、VT_ERROR というタグを持つバリアントを渡さなければならないと書いてあります。object.method(a,,c) のように、途中にある名前なしの引数を省略する場合、それは明らかに正しいことです。しかし、省略可能引数が最後にある場合はどうでしょう。タイプ ライブラリを使わないスクリプティング言語で、渡すべきダミー引数の数を知るにはどうすればいいのでしょう。答え:不可能です。

名医はこれを不思議に思ったので、COM チームの何人かに確認したところ、この件については文書が正確でないという結論に達しました(次に改訂されるときに修正されるでしょう)。

最後にある名前なしの省略可能引数を省略したい場合は、そのまま単に省略できるのです。COM で実装されている IDispatch(詳細は後述します)を使っているオブジェクトは、IDL で指定されているデュアル インターフェイス関数への引数を渡すことで、この状況を正しく扱います。ただし、落とし穴が 1 つだけあります。呼び出し対象オブジェクトが COM に頼らずに、IDispatch::Invoke を独自に実装している場合、すべての省略可能引数と既定値を持つ引数について、ダミー引数が渡されるものと想定して書かれている可能性があります。したがって、タイプ ライブラリ情報を取得してパラメータの数を知ることができるのなら、ダミー引数を渡せるように、そうするべきです。

オートメーション:サーバー側

メソッドの呼び出し、プロパティの設定と取得、戻り値のアクセス、パラメータ引渡しの方法について、すでに知りたい以上のことを見てきました。オートメーション オブジェクトはこれ全部をどのように扱うのでしょう。

一言で言うと、オブジェクトはこれ全部を、IDispatch::Invoke(そしてもちろん、GetIDsOfNames を含む IDispatch の残りと)を実装することによって扱います。しかしオブジェクトを実装しようとしていると想定しましょう。これらのメソッドをどのように実装したらよいのでしょう。

難しい方法での IDispatch の実装

もしメソッドに対して何のパラメータもなく(あるいはパラメータが 1 つだけで)、パラメータ化されたプロパティがないならば、自分自身で IDispatch::Invoke を実装するのは比較的簡単です。dispid と型の組み合わせごとに正しい関数を呼び出す switch ステートメントを用意するだけで済みます(呼び出しテーブルも使えます)。パラメータを適切な型に変換しなければなりませんが、VariantChangeType を呼び出せば、任意の型のバリアントを必要な任意の型に簡単に変換できます(変換が失敗した場合は、呼び出し元にエラーを返します)。

しかし、パラメータが複数ある場合の混乱は想像がつきます。そもそも、名前なしのパラメータは配列内で逆順に並んでいます。名前付きの場合、並び順は dispid の順番によって決まります。つまり、自分でそれらを整理しなければならないということです。しかし待ってください。それだけではありません。省略可能パラメータと、既定値を持つ省略可能パラメータをサポートしたい場合もあります。これ全部を判別するのは非常に面倒です。難しいだけでなく、間違える可能性も高いでしょう。その結果、処理できたはずの呼び出しに対してエラーを報告することになるでしょう。ユーザーの信頼を勝ち取る方法としては、あまりよい方法とは言えません。

もっと簡単な方法があるかって?当然です。

難しい方法はやめよう。COM にやらせよう!

サーバーが比較的シンプルな 2 つの要件を満たすならば、COM の組み込まれている IDispatch の実装を利用できます。要件は以下のとおりです。

  • オートメーション インターフェイスが純粋なディスパッチ インターフェイスでなく、(ATL が生成した)デュアル インターフェイスである必要があります。

  • COM がメソッドとプロパティについて知ることができるように、タイプ ライブラリを生成して利用できるようにする必要があります。

ATL は純粋なディスパッチ インターフェイスを受け取るためのサポート機能さえも持っていないので、選べるのはデュアル インターフェイス(オートメーションを含む)とカスタム インターフェイスだけです。デュアル インターフェイスと、同等のカスタム インターフェイスとの主な違いは、デュアル インターフェイスが IUnknown ではなく、IDispatch から派生する点です。つまり、デュアル インターフェイスは、QueryInterfaceReleaseAddRef に加えて、(GetIDsOfNamesInvoke を含む)IDispatch のメソッドをすべて実装しなければなりません。ATL は、IUnknown のメソッドの実装を提供するのと同じく、IDispatch のメソッドの実装も提供します。

デュアル インターフェイスの IDL

デュアル インターフェイスの IDL は、カスタム インターフェイスの IDL によく似ています。BeepCnt オブジェクトの場合、IBeepCnt インターフェイスの IDL は以下のようになります。

     [
object,
uuid(4F74530F-3943-11D2-A2B5-00C04F8EE2AF),
dual,
helpstring("IBeepCount Interface"),
pointer_default(unique)
   ]
interface IBeepCount :IDispatch
   {
[id(1), helpstring("method Beep")] HRESULT Beep();
[propget, id(2), helpstring("property Count")]
HRESULT Count([out, retval] long *pVal);
[propput, id(2), helpstring("property Count")]
HRESULT Count([in] long newVal);
   };

デュアル インターフェイスの IID と、インターフェイスがデュアル インターフェイスであるということは、インターフェイスの属性に指定されています。またインターフェイスが IUnknown ではなく、IDispatch から派生している点にも注目してください。

メソッド属性の ID は、オートメーション インターフェイスのための dispid です。プロパティを実装しているメソッドが特別な属性を持っていることに注意してください。このケースでは、Count プロパティが 2 つのメソッドを持っています。1 つは値を取得するためのもので、もう 1 つはそれを設定するためのものです。MIDL が C++ のヘッダ ファイルを生成するときに、その 2 つのメソッドには get_Countput_Count という名前が与えられます。

ところで、このインターフェイスのための関数テーブルは 10 個のエントリを持つことになります。3 つが IUnknown 用、4 つが IDispatch 用、そして 3 つが IBeepCount メソッド用です。

タイプ ライブラリ

タイプ ライブラリは IDL ファイルの library セクションに基づいて生成されます。

  [
uuid(4F745303-3943-11D2-A2B5-00C04F8EE2AF),
version(1.0),
helpstring("BeepCnt 1.0 Type Library")
]
library BEEPCNTLib
{
importlib("stdole32.tlb");
importlib("stdole2.tlb");

   [
uuid(4F745310-3943-11D2-A2B5-00C04F8EE2AF),
helpstring("BeepCount Class")
   ]
coclass BeepCount
   {
[default] interface IBeepCount;
   };
};

library 属性は LIBID、バージョン、ヘルプ文字列を指定します。ライブラリは標準のタイプ ライブラリをインポートしてから、このオブジェクト固有の coclass を指定します。

このオブジェクトの CLSID は、coclass の属性リストに含まれている GUID によって指定されています。これは単純なオブジェクトなので、あとはオブジェクトが実装するインターフェイスを指定するだけです(IUnknown と IDispatch は継承によって含まれています)。

MIDL はタイプ ライブラリを .tlb ファイル内に生成します。このファイルは独立に使えますが、普通はそうする必要はありません。このファイルは DLL ファイルのリソース セクションに含まれています。つまり、DLL に組み込まれているのです。

タイプ ライブラリの本来の使い方もたいへん重要です。Visual Basic、Visual J++、Visual C++ のスマート ポインタ(#import)などのツールは、タイプ ライブラリを使ってオブジェクトが持っているメソッドとプロパティを調べます。

ATL が COM を使って IDispatch を実装する方法

ATL の IDispatch の実装は、この点ではとてもシンプルです。まず、DllMain 関数(DLL がロードされたときに実行されます)内で、Module オブジェクトの Init メソッドがほかのものとともにタイプ ライブラリをロードします。COM では、標準の COM インターフェイスである ITypeLib と ITypeInfo を通してタイプ ライブラリにアクセスできます。Init はオブジェクトのタイプ ライブラリの ITypeInfo インターフェイスへのポインタを保存します。

われらがよき友、ITypeInfo

ITypeInfo インターフェイスには、GetIDsOfNamesInvoke というメソッドがあります。 ITypeInfo::GetIDsOfNames はタイプ ライブラリの情報を使って、渡された名前に対応する正しいディスパッチ ID を取得します。このメソッドの実装は COM に組み込まれているので、自分で書く必要はありません。タイプ ライブラリを提供するだけでいいのです。

ITypeInfo::Invoke は、それよりもかなり興味深いものです。このメソッドは dispid とパラメータ(その他)を解析して、デュアル インターフェイスの関数テーブルを通じて適切な C++ メソッドを呼び出します。このすべてを行うために、タイプ ライブラリ情報を使って、パラメータの変換後の型や、既定値のあるパラメータ、省略可能パラメータ、名前付きパラメータの扱い方、関数テーブルのオフセットを調べます。それからパラメータ用のスタック フレームを作って呼び出しを行います。

COM がこれらの作業すべてを行うので、IDispatchImpl における ATL の IDispatch メソッドの実装はとてもシンプルです。オブジェクトが初期化されたときに保存される ITypeInfo インターフェイスのポインタを通して、呼び出しをタイプ ライブラリに渡すだけです。たとえば、IDispatchImpl::Invoke のコードは以下のようにシンプルです。

  STDMETHOD(Invoke)(DISPID dispidMember, REFIID riid,
LCID lcid, WORD wFlags, DISPPARAMS* pdispparams,
VARIANT* pvarResult, EXCEPINFO* pexcepinfo, UINT* puArgErr)
   {
return _tih.Invoke((IDispatch*)this, dispidMember, riid, lcid,
wFlags, pdispparams, pvarResult, pexcepinfo, puArgErr);
   }

ITypeInfo のポインタは tih に格納されています。オブジェクトへのポインタと、渡されたパラメータを渡せば、あとは COM がやってくれます。これほど簡単なことはありません。

オッケー、じゃあ落とし穴どこ?

しかし、みなさんの推測どおり、デュアル インターフェイスの IDispatch 側を使うと速度が犠牲になります。タイプ ライブラリを掘り返して、コール スタックを組み立てるのはタダではありません。とはいえ、COM の実装はオーバーヘッドを最小にするように最適化されています。いずれにしても、IDispatch の呼び出しは、呼び出し側で行う必要のある処理(上で示したような配列の準備、コール スタックの準備など)のおかげで遅いのです。したがって、違いはそんなにありません。特に、IDispatch::Invoke を自分で実装した場合には、タイプ ライブラリを掘り返さないことを除けば、ほぼ同じくらいの作業が必要になるからです。

コンポーネントでオートメーションをサポートするべき状況

では、オートメーションはどのような場合にサポートするべきで、どのようなときにサポートするべきではないのでしょう。

当然ながら、コンポーネントをスクリプティング言語やほかのオートメーション専用クライアントで使ってもらいたい場合には、オートメーションをサポートしなければなりません。したがって、コンポーネントが Web ページの中、あるいは Windows Script Host または Office アプリケーション、その他多くのアプリケーションの Visual Basic for Applications で使われる場合には、オートメーションをサポートする必要があります。

つまり、自分が作るコンポーネントの市場を思い切って狭めるつもりがないのならば、オートメーションをサポートする必要があるということです。そしてそれは ATL で適切なボタンをクリックするだけという簡単な操作で実現できるので、サポートしない理由はないでしょう。

その一方で、関数テーブル対応の言語(Visual C++、Visual J++、Visual Basic など)からのみ呼び出すコンポーネントもあります。システムがコンポーネントを使うように設計されている(たいへんよいことです)ためにコンポーネントを書いている場合がそのよい例です。こうしたコンポーネントは、そのシステム専用に作られるので、それ以外の場所で使えることはあまりありません。したがって、たとえば Web ページでは、これらのコンポーネントは使う意味はないかもしれません。

オートメーションをサポートするコストは?

オートメーションをサポートするには、いくらかコストがかかります。

サイズとスピード

デュアル インターフェイスを提供すると、ATL コンポーネントのサイズが若干大きくなります。通常、これはあまり影響ありませんが、1 バイトの違いがものいうような場合は、コンポーネントを両方の方法で生成して、影響を調べるとよいでしょう。

IDispatch の呼び出しは遅いですが、デュアル インターフェイスを使えば、クライアントは速い呼び出し(関数テーブル)と遅い呼び出し(オートメーション)のどちらかを選択できます。したがってパフォーマンスはあまり大きな問題ではありません。

しかし、クライアントとサーバーが別々のプロセス(または別々のマシン)にある場合のように、マーシャリングがからむ呼び出しのときに呼び出しが遅くなるという微妙なパフォーマンス上の問題があります。デュアル インターフェイスは COM のユニバーサル マーシャラを使います。これはタイプ ライブラリ情報に基づいて処理をします。このマーシャラは、MIDL が生成するものよりもやや遅くなります。しかし、呼び出しをマーシャリングするのにかかる時間は、プロセスを切り替えたり、ネットワーク上のほかのマシンと通信するのにかかる時間に比べると、まったく些細なものです。

ふつうインプロセス(DLL)サーバーではマーシャリングをする必要がないので、この問題はほとんどのケースでは影響ありません。しかし、しばしばマルチスレッド アプリケーションに見られるように、クライアントとオブジェクトが異なる「アパートメント」にある場合には、インプロセス サーバーでもマーシャリングが行われます。インプロセスでアパートメントをまたがるケースでは、マーシャリングにかかる時間が大きな影響を与える場合があります。

古きよき C++ ほど柔軟ではない

もっと大きな問題が、オートメーション インターフェイス モデルと標準の COM インターフェイス モデルとの違いに関係しています。まず第一に、それぞれのオブジェクトには、オートメーション インターフェイスが 1 つしかないというのが普通です。これは、設計によっては問題になることがあります。

さらに問題になるのは、パラメータと戻り値が、バリアントに格納できる型に限られている点です。バリアントについては次回のコラムで取り上げますが、ここでは、ほとんどのスカラー型のほか、文字列と配列がサポートされていることを知っておきましょう。つまり、私たちが必要とするもののほとんどがサポートされています。

しかしオートメーション インターフェイスでは構造体はサポートされません。コードを書いてこれに対処することはできますが、大変です。また、これはリンクされたデータ構造もサポートするのが難しいということです。

したがって、C++ の複雑なデータ構造などを受け渡したい場合、オートメーションは不向きです。それでもオートメーションをサポートしたい場合にはどうすればいいのでしょうか。

スマートな代替手段

オートメーションをサポートしない適切な理由があるにもかかわらず、そうする必要があるのならば、解決策があります。デュアル インターフェイスと(機能的に)同等のカスタム インターフェイスの両方を作るのです。

カスタム インターフェイスでは C++ の強力なデータ構造が利用でき、ユニバーサル マーシャラを使う必要がありません。実際、最高のパフォーマンスを得るために、カスタムのマーシャラを書くこともできます。C と C++ のクライアントはこのインターフェイスを使うことになります。

それ以外の人はデュアル インターフェイスを使います。複雑な C++ データ構造を、バリアントに格納できる適切な形に変換する方法を知るためには、複雑な操作をしなければならないかもしれませんが、それができてしまえば、どのクライアントでも使えるオブジェクトが手に入ります。

やってみよう!

これまでお話ししてきたことを実際に試してみるまでは、説明をきちんと理解しているかどうかわからないことは Dr. GUI も十分わかっています。ならば、実際にやってみましょう!

  • ** IDispatch::Invoke** 呼び出し(と関連するコード)をコーディングして、単純な COM オートメーション コンポーネントを呼び出す、単純なアプリケーションを書いてみましょう。これをたびたびやりたくはないでしょうが、一度やっておくことをお薦めします。少なくとも、スクリプティング言語が何をしてくれるのかを味わう手助けになるからです。

  • 恐るべき Invoke メソッドを含む IDispatch を実装する単純なオートメーション コンポーネントをあなた自身が書いてください。ここで言及しなかった単純なメソッドが 2 つあります。それらについてはドキュメントを参照してください。もしそうしたければ、COM 標準の IDispatch の実装を使うようにソリューションを書き換えてもいいでしょう。

今までの復習、これからの予定

今回は、オートメーションのプロパティとメソッドを使う方法を説明することで、COM オートメーションの謎に対する頭金を払いました。次回は、オートメーションのデータ型を取り上げ、デュアル インターフェイスを探求します。