Dr.GUI と COM オートメーション、第 3 部:続 COM のすばらしきデータ型

1999 年 4 月 20 日

参照:
「Dr.GUIとCOMオートメーション、第1部」
「Dr.GUIとCOMオートメーション、第2部」

目次

Dr.GUIのビットとバイト
これまでの復習、これからの予定
やってみよう
これまでの復習、これからの予定

Dr.GUI のビットとバイト

ATL に関するすばらしい新刊

このコラムの締め切り直前に、Dr.GUI は ATL プログラマ必携の本、『ATL Internals 』(Brent Rector、Chris Sells 著 Addison‐Wesley 出版)を受け取りました。まさしく ATL を作る人間が書きたいと思っていた、すばらしい本です(ATL の開発主任、Jim Springfield の感想をご覧ください)。http://cseng.aw.com/bookdetail.qry?ISBN=0-201-69589-8&ptype=0 で Amazon.com の非常に興味深いレビューをご覧ください。

Time に掲載された Tim Berners-Lee のプロフィール

Dr.GUI は、Time 誌が Tim Berners-Lee を Time100 人、つまり 20 世紀の最も重要な人物 100 人の 1 人として選んだ(20 世紀は 2000 年の終わりまであることをお忘れなく)というメールを受け取りました。http://cgi.pathfinder.com/time/time100/scientist/profile/bernerslee.html でその記事をご覧になれます。

