読者です 読者をやめる 読者になる 読者になる

COMの簡単な解説

そもそもまともな日本語資料が少ないっぽい

英語ドキュメント読めないとしんどい世界なのかな。

COMプログラミング


※OLE2T が実行されるたびに、スタック領域が消費されるのでループ内での使用や、非常に大きな文字列の使用は避けてね!


消えるとこまるのでコピペしておく

COMプログラミング

 COM...正直言ってプログラミングではかかわらないほうが良い。それでも尚COMをやりたいと言うならば、このページが助けになる場面もあろう。何故なら、このページに書かれていることは私が実際にAnpsockやLHAコントロールを作成したときに大いに頭を悩ませた事ばかりだからである。
 日本語で書かれたCOMのドキュメントは数少ない。決して趣味でやろうなどと考えてはいけない。(私は趣味だが)本文中の記述例およびサンプルは、すべてVB6またはVC6で書いてある。

 記述例およびサンプルに含まれるファイルの全部、または一部を使用したことによる損害等について、一切の責任を負いません。

COM
VARIANT
[VC]SAFEARRAY
BSTR
[VC,ATL]VARIANTによる引数の省略
[VC,ATL]SAFEARRAYをVARIANTで受ける
[VC,ATL]SAFEARRAYをVARIANTの引数で返す
[VC,ATL]コントロールを実行時不可視にする
[VC,ATL]BSTR型のプロパティ
[VC,ATL]_ATL_MIN_CRTマクロ
[VC,ATL]OLE2Tマクロ
[VC,ATL]Date型のプロパティ
COM

 COMとはComponent Object Modelの略である。COMにはいろいろな側面があるが、敢えて一言で言うならばコードをバイナリレベルで再利用するための仕組みと言える。最も多いのがVisual C++で作成したコンポーネントをVisual Basicで利用することではないだろうか。
 一般に、あるプロセスにおけるアドレス値は、別のプロセスでは意味を持たない。これは、プロセス毎にシステムから異なるアドレス空間を与えられるからだ。ところが、COMではインプロセスはもちろんのこと、アウトプロセス、更にはリモートまで、プロセスやマシンの境界を越えてのメソッド呼び出しが可能だ。これを実現するための仕組みがマーシャリングである。

VARIANT

 (.NETではない)Visual BasicではおなじみのVariant型の実体はこのVARIANT構造体である。格納されているデータの型情報とデータの実体部分の共用体をフィールドに持つ構造体で、COMでは非常に重要だ。メソッドの引数を省略可能にするときもこのVARIANTを使う。
 一般的なlong、short、doubleなどのほか、後述するSAFEARRAYやBSTRも格納することができる、Visual Basicにおいては非常に柔軟なデータ型となる。ただし、単純なデータ型に比し多少コストはかかる。

[VC]SAFEARRAY

 Visual Basicの配列の実体はこのSAFEARRAYで、次元や要素数とデータを指すポインタのフィールドを持つ構造体である。SAFEARRAYもCOMでは非常に重要となる。
 SAFEARRAYの簡単なサンプルを作成した。COMのと言うわけではなく単にSAFEARRAYのサンプルとなっている。

SAFEARRAYの簡単なサンプル(ソースファイルのみ)

BSTR

 簡単に言うとサイズ情報をもった文字列型である。BSTRを扱うためのCComBSTRというクラスもあり、操作はそれほど難しくない。
[VC,ATL]VARIANTによる引数の省略

 COMではIDLでVARIANTの引数に[optional]を記述することで、引数の省略をすることが可能になる。その場合、省略された引数のvtフィールドははVT_ERRORになる。(注:MSDNのドキュメントには[optional]はVARIANTかVARIANT*でないと、意味をなさないとある。私が試した限りでは、longパラメータなどに[optional]キーワードをつけても省略可能になるが、このメソッドの中で省略したlongパラメータの値を見ると0になっていた。つまり、メソッドは値0で呼び出されるようだ。)
<idlファイルの例>

        [id(12), helpstring("メソッド Connect")] HRESULT Connect([in,optional] VARIANT RemoteHost,[in,optional] VARIANT RemotePort);
<メソッドの記述例>

STDMETHODIMP CSockCtl::Connect(VARIANT RemoteHost, VARIANT RemotePort)
{
    if(RemoteHost.vt == VT_ERROR)   // 引数が省略された?
        // 何らかの処理;
[VC,ATL]SAFEARRAYをVARIANTで受ける

 ATLで作成したコンポーネントのクライアントは、Visual Basicが主なものであろう。例えばVisual Basicでこんなコードを書いたとする。
<クライアント(Visual Basic)の記述例>

    Dim bytData(16383) As Byte

    askServer.SendData bytData
 ATL側はこんな感じで書く。

<idlファイルの記述例>

        [id(6), helpstring("メソッド SendData")] HRESULT SendData([in] VARIANT data);
<メソッドの記述例>

STDMETHODIMP CSockCtl::SendData(VARIANT data)
{
    SAFEARRAY   *psa;
    char*   buf;

    if(data.vt & VT_BYREF)
        psa = *(data.pparray);
    else
        psa = data.parray;

    SafeArrayAccessData(psa,(void**)&buf);
ATLTRACE(_T("cElements = %d\n"),psa->rgsabound->cElements);

    // bufを使って何らかの処理

    SafeArrayUnaccessData(psa);
[VC,ATL]SAFEARRAYをVARIANTの引数で返す

 SAFEARRAYをクライアントから引数で渡されたVARIANTに格納して、クライアントに返すことができる。Visual Basicでこんなコードを書いたとする。
<クライアント(Visual Basic)の記述例>

    Dim bytData() As Byte

    askServer.GetData bytData
Debug.Print "bytData(0) = " & bytData(0)
Debug.Print "bytData(1) = " & bytData(1)
Debug.Print "Ubound = " & Ubound(bytData)
 ATL側ではこんな感じで書く。

<idlファイルの記述例>


        [id(4), helpstring("メソッド GetData")] HRESULT GetData([in,out] VARIANT* data,[in,optional] VARIANT type,[in,optional] VARIANT maxLen);
data);
<メソッドの記述例>

STDMETHODIMP CSockCtl::GetData(VARIANT* data, VARIANT type, VARIANT maxLen)
{
    char*   buf;
    SAFEARRAY   *psa;
    SAFEARRAYBOUND  rgb[1];

ATLTRACE(_T("vt = %u\n"),data->vt); // ちなみにvtの値はVT_UI1 | VT_ARRAY | VT_BYREFです
    if(data->vt & VT_BYREF)
        psa = *(data->pparray);
    else
        psa = data->parray;
ATLTRACE(_T("psa1 = %p\n"),psa);    // この時点ではpsaは0

                                    // 0~9のBYTE配列SAFEARRAY作成
    rgb[0].cElements = 10;
    rgb[0].lLbound = 0;
    psa = SafeArrayCreate(VT_UI1,1,rgb);

ATLTRACE(_T("psa2 = %p\n"),psa);
                                    // データへのポインタ取得
    SafeArrayAccessData(psa,(void**)&buf);
    buf[0] = 91;
    buf[1] = 92;
    SafeArrayUnaccessData(psa);

                                    // VARIANTにSAFEARRAYを格納
    if(data->vt & VT_BYREF)
        *(data->pparray) = psa;
    else
        data->parray = psa;
 SAFEARRAYの使用するメモリ領域の解放は、クライアント側でVARIANTを解放するときにやってくれる(はず)。

[VC,ATL]コントロールを実行時不可視にする

 コントロールを実行時不可視にするには、「ATLオブジェクトの新規作成」で「その他」の「その他ステータス」で「実行時には不可視」をチェックする。が、もう作っちゃってからそんなこと気づいても遅い。それで後から実行時不可視にできないかと思い調べていると、どうもレジストリに登録するときの値の設定だけが影響するようだ。
 となると.rgsファイルが臭そうである。デフォルトのプロジェクトを作成し、1つは「実行時には不可視」をチェック、もう1つはチェックしないで.rgsを比較してみた。すると、不可視のほうは'1' = s '132497'、不可視でないほうは'1' = s '131473'で変化している行を見つけた。16進値にすると132497=0x20591、131473=0x20191で1ビットだけ変化している。私は.rgsファイルを開いて以下のような修正を加えてみた。

                '1' = s '132497'
 この方法でうまくいった。この件に関しては他に情報がないので、これで正しいかどうかは分からないが、今のところ問題は発生していない。
[VC,ATL]BSTR型のプロパティ

 BSTR型のプロパティは以下のようにする。getメソッドではCopyメソッドを使って値を設定する。
<idlファイルの記述例>

        [propget, id(10), helpstring("プロパティ RemoteHost")] HRESULT RemoteHost([out, retval] BSTR *pVal);
        [propput, id(10), helpstring("プロパティ RemoteHost")] HRESULT RemoteHost([in] BSTR newVal);
<メソッドの記述例>

    CComBSTR    m_RemoteHost;

STDMETHODIMP CSockCtl::get_RemoteHost(BSTR *pVal)
{
    // TODO: この位置にインプリメント用のコードを追加してください
    *pVal = m_RemoteHost.Copy();

    return S_OK;
}

STDMETHODIMP CSockCtl::put_RemoteHost(BSTR newVal)
{
    // TODO: この位置にインプリメント用のコードを追加してください
    m_RemoteHost = newVal;

    return S_OK;
}
[VC,ATL]_ATL_MIN_CRTマクロ

 CRTはCathode Ray Tubeではない。私はしばらくの間、そうだと勝手に思い込んでいて、「何かの表示のオプションかな」と気にしていなかった。本当はC RunTime libraryのことらしい。で、本題に入るがこのマクロを定義しておくと、Cのランタイムをリンクしない。(正確に言うとちょっと違うらしい)
 このためCのランタイムを使用する場合は、このマクロを削除しなければならない。(リリースビルドではデフォルトで定義されている)私はAnpsockを作るにあたって、CreateThread関数を使っていることもあり、Cのライブラリは使わないようにしている。

 にもかかわらず、あるときリンクでエラーが出てしまった。しかし、いくらコード全体を見直してもCのライブラリは使用していない。仕方がないので、プロパティ、メソッドを全てコメントアウトし、1つずつ生かして地道に調べた。すると、なんとConnectメソッドのところで適当に使っていた_bstr_tがこのエラーの原因であった。

 こりゃー正直言って分かんないよね。_bstr_tを削除してやっとリンクが通った。

<件のエラー>

LIBCMT.lib(crt0.obj) : error LNK2001: 外部シンボル "_main" は未解決です
[VC,ATL]OLE2Tマクロ

 デバッグなどでBSTRの中身を見たいときそのままではATLTRACEに渡せない。BSTRをTSTRに変換するためにOLE2Tマクロが用意されている。これはデバッグに限らず非常に重宝する。逆の変換を行うT2OLE等も用意されている。
 このマクロを使用するためにはそれより先にUSES_CONVERSIONマクロを記述しなければならない。変換のためのリソースを確保するためのもののようなのでループの内側などで記述してはならない。

<呼ぶ側>

    USES_CONVERSION;    // OLE2Tを使うためのマクロ
    if(createSocket(OLE2T(m_RemoteHost.m_str),m_nRemotePort) != 0)
<呼ばれる側>

int CSockCtl::createSocket(LPTSTR    remoteHost,long nRemotePort)
{
ATLTRACE(_T("remoteHost = %s nRemotePort = %d\n"),remoteHost,nRemotePort);
[VC,ATL]Date型のプロパティ

 COMのDate型は何故かdoubleになっている。APIにSystemTimeToVariantTimeとVariantTimeToSystemTimeという関数が用意されており、SYSTEMTIMEとVARIANT time(Date型)との相互変換が用意に行える。これらを用いれば簡単にDate型のプロパティを記述できる。
<idlファイルの記述例>

        [propget, id(4), helpstring("プロパティ LastWriteTime")] HRESULT LastWriteTime([out, retval] DATE *pVal);
<メソッドの記述例>

STDMETHODIMP CFileItem::get_LastWriteTime(DATE *pVal)
{
    // TODO: この位置にインプリメント用のコードを追加してください
    SystemTimeToVariantTime(&m_SystemTime,pVal);
    return S_OK;
}
Copyright(C) 2001-2003 Allergy Design Office All rights reserved.

[Home]