proxy/stubの利用 - インターフェースの定義

いちごパック > COM/ActiveXの解説 > proxy/stubの利用 - インターフェースの定義
同一ApartmentであればCOMシステムを介さず直接呼び出せますので、 好きな引数、好きな戻り値が使用できます。 しかし、Apartmentを超える場合はCOMシステムの仲介によりメソッド呼び出しを実現します。 そのため、メソッドにはいくつかの制約があります。
  • 戻り値はHRESULTとする。COMシステムでエラーが起こった場合に、対応するHRESULT値を返すためです。
  • 引数にデータへのポインタを含む場合は、その内容を明確にCOMシステムに示す。そうでないと、別のプロセスにデータを渡すことができません。
  • 引数に処理(クラスや関数など)へのポインタを含めたい場合は、その呼び出しに対してCOMシステムの介在が必要になるため、決められた方法で渡す。
  • またCOMの制約として、インターフェース(IUnknown等)へのポインタを渡す場合は、巡回参照が起こらない設計にする必要があります。
    このように制約はありますが、関数ポインタやCOMでないクラスは渡せないと考えておけば十分でしょう。

    インターフェースの定義方法

    まずCOMから離れて、次の関数を考えてみます。
    void set_data(int num, int* data);
    
    この関数を呼び出す場合、呼び出し側は、
  • 数値の配列(data)
  • 配列の要素数(num)
  • の2つを用意しています。 呼び出し側の疑似コードは、例えば次のようになるでしょう。
    int data[] = {1,2,3,4,5}
    int num = 5;
    set_data(num, data);
    
    呼び出しにシステムを仲介させようと考えると、 この関数インターフェースは明確ではありません。 例えば、set_data()の引数であるdataは、int値を含むポインタを渡しているだけかもしれません。 配列であったとしても、その要素数がnumであることはわかりません。 また、*dataに数値をセットして返す関数とも区別できません。
    インターフェースを定義するには、 引数が入力であるのか、出力であるのか、ポインタが配列を表すのか、 また配列であれば要素数はいくつであるのか、を明確にする必要があります。
    midlでは、これらの追加情報を与えるために、 引数の前に[]で括った属性を与えられるようになっています。 (何も与えない場合はデフォルトの属性が使われます)
    先ほどの関数では、次のように与えるとその意味が明確になります。
    void set_data(
      [in] int num,
      [in, size_is(num)] int* data);
    
    ここでinやsize_isといった属性が出てきました。 これらの意味は次の通りです。
    属性内容
    inこの引数は入力である。
    outこの引数は出力である。
    size_is(x)この引数は可変長配列であり、そのサイズはxである。ここで、xは引数を使った計算式です。固定長配列の場合はsize_isを使わずに、要素数つきの配列を引数として渡します。
    size_is(x,y)この引数は2次元の可変長配列であり、そのサイズは[x][y]である。
    length_is(x)この引数は可変長配列であり、そのうち意味のあるデータのサイズはxである。
    これをふまえて、IIchigo7というCOMのインターフェースを定義する例を示します。 この例はそのままmidlでコンパイル可能です。
    //
    // Copyright (c) 2018 The Ichigopack Project (http://ichigopack.net/).
    //
    
    import "unknwn.idl";
    
    // IIchigo7 interface
    [
        object,
        uuid(8F76DB6F-9539-46A7-BB60-049681B4144B),
        pointer_default(unique)
    ]
    interface IIchigo7 : IUnknown
    {
        HRESULT ReadBuf(
            [in] DWORD len,
            [out] DWORD* plen_read,
            [out, size_is(len), length_is(*plen_read)] BYTE* pbuf);
        HRESULT Read(
            [out] DWORD* plen_read,
            [out, size_is(,*plen_read)] BYTE** ppbuf);
        HRESULT WriteData(
            [in] DWORD len,
            [in, size_is(len)] BYTE* pdata);
    };
    
    // Type library
    [
        version(1.0),
        uuid(5AE2BCDB-1653-4BBB-A368-50F90E9E6ADB)
    ]
    library Ichigo7TypeLib
    {
        importlib("stdole32.tlb");
        interface IIchigo7;
        [ uuid(17C60F8A-B0B1-438B-A1EC-95336DB669E8) ]
        coclass Ichigo7
        {
            interface IIchigo7;
        };
    };
    
    ReadBuf()とRead()はいずれもデータ読み込みメソッドです。 ReadBuf()は引数としてバッファサイズを与える例、 Read()はバッファ自体をメソッド実装側で確保する例です。 Write()はデータ書き出しメソッドの例です。
    このidlでは各データの扱いと配列のサイズが明確に定義されていますので、 proxyとstubのコードを生成させ、それらをビルドすれば、 COMシステムと協調して必要なデータの送受信を行えます。
    なお、COMの引数として渡すデータに対してメモリの確保や解放を行う場合は、 proxyやstubのコードと同じメモリ管理関数を使う必要があるため、 次の関数を使うことになっています。
    LPVOID CoTaskMemAlloc(SIZE_T cbMem);
    void CoTaskMemFree(LPVOID pvMem);
    
    使い方はC言語のmalloc()、free()と同じです。

    サンプルコード

    IIchigo7メソッド実装部分は、例えば次のようになるでしょう。 なお、IIchigo7をDLLサーバとして実装する場合は、 仕組みで説明したDllSurrogateに空のパスを設定しておくと、 EXEサーバとしても動くようになります。
    static const char g_ichigodata[] = "Ichigopack http://ichigopack.net/";
    
    HRESULT STDMETHODCALLTYPE CIchigo7::ReadBuf( DWORD len, DWORD* plen_read, BYTE* pbuf) { DWORD len_full = strlen(g_ichigodata); if (len > len_full) { len = len_full; } memcpy(pbuf, &g_ichigodata[0], len);
    *plen_read = len; return S_OK; }
    HRESULT STDMETHODCALLTYPE CIchigo7::Read( DWORD* plen_read, BYTE** ppbuf) { *plen_read = 0; *ppbuf = NULL;
    DWORD len_full = strlen(g_ichigodata); BYTE* pbuf = (BYTE*)CoTaskMemAlloc(len_full); if (pbuf == NULL) return E_OUTOFMEMORY; memcpy(pbuf, &g_ichigodata[0], len_full);
    *ppbuf = pbuf; *plen_read = len_full; return S_OK; }
    HRESULT STDMETHODCALLTYPE CIchigo7::WriteData( DWORD len, BYTE* pdata) { return suppLogFileAppendW( LOGFILENAME, pdata, len ); }
    また、このサンプルインターフェースの呼び出しコードは、 例えば次のようになります。 proxyとstubを登録しておくと、 ICHIGO7_SERVER_TYPEをCLSCTX_LOCAL_SERVERとしても動くようになります。
    //
    // Copyright (c) 2018 The Ichigopack Project (http://ichigopack.net/).
    //
    
    #include <windows.h>
    #include <ole2.h>
    #include <iostream>
    #include <string>
    #include <iomanip>
    #include "ichigocom7.h"
    
    
    #if !defined(ICHIGO7_SERVER_TYPE)
        #define ICHIGO7_SERVER_TYPE \
            CLSCTX_INPROC_SERVER
    #endif
    
    int main(int argc, char** argv)
    {
        HRESULT hr;
        hr = CoInitialize(NULL);
        if ( FAILED(hr) ) {
            std::cout << "CoInitialize() error: " << hr << std::endl;
            return 1;
        }
        IUnknown* punk = NULL;
        IIchigo7* pObj = NULL;
        hr = CoCreateInstance(
                 CLSID_Ichigo7, NULL,
                 ICHIGO7_SERVER_TYPE,
                 IID_IUnknown, (LPVOID*)&punk);
        if ( FAILED(hr) ) {
            std::cout << "CoCreateInstance() error: "
                      << std::hex << std::setfill('0')
                      << std::setw(8) << hr << std::endl;
        }
        if ( SUCCEEDED(hr) ) {
            hr = punk->QueryInterface(
                IID_IIchigo7, (LPVOID*)&pObj);
            punk->Release();
            if ( FAILED(hr) ) {
                std::cout << "QueryInterface() error: "
                          << std::hex << std::setfill('0')
                          << std::setw(8) << hr << std::endl;
            }
        }
        if ( SUCCEEDED(hr) ) {
            BYTE  localbuf[100];
            BYTE* pbuf = NULL;
            char data[] = "ichigopack COM sample 7\n";
            DWORD dw = 0;
    
            hr = pObj->ReadBuf(15, &dw, localbuf);
            if ( SUCCEEDED(hr) ) {
                std::cout.write((char*)localbuf, dw);
                std::cout << std::endl;
            }
            hr = pObj->ReadBuf(100, &dw, localbuf);
            if ( SUCCEEDED(hr) ) {
                std::cout.write((char*)localbuf, dw);
                std::cout << std::endl;
            }
            hr = pObj->Read(&dw, &pbuf);
            if ( SUCCEEDED(hr) ) {
                std::cout.write((char*)pbuf, dw);
                std::cout << std::endl;
                CoTaskMemFree(pbuf);
            }
    
            hr = pObj->WriteData( strlen(data), (BYTE*)(&data[0]) );
            pObj->Release();
        }
        CoUninitialize();
        return 0;
    }
    
    

    目次

  • COM/ActiveXの解説ページ 目次
  • 仕組み
  • インターフェースの定義
  • ビルド