そのサイトで、Alan Turing(http://cgi.pathfinder.com/time/time100/scientist/profile/turing.html)、William Shockley(http://cgi.pathfinder.com/time/time100/scientist/profile/shockley.html)、Thomas Watson, Jr.(http://cgi.pathfinder.com/time/time100/builder/profile/watson.html)、Bill Gates(http://cgi.pathfinder.com/time/time100/builder/profile/gates.html)など、コンピュータ分野におけるその他の重要人物についてもご覧になってください。

これ以外の 75 人のプロフィールについても http://cgi.pathfinder.com/time/time100/index.html でご覧ください(最後の「ヒーローとインスピレーション」の 20 人は 6 月まで投稿されません)。それらの記事は概してよく書けています(名医は Gates の記事の言い回しには少し不満がありますが、その一方で Bill を贔屓目に見ていることも認めます)。

Web 上でのオーディオ

Dr.GUI は様々なマーケティングの担当者の要望にも関わらず、ストリーミング メディアについて書くことを(ほとんどの場合)断固として拒否してきました。しかし、新しいオーディオ形式は Dr.GUI を動かしました。ストリーミングかつダウンロード可能で、MP3 の半分のサイズ、MP3 と同等またはそれ以上の音質、そしてオーサーは好みに応じてライセンシングを使って作品を保護できます。

Dr.GUI は Windows Media Audio SDK をダウンロードする機会を得られませんでしたが、ぜひ試してください。たったの 836K バイトなので、ダウンロードも時間はかからないでしょう。

次回の C++ コラム

Dr.GUI はここで喜んで MSDN が間もなく新しいコラムニストを迎え入れることを発表します。新しいコラムをお願いすることになった Bobby Schmidt は、「C/C++ User's Journal」で記事を書くほか、さまざまなことをしています。Bobby は(Microsoft の製品サポートと製品開発や P.J.Plauger と Metrowerks での仕事を含め)Microsoft 内外での経験が豊富です。彼は、C++ のプログラマがより効率的に C++ を使えるように手助けをしていくつもりです。Dr.GUI は彼と仕事ができて光栄です。Bobby のコラムが来月の MSDN Online Voices に初登場すればみなさんも満足することでしょう。

Web アプリケーションがすべての問題を解決しない理由

Bobby はかなり長い間 Windows では作業していません。Mac 系の人なのです。そのため、彼は書く内容を見つけるために、名医に MSDN の CD/DVD を「本当に」インストールする必要があるのかどうかを尋ねてきました。

まあ、Dr.GUI は可能であれば「いつでも」 CD/DVD を使います。その速度をとても気に入っているからです。しかしここには高速なインターネット接続もあるので、Bobby と名医は MSDN Online Library を CD/DVD の代わりに利用できるか考えていました。

答えは簡単でした。いいや、まだしない方がいい。

理由は次のとおり:

  • Visual Studio®と統合されていないために F1 ルックアップが Web では動作しません。

  • Web にはキーワード索引がありません。Dr.GUI の(あまり謙虚でない)見解では、何かを調べるときにはキーワード索引がずば抜けて便利です。

  • Web 検索には[タイトルのみ検索]や[以前の結果から検索]などの重要な機能がありません。また最初の 50 件しか結果として返しません。1G バイト以上の文書を検索するときにはあまり有用ではありせん。

  • Web からサンプル コードを参照するためには、.zip ファイル全体をダウンロードしなければなりません。CD/DVD の場合のように、ファイルを 1 つだけ見るといったことができません。

内容はすべてそろっていて、ツリー コントロール形式の目次もありますが、検索が必要なときあるいは索引を使うときは、苦痛が襲ってきます。MSDN Online Library はすばらしい Web サイトですが、CD/DVD のように開発者が便利に使えるようになるためには、まだまだ改良が必要です。

なぜなのでしょう。答えは簡単です。HTML/HTTP は元来アプリケーション プラットフォームとして開発されたものではないのです。元々はハイパーリンク テキストを表示する方法として開発されたのです。しかし、特別ハイパーリンク テキスト表示が得意というわけでもありません。レイアウト機能は信じがたいほど原始的でした。HTML はすばらしい結果を生んでいるのは確かです。Tim Berners-Lee を表彰した点では『Time』は正しいことをしました。しかし、HTML が何でも可能な究極のものでないのは明らかです。

その後、HTML/HTTP のレイアウト機能とプログラミング機能を改善するためにさまざまな改良が加えられましたが、最上のアプリケーション プラットフォームにはなっていません。HTML で快適に動作するアプリケーションはたくさんありますが、HTML/HTTP で本格的なクライアント サーバー アプリケーションや分散アプリケーションを開発しようとすると、すぐに大きな制約に直面します。

そういうわけで、MSDN Library の Web 版がそれほどよくない理由の一部は簡単です。プラットフォームに限界があるのでよいものを作るのが非常に大変なのです。Web はすべての問題を解決してくれません。

ところで、ソフトウェア開発で生計を立てている方の場合、MSDN Library やほかの MSDN 製品に費やすお金はそれだけの価値があります。なかなか気付きませんが、検索に要する時間を節約するだけでも MSDN 製品に払った分だけペイしています。さらに、Microsoft Visual Studio のどのコピーにも、MSDN Library の特別版が無料でついてきます。

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

今回は COM オートメーションについての 3 回シリーズの第 3 部です。そうです、遂に終わるのです。次回は COM イベントについてです。

前回は VARIANT についてお話しました。今回は COM が提供するその他の特別なデータ型、BSTRDATECURRENCYDECIMALSAFEARRAY についてお話します。またコレクションと、詳細エラー情報を報告するエラー オブジェクトについて取り上げます。

BSTR

ほとんどの COM 文字列は Unicode へのポインタで構成される 0 で終わる C/C++ 形式の文字列(LPOLESTR)を使って渡されます。Unicode 文字列(0 終端文字を含む)の各文字は 2 バイトで構成されます(Unicode に関する詳細は http://www.unicode.org/ でご覧いただけます )。Unicode 文字は 2 バイトなので、最大 65,536 の Unicode 文字が可能します。これはアジアの言語を含め、あらゆる言語の文字をすべて表すのに十分な数です。

しかしオートメーションでサポートされている文字列形式は BSTR です。BSTR の「B」は Basic を表します。これは Microsoft Visual Basic®が内部で使う文字列形式です。BSTR LPOLEST 型の 0 で終わる文字列へのポインタです。違いは文字列の「前に」付加される情報です。文字列の先頭にある文字の直前の 4 バイトに、文字列の長さ(単位はバイトで、終端文字は含まれない)が格納されます。文字列の長さがわかるので、文字列の中にバイナリの 0 の値を持つ文字を含めることができます。そのため、0 が埋め込まれている可能性のある BSTR 文字列を操作するとき、特にほかから渡された文字列を操作するときには注意しましょう。

また BSTR 文字列は、COM のメモリ アロケータを使ってメモリを割り当てた文字列を使って COM コンポーネント間で文字列データをやり取りするときにも使います(このアロケータを使えば、コンポーネントが動作しているプロセスまたはマシンにかかわりなく、あるコンポーネントで文字列を割り当て、別のコンポーネントで文字列を解放できます)。

形式

以下に COM API の SysAllocString SysFreeString を使って BSTR の割り当てと解放を行う簡単な例を示します。

    BSTR bstrMsg = SysAllocString(OLESTR("Hello"));
    // call some method that expects a BSTR or LPOLESTR passing bstrMsg
    ptrInterface->Method(bstrMsg);

ここでは OLESTR マクロを使って文字列定数が正しい型になるようにしています。Microsoft Win32®ではこれにより文字列の先頭に大文字「L」が置かれ、文字列が Unicode ワイド文字列に変換されます。

    L"Hello"

プログラマ自身が「L」と書くこともできますが、マクロを使うことですべてのプラットフォームで処理が正しく行われることが保証されます。

結果の文字列はメモリ上では次のように見えます。ポインタが Unicode 文字列のカウントではなく、最初の 1 バイトを指している点に注目してください。

カウントには注意が必要です。これは文字列の文字数ではなく「バイト数」を表します。このカウントには終端の 0(2 バイト)は含まれません。したがって、5 文字の文字列「Hello」のカウントは 10 になります。

この文字列に対するメモリの割り当ては最低でも 16 バイトになることに注目してください。カウントに 4 バイト、5 つの Unicode 文字(各文字 2 バイト)に 10 バイト、終端の 0 のために 2 バイトの計 16 バイトです。

文字列が必要なくなったら、BSTR を次のようにして解放します。

    SysFreeString(bstrMsg);

2 番目の注目点は、別のマシンにあるコンポーネントに BSTR を渡せることです。BSTR を渡されたコンポーネントは SysFreeString を呼び出して文字列を解放できます。COM によって正しい処理が行われることが保証されています。

COM API

すでに SysAllocString SysFreeString など、COM API をいつか紹介しました。このほかに、固定長文字列やバイナリ データ(値が 0 のバイトを含むこともある)が含まれる文字列を割り当てる SysAllocStringLen SysAllocStringByteLenBSTR(古い BSTR の解放を含む)の再割り当てをする SysReAllocString SysReAllocStringLen、文字列の長さを文字単位とバイト単位で返す SysStringLen SysStringByteLen があります。

文字列の文字にアクセスするにはポインタ演算と標準 C/C++ 文字列関数を使います。文字列の長さは変更できません。変更する必要がある場合には、C/C++ Unicode 文字列を作成して SysReAllocString 関数の 1 つを使って BSTR を更新します。

VectorFromBstr BstrFromVector を使って BSTR 文字列を一次元のセーフ配列に変換できます(セーフ配列の詳細については後述します)。

また現在文書化されていない役に立つ関数も一組あります。VarBstrCat は 2 つの BSTR 文字列を連結して新しい文字列を作ります。VarBstrCmp は 2 つの BSTR 文字列を比較して VTCMP_LTVTCMP_EQVTCMP_GT または VTCMP_NULL を返します。

C++ ヘルパー クラス

Microsoft Foundation Class(MFC)には BSTR をラップするヘルパー クラスがありません。代わりに、別の方法で BSTR と MFC の CString クラス間の変換が簡単に行えます。次のように BSTR CString コンストラクタに渡すだけで BSTR CString に変換できます。

    CString strFoo(bstrMsg);

CString BSTR に変換するには、CString クラスのメソッドのうち、新しい BSTR を作成する AllocSysString、または BSTR の再割り当てをする SetSysString のいずれかを使います。

ATL は CComBSTR クラスを提供することで、BSTR 文字列へのより簡単なインターフェイスを提供します。CComBSTR は文字列の作成、割り当て、解放に加えて、文字列を連結するためのメソッドも提供します。

Microsoft Visual C++® version 5.0 およびそれ以降のバージョンは、より高度な BSTR 用のラッパーを提供する _bstr_t class をサポートします。CComBSTR の機能のほかに、_bstr_t には比較演算機能もあります。さらに、_bstr_t は参照カウントを使うことで、余分な BSTR オブジェクトが割り当てられるのを回避します。効率的ですが CComBSTR ほど軽量なラッパーではありません。

C++ 標準ライブラリの wstring クラスを使うこともできますが、BSTR への変換と BSTR からの変換が必要です。適切なコンストラクタを使って新しい wstring オブジェクトを作成することによって、BSTR から wstring に変換できます。BSTR に変換するには、wstring オブジェクトでラップされている文字列へのポインタを取得して、SysAllocString など BSTR を割り当てる COM API のいずれかを呼び出します。

通貨

Dr.GUI は、みなさんが C、C++、Java の float double、Visual Basic の Single Double などのバイナリ不動小数点数型は、小数部を完全な精度では表せないと知っていることを願っています。同様に、小数は特定の分数を完全な精度で表せません(たとえば、小数で 1/3 を正しく表現できません)。いずれの場合も、桁数を増やせば実用的な近似値は得られますが、完全に等しい値は得られません。どちらにしてもコンピュータの桁数にも限界があります。

この問題は、通貨を処理するプログラムで表面化します。たとえば、次のプログラムで 1000 万個のペニーの値を調べようとすると、結果は 100,000 ドルではなく 99,999.999986 ドルになります。

    double acc = 0;
    for (int i = 0; i < 10000000; i++) {
        acc = acc + 0.01;
    }
    printf("Accumulated value is $%f\n", acc);

この間違いは小さなものですが、小さな間違いは累積します。経理が 1 銭の違いも嫌うのはよく知っていますよね。

この問題は欠陥でも何でもありません。これは単にバイナリ分数(この値のビットは 1/2、1/4、1/8、1/16 など 2 の累乗分の 1 を表します)であるデータ型では、1/10 や 1/100 などの分数を完全な精度で表せないからなのです。

これらの問題は、注意深く小数を丸めることによって解決を図ることもできます。または、小数を正確に表すデータ表現を使うこともできます(ただし先ほどの 1/3 の問題は解決されません。そのためには分子と分母を分けて有理数で表現しなければなりません。それでも、e や pi などの無理数は表現できません)。

一般的に使われている方法の 1 つは、整数を使って小数を表し、小数を出力する前に 100、1,000 または 10,000 でその整数を割るという方法です。つまり、整数に 1 を加算することが、表現方法に応じて実際には 1 ペニー、1/10 ペニーまたは 1/100 ペニーを加算することになります。また別の言い方をすれば、1 ドルを加算するために、同様に表現に応じて 100、1,000 または 10,000 を加算しなければなりません。

別の表現方法に、2 進化 10 進数(BCD)を使う方法があります。10 進数の各桁は 4 ビットに格納されるので各バイトは 2 桁の 10 進数 0~99 を格納することになります。

整数として値を格納する方法には、演算を行うために 64 ビットの標準整数処理が使えるという大きなメリットがあります。BCD を使った演算のアルゴリズムはとても醜くい上に処理も遅いのです(Intel プロセッサでは、プロセッサの浮動小数演算部に BCD 演算処理機能が組み込まれているので、BCD に関して速度の問題はありません。この演算機能は、BCD 数値を、整数を含む 80 ビットの浮動小数点数に変換します)。

書式

COM の CY と Visual Basic の Currency 型は 1/10,000(セントの 1/100)単位の 64 ビットの符号付き整数です。通貨の型は正確にドルとセントの値を表現できるので、財務計算に向いています。通貨型は、-900 ~+900 以上の範囲を表せるので、少なくとも今後数年の間は、国際発行高の計算まで行えます(これでも十分でない場合は、後で説明する DECIMAL 型を使います)。

COM API

通貨型を処理する COM API で文書化されているのは、別の型からの変換、または別の型への変換を行う 20 または 30 組みの変換関数だけです。_int64 データ型を使っていれば、通過の型は整数なのでほとんどの演算を安全に行えます。

現在文書化されていない通貨演算関数もあります。これらは対応するバリアント演算関数に似ています。これらの関数には、VarCyAddVarCyMulVarCyMulI4VarCySubVarCyAbsVarCyFixVarCyIntVarCyNegVarCyRoundVarCyCmpVarCyCmpR8 があります。VarCyMulI4 VarCyCmpR8 は第 2 パラメータに long と double を使います。これは、他の型から通貨型に変換するよりは効率的です。現在除算するための関数はありません。

C++ ヘルパー クラス

C++ の通貨クラスの数ははるかに少ないです。MFC だけがラッパー クラス COleCurrency を持っています。COleCurrency には、算術演算子、比較演算子、文字列として通貨オブジェクトを書式化するメンバ、文字列を解析して通貨オブジェクトを取得するメンバ、そしておなじみのコンストラクタ、デストラクタ、代入演算子があります。

DECIMAL

さて、900 兆では十分でないのですね。28 桁の 10 進数の数値を正確に 10 進数で表現する必要がある場合には、DECIMAL を使うとよいでしょう。DECIMAL は 16 バイトで表されます。VARIANT とまったく同じです。最初の 2 バイトは予約済み(VT_DECIMAL に使用される)、次の 1 バイトが精度、続いて符号を表す 1 バイト、そして残りの 12 バイト(96 ビット)で整数を表します。この整数にさらに 32 ビットを追加すれば、28 または 29 桁の 10 進数の数値を扱えます。これはかなり大きな数値です。0 から 28 までの精度(小数点の右側の桁数)を設定できるので、DECIMAL 型は保持できる数値についてはより柔軟性があります。ただし、DECIMAL を使った数値演算はたいへん時間がかかります。これほど拡張する必要がない場合は、通貨型を使うようにしましょう。その方が断然速いです。

COM API

DECIMAL 型を処理する COM API で文書化されているのは、別の型からの変換、または別の型への変換を行う 20 または 30 組みの変換関数だけです。

間もなく文書化される予定の関数は OLEAUTO.H にあり、みなさんの期待通り、除算だってします。関数は VarDecAddVarDecDivVarDecMulVarDecSubVarDecAbsVarDecFixVarDecIntVarDecNegVarDecRoundVarDecCmpVarDecCmpR8 です。

C++ ヘルパー クラス

こちらはあまり恵まれていません。DECIMAL が COM に追加されたのは比較的最近のことなので、ヘルパー クラスがありません。MFC にもありません。

DATE

COM の DATE 型は日付と時刻を表します。日付と時刻は 8 バイトの浮動小数点数(C、C++、Java の double)として格納されます。この数値の整数部が日付を表します。そして小数部が時刻を表します。この形式は西暦 100 年の 1 月 1 日から西暦 9999 年の 12 月 31 日までを表現でき Y2K 問題はありません(とはいえ、Y10K 問題はありますが、D10K 問題ほど悪い事態ではありません。Dr.GUI は気にしていません)。

注意:時刻はバイナリの不動小数点数の小数部で表わされるので、多くの時刻を正確に表すことができません。正確に表すことができるのは、1/2(12 時間)、1/4(6 時間)、1/256(5.625 分)の倍数など、バイナリ分数の倍数として表せる時刻だけです。したがって、1 時間、1 分、1 秒は正確に表せません。

通常、時刻の値を使った計算を繰り返すことはないので、丸め誤差は大した問題になりませんが、注意は必要です。また、1899 年の 12 月 30 日の 0 時から遠ざかるほど日付を表すために必要なビット数が増えるので、時刻を表すビットが少なくなり時刻を表す精度は低くなります。桁数については心配する必要はありません。西暦 9999 年の日付を表すには最大 22 ビット必要ですが、残っている約 40 ビットで時刻を表せるからです。1/100 秒までを表すには約 24 ビットあればよいので、残っている 16 ビットを使えばさらに精度を上げることができます。

COM API

日付を処理する COM API は非常に小さいものです。前に説明した VarFormatDateTime 関数、20 または 30 の変換関数、地域での曜日の名称を文字列として取得する VarWeekdayName、月の名称を文字列として取得する VarMonthName、代替の月の名称(ヒジュラ アラビア太陰暦、ポーランド、ロシアの月の名称の代替)の一覧を取得する GetAltMonthNames があります。

DATE オブジェクトに日数を表す数値を加算したり減算したりすることで日付の演算ができます。加算や減算を行うには、単純に標準の浮動小数点演算を行います。たとえば、1 日先に進めるには 1 を DATE オブジェクトに加算します。6 時間戻すには DATE オブジェクトから 0.25 を減算します。年や月の日数は変わるので、1 年あるいは 1 月先に進めるのは非常に難しいのです。

DATE を、SYSTEMTIME 構造体と年の通算日(1 月 1 日は 1、2 月 1 日は 32 など)を含む構造体に展開し、逆にそうした構造体を DATE にパックする非常に便利な関数が 2 つあります。VarUdateFromDate DATE UDATE 構造体に展開し、VarDateFromUdate UDATE 構造体を DATE にパックします。UDATE 構造体には以下が含まれています。

typedef struct {
    SYSTEMTIME st;
    USHORT  wDayOfYear;
} UDATE;

そして SYSTEMTIME 構造体には以下が含まれています ...

typedef struct _SYSTEMTIME {  // st
    WORD wYear;
    WORD wMonth;
    WORD wDayOfWeek;
    WORD wDay;
    WORD wHour;
    WORD wMinute;
    WORD wSecond;
    WORD wMilliseconds;
} SYSTEMTIME;

最後に、DATE(実のところは double)から DOS の日付と時刻の書式と先ほど説明した SYSTEMTIME 書式に変換する、またその逆の変換を行う 4 つの関数があります。wDayOfYear メンバが必要でない場合には、VariantTimeToSystemTime SystemTimeToVariantTime という 2 つの SYSTEMTIME 変換関数を使うとよいでしょう。

日付を UDATE または SYSTEMTIME に変換し、UDATE(または SYSTEMTIME)構造体の適切なメンバを変更して、計算をした後に DATE に戻すことで、日付に対して年、月、日、時間、分、秒、ミリ秒を加算または減算するなどの日付演算が行えます。構造体のメンバの一貫性を保ち、オーバフローが起きないように注意してください。たとえば、wDay を変更する場合には、同じように wDayOfWeek(と wDayOfYear)も修正しなくてはなりません。そして、いずれのメンバも定義されている値をオーバーフローしないように注意を払う必要があります。同様に、wMonthwHourwMinutewSecond または wMilliseconds を修正する場合は、それらがオーバーフローしないようにしなければなりません。オーバーフローした場合、VariantTimeToSystemTime または VarDateFromUdate を呼び出して元の形式に戻すときにおかしな結果になります。

C++ ヘルパー クラス

MFC には日付処理に役立つ 2 つのクラス COleDateTime COleDateTimeSpan があります。

COleDateTime には指定した時刻または現在の時刻を使って日付オブジェクトを作成するメソッドが含まれています。また、SYSTEMTIME 構造体への変換、年 / 月 / 日 / 時間 / 分 / 秒 / 曜日 / 通日の取得、日付と時刻の設定、文字列の書式化と文字列の解析、2 つの COleDateTime オブジェクトの減算または比較を行うためのメソッドもあります。

2 つの COleDateTime オブジェクトで減算をした結果は COleDateTimeSpan オブジェクトになります。これは経過時間を表します。また COleDateTimeSpan オブジェクトを COleDateTime オブジェクトに加算することもできます。結果は COleDateTime オブジェクトです。

COleDateTime オブジェクトは、時間経過の設定、日数 / 時間数 / 分数 / 秒数の取得、日数 / 時間数 / 分数 / 秒数の合計の計算、書式設定、2 つの COleDateTimeSpan オブジェクトの加算と減算を行うメソッドをサポートします。

セーフ配列

オートメーションを使って配列を渡すには、セーフ配列(へのポインタ)を含むバリアントを作成します。セーフ配列は単一のデータ型の一次元または二次元の配列です(しかしこの単一データ型は VARIANT でもよいため、型を複数含んだ配列も可能です)。Visual Basic では配列の下限は 0 でなくてもよいので、セーフ配列には下限とサイズも格納しなければなりません。

これらの配列がセーフ配列と呼ばれるのは、これらが範囲情報持ち、配列のデータにアクセスする前に添え字の範囲を確認できるからです(SafeArrayGetElement SafeArrayPutElement API はこれを自動的に行います)。これに比べ、C と C++ 配列の範囲検査が行われることはめったにないため、添え字の有効範囲を超えたことによる原因究明の難しい大きなエラーが起きやすいのです。

最後に、セーフ配列はロック(とアンロック)ができるため、取得したデータへのポインタが有効なことが保証されます。

書式

セーフ配列にアクセスするための、詳細に文書化された COM 関数の大きなセットがあります。構造体やポインタを直接操作するよりも、これらの COM 関数を使うべきです。しかし、内部で何が起きているのかわかるように、ここではこれらの構造体について説明します。

SAFEARRAY の定義は次のとおりです(コメントを追加しておきます)。

typedef struct  tagSAFEARRAY
    {
    USHORT cDims;  // number of dimensions
    USHORT fFeatures;  // flags for allocation type, data type
    ULONG cbElements;  // size of a single element
    ULONG cLocks;  // lock counter
    PVOID pvData;  // pointer to actual data block
    SAFEARRAYBOUND rgsabound[ 1 ]; // array of bounds structs
    }    SAFEARRAY;

ほとんどのメンバは比較的簡単に理解できます。フラグ ワード fFeatures により、セーフ配列がスタック上に割り当てられているのか、構造体内にあるのか、静的に割り当てられているのか、サイズが可変なのか、VARIANTBSTRIDispatch、または IUnknown ポインタを含んでいるのかがわかります。

rgsabound 配列は次元ごとに 1 つの要素が含まれています。要するに SAFEARRAY 構造体のサイズが一定でないということです。cDims に格納されている次元の数によります(C と C++ 配列の範囲検査をしないため、要素が 1 つしかないと宣言されていても、rgsabound 配列の任意の数の要素にアクセスできます)。

SAFEARRAYBOUND 構造体は次のように単純です。

typedef struct  tagSAFEARRAYBOUND
    {
    ULONG cElements;
    LONG lLbound;
    }    SAFEARRAYBOUND;

この構造体には、要素の数と、次元の下限だけが含まれています。

COM API

セーフ配列にアクセスするための関数は 29 個あります。そのうちの 21 個が現在文書化されています(残りの 8 個は次回のリリース時に文書化される予定です)。

通常、SafeArrayCreateSafeArrayCreateVectorSafeArrayCopySafeArrayCreateEx または SafeArrayCreateVectorEx を呼び出してセーフ配列を作成します。「Vector」系では一次元配列のみ作成できます。「Ex」系では各種 VARIANT データ型が作成でき、その配列に関連付けるインターフェイス ID を指定できます(レコードまたはインターフェイス ポインタ含んだ便利なセーフ配列)。

それから SafeArrayGetDimSafeArrayGetLBoundSafeArrayGetUBoundSafeArrayGetElemsizeSafeArrayGetElement SafeArrayPutElement を使って配列の次元情報とデータにアクセスできます。アクセス速度を上げるために、SafeArrayLock または SafeArrayAccessData を使って配列をロックしてデータへのポインタを取得できます。その後、SafeArrayPtrOfIndex を使って任意の要素へのポインタを取得したり、自分で自由にアクセスしたりできます。直接データにアクセスする作業が終わったら、配列を SafeArrayUnlock または SafeArrayUnaccessData でアンロックするのを忘れないようにしましょう。

配列のデータにアクセスするときは、添え字の配列(または単独の添え字)へのポインタを渡す必要があることに注意してください。つまり、配列を用意しておいて、呼び出しの前に必要な変更を加える必要があるのです。添え字をパラメータ リストとして直接渡すことはできません。残念。

配列を作成するときにサイズを可変にしないと指定しない限り、SafeArrayRedim を使って配列の右端の次元を変更できます。言い替えると、配列の要素数を 20 個から 10 個または 30 個に変更したり、10×10×5 の配列を 10×7×5 に変更できません。言い替えると、配列の要素数を 20 個から 10 または 30 個に変更したり、10×10×5 の配列を 10×10×10 に変更できます。しかし、10×10×5 の配列を 10×7×5 には変更できません。COM は必要に応じてメモリの解放や割り当て、要素の初期化を行います。

配列の作業が終わったら SafeArrayDestroy を呼び出します。

いくつかの関数は配列記述子(SAFEARRAY 構造体)または個々のデータを処理します。それらの関数としては、SafeArrayAllocDescriptorSafeArrayAllocDescriptorExSafeArrayAllocDataSafeArrayCopyDataSafeArrayDestroyDescriptorSafeArrayDestroyData があります。

最後に、主に UDT 配列(レコード)と IUnknown/IDispatch ポインタを処理するときに役に立つ、現在文書化されていない関数もいくつかあります。SafeArraySetIID SafeArrayGetIID は配列に関連付けられている IID の設定と取得に使います。SafeArrayGetRecordInfo SafeArrayGetRecordInfo はレコード配列のレコード型に対応する IRecordInfo インターフェイスの IID の設定と取得に使います。そして SafeArrayGetVartype は配列を作成したときに「Ex*」版の作成関数に渡した要素の型情報を返します。

C++ ヘルパー クラス

ここでも、セーフ配列用のヘルパー クラス(COleSafeArray)を提供しているのは MFC だけです。このクラスのほとんどのメソッドは、上記の関数とよく似ています。名前さえ、「SafeArray」で始まらない点を除けば同じです。これらは前記の関数を包む軽量のラッパーなので、実行する処理はまったく同じです。

CreateOneDimGetOneDimSizeResizeOneDim と呼ばれるベクトルを扱う 1 組のメソッドがあります。これらのメソッドを使えば、ベクトルを少しだけ簡単に処理できます。

最後に、COleSafeArray には、コンストラクタのほか、代入、比較、アーカイブ書き出し演算子、そして、デバッグ ダンプ コンテキストがあります。

コレクション

オートメーション オブジェクトがコレクションというサブオブジェクトを公開していることはよくあります。コレクションは、ほかのオブジェクトの集まりが入るコンテナです。たとえば、請求書オブジェクトが、Item(細目)オブジェクトの集合入れる Item というコレクションを持っていることが考えられます。規則により、オートメーション コレクションの名前はコレクションに含まれるオブジェクトの型名の複数形とされています。したがって、たとえば Page オブジェクトが入るコレクションは Pages という名前になります。

コレクションは、コレクションに含まれる項目とは独立のオートメーション オブジェクトなので、コレクションとしての処理を行うための専用のオートメーション インターフェイスを持っています。このインターフェイスは、主にコレクション内の項目にアクセスするために使われます。これらのコレクションと MFC や STL の C++ ライブラリで使用できるコレクションを混同しないでください。C++ のコレクションもオブジェクトを保持しますが、それらが保持するのは COM オブジェクトではなく C++ オブジェクトです。また、インターフェイスも異なり、オートメーション クライアントから直接アクセスすることもできません。言い替えると、名前と基本的な考え方が同じ以外はすべて違うのです。

とても基本的なコレクション

コレクションはデータへのアクセスを可能にする以外にはほとんど何もしません(コレクションに Add Remove メソッドがあることもあります)。データにアクセスする最も簡単で標準的な方法は、Count というプロパティと Item というメソッドを公開することです。Count はコレクションが保持している要素の数を示す long 型の値を返します。Item long 型の値をパラメータとして受け取って、指定されたその添え字の位置にある項目を返します。これによって、次のような Visual Basic コードが書けます。

REM Documents is a collection
for i = 1 to Documents.Count
   Documents.Item(i).Visible = false
next i

Document インターフェイスが Visible というプロパティをサポートすると仮定すると、このループはコレクションの全文書を非表示にします。

さらに高度なコレクション

これまでのところ特に問題はありませんが、多くのコレクションでは添え字の指定が効率的にできません。これらのコレクションを扱うために、VisualBasic では for…eachループが使えます。このループはコレクションの先頭から始めて、コレクションの要素を順番に処理します。これを行う Visual Basic コードは次のようになります。

for each doc in Documents
   doc.Visible = false
next i

簡単な上に、かなり効率的です。しかしこの裏では何が行われているのでしょうか。

列挙子を使ったデータへのアクセス

裏を明かせば、Visual Basic は「列挙子」を使ってコレクションにアクセスします。この列挙子はまた別の COM オブジェクトです。この COM オブジェクトは、0 から始まり、インクリメントが可能な添え字のような働きをします。各列挙子オブジェクトはコレクションの位置を表します。配列の添え字を表す integer 変数 ijk に似ています。

for…each が利用できるコレクションには、DISPID_NEWENUM という dispid を持つ _NewEnum というメソッドがなければなりません。_NewEnum という名前の先頭にあるアンダースコアは、このメソッドをオブジェクト ブラウザからは隠すということを示します。

名前が示すように、_NewEnum が新しい列挙子オブジェクトを指す IEnumVARIANT ポインタを返すように書かなければなりません。配列に複数の添え字を持てるように、コレクションも複数の列挙子を持てます。

こうしておけば、クライアントは列挙子の IEnumVARIANT インターフェイスを通じてコレクションにアクセスします。最も重要なメソッドは Next です。このメソッドはコレクションから 1 つまたは複数の要素を取得して、最後に取得した要素の次の様相に列挙子を進めます。ほかにも実装しなければならないいくつかメソッド(CloneResetSkip など)はありますが、Next ほど一般的には使われていません。

そこで前出の for…each について、Visual Basic は内部で次のことをしなけばなりません。

  1. 秘密の _NewEnum メソッドを呼び出して、新しい列挙子オブジェクトへのポインタを取得します。

  2. このポインタを使って Next を呼び出してオブジェクトを取得します。Next がエラーを返した場合はループを終了します。

  3. (ループの中で)オブジェクトを処理します。

  4. 手順 2 に戻ります。

列挙子を実装するには、IEnumVARIANT の 4 つのメソッドを実装しなければなりません。そのために、反復子は現在のオブジェクトを取得するために十分なデータを格納しなければなりません。たとえば、オブジェクトの要素へのポインタ、あるいはコレクションとある種の添え字へのポインタなどです。IEnumVARIANT を正しく実装する限り、データの形式は問われません。ただし、_NewEnum が呼び出されるたびに新しい列挙子を作成しなければなりません。そのため、コレクションを保持しているオブジェクトと同じオブジェクトの中に IEnumVARIANT を実装することはできません。実装したとしたら、コレクションの列挙子は 1 つしか持てません。

列挙子は、_NewEnum メソッドを呼び出したときに、メインのコレクション オブジェクトによって作成されるので、CoCreateInstance を呼び出して列挙子を作成することはありません。そのため、列挙子オブジェクトにはクラス ファクトリは必要なく、IClassFactory を実装する必要もありません(もちろん、IUnknown は実装しなければなりません)。

COM API

列挙子インターフェイス(IEnum...)の定義は別にして、列挙子の実装または使用で COM の助けはありません。

C++ ヘルパー クラス

MFC や Visual C++ の COM サポート クラスには、コレクションの使用や実装を支援するヘルパーはありませんが、ATL は列挙を実装するのに役立つ、一部だけ文書化されているテンプレート クラスをいくつか提供します。これらのテンプレート クラスは文書化されているとは言えない(が、軽く済ませるわけにはいかない)ので、Dr.GUI はここではこれらについて取り上げません。ただし ATL はソース コードで提供されているので、ぜひ自分の目で確かめましょう。最も興味深いクラス テンプレートは CComEnum _Copy です。ただし注意してください。テンプレートのコードを読むのは必ずしも簡単ではありませんよ。

これらのクラスについては、先に紹介した新しい「ATL Internals」本の中に、きわめて詳しく書いてあります。したがって、テンプレートのコードを読む気がしない方は、「ATL Internals」を手に入れて読むとよいでしょう。

詳細エラー情報を渡す

オートメーション メソッドの詳細エラー情報が必要になることはあまりありませんが、必要であれば COM が提供する詳細エラー情報にアクセスする手段を利用することができます。オートメーションに関する初めての記事、または Platform SDK の IDispatch::Invoke に関するドキュメントを思い出してください。Invoke に渡されるパラメータの 1 つは EXCEPINFO 構造体へのポインタだということを覚えているでしょうか。この構造体は、詳細エラー情報を受け取るために使用されます。このエラー情報には、エラー コード、エラーの発生源と内容を示す文字列のほか、さらに詳しい情報を得るためのヘルプ ファイルとコンテキスト ID が含まれています。詳細エラー情報を返すには、IDispatch::Invoke の実装でこの構造体を情報で埋めて、HRESULT として DISP_E_EXCEPTION を返します。

自分で実際に IDispatch::Invoke を実装するのであれば、これで何も問題はありません。しかしほとんどの人はそのようなことはしません。普通はデュアル インターフェイスを書いて、COM がタイプライブラリの情報に基づいてそれを呼び出すようにします。自分では Invoke メソッドを書かないので、構造体のメンバを設定する機会がありません(ただし HRESULT として E_FAIL または別のエラー コードを返せます)。「グローバルな構造体を渡して返してもらう」方法は、マルチスレッド プログラムでも問題が起きる可能性があります。さらに具合の悪いことに、デュアル インターフェイスの vtable 側から呼び出しをした場合、詳細エラー情報を取得する方法はありません。詳細エラー情報は Invoke へパラメータとして渡されるので、この情報を利用できるのはディスパッチ インターフェイスを通じて呼び出しをした場合だけです。

明らかに別の解決法が必要だったので、COM によってそれが提供されました。さらによいことに、この解決策は古い方法と互換性があるため、古い方法を使い続けるべき理由はまったくありません。

新しい方法では、COM が 2 つのインターフェイス(IErrorInfo ICreateErrorInfo)を持つエラー オブジェクトを作成します。ICreateErrorInfo はエラー情報を設定する Set... 関数群を含んでいます。IErrorInfo は設定情報を読み込むための、対応する Get... 関数群を含んでいます。

エラー オブジェクトの作成と呼び出し元への返送

クライアントに詳細エラー情報を渡す必要がある場合、COM API の CreateErrorInfo を呼び出して、これらのエラー情報オブジェクトを 1 つ作成します。COM API の CreateErrorInfo は、エラー オブジェクトの ICreateErrorInfo インターフェイスへのポインタを返します。その後、ICreateErrorInfo メソッドを呼び出してフィールドを埋めます。

次に SetErrorInfo を呼び出して、COM にこのエラー オブジェクトを使うように通知しなければなりません。ただし、SetErrorInfo ICreateErrorInfo ポインタではなく IErrorInfo ポインタを使用します。そのため、QueryInterface を呼び出して正しいポインタを取得しなければなりません。インターフェイスを使い終わったら、両方とも解放するのを忘れないでください。SetErrorInfo AddRef を呼び出してエラー オブジェクトを返せるようにします。

以下はエラー オブジェクトを作成するコードです。

ICreateErrorInfo *pcerrinfo;
IErrorInfo *perrinfo;
HRESULT hr;

hr = CreateErrorInfo(&pcerrinfo);
// set fields here by calling ICreateErrorInfo methods on pcerrinfo
pcerrinfo->SetHelpContext(dwhelpcontext);  // and so forth

hr = pcerrinfo->
     QueryInterface(IID_IErrorInfo, (LPVOID FAR*) &perrinfo);
if (SUCCEEDED(hr))
{
    SetErrorInfo(0, perrinfo);
    perrinfo->Release();
}
pcerrinfo->Release();
// then, eventually...
return E_FAIL;  // E_FAIL or other appropriate failure code

しかしオブジェクトが古い方法(IDispatch::Invoke 経由)で呼び出された場合はどうすればよいのでしょう。COM の標準ディスパッチ実装を使っている場合、エラー オブジェクトからクライアントによって渡された EXCEPINFO 構造体へのデータの移動は、すべて COM が自動的に面倒を見てくれます。

ISupportErrorInfo

オブジェクトが詳細エラー情報を返すことをクライアントに知らせるには、オブジェクトに ISupportErrorInfo を実装しなくてはなりません。ISupportErrorInfo には InterfaceSupportsErrorInfo というメソッドが 1 つだけあり、チェック対象インターフェイスの IID を受け取ります。簡単な実装例を以下に示します。

STDMETHODIMP InterfaceSupportsErrorInfo(REFIID riid)
{
    return (riid == m_iid) ? S_OK : S_FALSE;
}

クライアントでのエラー オブジェクトの取得

ここまでの説明で、オブジェクトの中でエラー オブジェクトを作成して設定する方法と、詳細エラー情報が利用できることをクライアントに通知する方法を学びました。しかし、詳細エラー情報を返す可能性のあるメソッドを呼び出す場合にはどうすればよいのでしょうか。

最初のステップはメソッド呼び出しが返す HRESULT を確認することです。IDispatch::Invoke を使っている場合、エラーは DISP_E_EXCEPTION を含む HRESULT によって示されます。vtable 経由で呼び出している場合は、メソッドが返すエラー コード、一般的には E_FAIL を受け取ることになります。以下のコードのように、FAILED マクロを使ってこれを確認する必要があります。

IDispatch::Invoke 経由でメソッドを呼び出した場合、残りの作業は簡単です。EXCEPINFO 構造体へのポインタを渡したと仮定すれば、どのようなエラーが起きて、どのように対処すべきかを判断するためには、この構造体のメンバを調べます。

vtable 経由でメソッドを呼び出した場合、作業は少しだけ複雑になります。まず、エラーに処理できるかどうかを判断するために、メソッド呼び出しが返す HRESULT を調べる必要があります。処理できない場合には、呼び出したメソッドに同じ HRESULT を返します。

エラーを処理できると判断したら、オブジェクトを対象に QI を実行して、オブジェクトが ISupportErrorInfo をサポートするかどうかを調べます。サポートする場合は、InterfaceSupportsErrorInfo を呼び出してエラー オブジェクトが有効かどうかを調べます。

有効であれば、GetErrorInfo を呼び出してエラー オブジェクトへのポインタを取得します。これはエラー オブジェクトへの IErrorInfo ポインタです。IErrorInfo メソッドを使ってエラーについての情報を調べて、必要な処理をします。

GetErrorInfo を呼び出すとスレッドからエラー情報が消去されます。したがって、エラーを処理できると判断した場合にだけ呼び出すようにします。

vtable 呼び出しのコードは次のようになります。

HRESULT hrMethod = pVtableInterface->MethodCall();
if (FAILED(hrMethod)) {
   if (bCanHandle(hrMethod)) { // true if I can handle this error
      ISupportErrorInfo *pSupport;
      HRESULT hr =
         pVtableInterface->QueryInterface(IID_ISupportErrorInfo,
                                             &pSupport);
      if (SUCCEEDED(hr)) {
         hr = pSupport->InterfaceSupportsErrorInfo(IID_IYours);
         if (hr == S_OK) { // can't use SUCCEEDED here! S_FALSE succeeds!
            IErrorInfo *pErrorInfo;
            hr = GetErrorInfo(0, &pErrorInfo);
            if (SUCCEEDED(hr)) {
               // FINALLY can call methods on pErrorInfo!
               // ...and handle the error!
               pErrorInfo->Release();  // don't forget to release!
            }
         }
       pSupport->Release();
      }
   }
   return hrMethod; // couldn't handle
}
// no error—continue

ちっぽけなメソッド呼び出しにしては、ずいぶんたくさんのコードを書くもんだと思っている方もいるでしょう。Dr.GUI も同意見です。少なくとも、IErrorInfo ポインタを取得する処理を、ほかでも再利用できる関数にカプセル化できるはずです。もっといいのは、次のいずれかを使うことです。

C++ ヘルパー クラス

MFC は、COleDispatchException クラスと AfxThrowOleDispatchException グローバル API という形で詳細エラー情報を返す支援機能を提供します。基本的に、詳細エラー情報をクライアントに渡すには、COleDispatchException オブジェクトを作成して初期化し、AfxThrowOleDispatchException に渡すだけです。

MFC アプリケーションが、COleDispatchDriver から派生したクラスを通じてオートメーション オブジェクトを呼び出している場合、オブジェクトがエラー返すと COleDispatchException(または COleException)がスローされます。これらは try/catch ブロックで捕らえることができます。前出のネストされた if 文よりはるかに簡単です。

ATL は、エラー情報を渡したり受け取ったりするためのヘルパー オブジェクトを提供しませんが、ISupportErrorInfo の実装は提供します。ISupportErrorInfo を実装したい場合には、オブジェクトを作成するときに ATL Object Wizard のプロパティ ページの[アトリビュート(Attribute)]タブの[ISupportErrorInfo サポート(Support ISupportErrorInfo)]をチェックするだけです。これをチェックすると、ISupportErrorInfo が継承リストに追加され、クラスに InterfaceSupportsErrorInfo の比較的高度な実装が追加され(後で編集できます)、COM_MAP に適切なエントリを追加します。また、シングル インターフェイスでエラー情報を返すだけの場合は、ISupportErrorInfoImpl テンプレート クラスを使うことができます。

詳細エラー情報をサポートするコンパイラの COM クラスは実際とても役に立ち、MFC にとてもよく似ています。_com_error クラスがエラー情報をカプセル化します。エラーをスローするには、説明したとおりにエラー オブジェクト情報を作成して HRESULT と合わせて _com_raise_error に渡します。

スマート ポインタを使って(通常 #import を使って生成される _com_error スマート ポインタ クラスを使って)呼び出した COM メソッドがエラーを返した場合、ヘルパー関数は _com_error 例外をスローします。catch ブロックの中で _com_error オブジェクトに対してクエリー実行して詳細エラー情報を取得し、エラーを処理するのは簡単なことです。前出のコードよりはるかに簡単です。ATL プログラムも含め、どのプラグラムでもコンパイラの COM サポート クラスが使えます。

やってみよう

Dr.GUI はみなさんが今までお話してきたことを本当にわかっているかどうかは、実際に試すまではわからないと思っていると思います。

  • 前回のサンプルをベースに、VARIANT、BSTR 文字列、日付、通貨オブジェクト、10 進数、セーフ配列を作成、操作、破棄するプログラムを書きましょう。

  • これらのデータ オブジェクトを、オートメーション オブジェクトとの間でやり取りしてみましょう。

  • コレクションを使ってオブジェクト グループをやり取りしてみましょう。Visual Basic も対象にしてみましょう。

  • 詳細エラー情報を使ってみましょう。

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

今回はみなさんの予想以上に、COM のデータ型について詳しく取り上げました。Dr.GUI も当初予定していた以上に書いてしまいした。

次回は、接続ポイントを含め、イベントと COM のイベント モデルの謎に迫ります。

表示: