ゼロからのCOM - COMの目的とその仕組み

いちごパック > COM/ActiveXの解説 > ゼロからのCOM - COMの目的とその仕組み

COMとは何か?

ソフトウェアには様々なプログラミング言語、技術、プログラミングテクニックが使われています。 便利で高機能なソフトウェアの多くは、これらの組み合わせで成り立っています。 COM(Component Object Model)とは、 システムとしてこれらをWindowsプログラマに提供するためのプログラミングツールです。
COMは大雑把にいえば、次の機能を持つプログラミングツールです。
  • インターフェースクラス利用の強制(クラスの強制的な抽象化)
  • 参照カウントによるクラスオブジェクトの管理
  • 実行中のプログラムとは別のDLLやEXEに収められたクラスの作成と解放
  • 既存クラスに対するインターフェースクラスの追加(クラスの拡張)
  • 非互換なバイナリ呼び出しの仲介
  • 異なる言語やCPUビット数でつくられたバイナリ呼び出しの仲介
  • スクリプト言語によるバイナリ呼び出しの仲介
  • COMは多機能ですが、コンパイラに依存した機能(ガベージコレクションなど)をあまり使っていません。 したがって、C言語、C++言語、C#言語、Visual Basic、VBScript、JavaScript(JScript)など、 様々な言語に対応できます。
    C++言語ではCOMのすべての機能を利用できますが、 言語によってはその利用が一部の機能に限定されるため、 互換性を考慮したプログラミングが必要になることもあります。

    インターフェースとオブジェクト

    COMシステムは、ライブラリ(COMサーバ)のオブジェクトを、 呼び出し側(COMクライアント)から呼び出すための機能を提供します。 COMシステムでは、次のような形で呼び出しが実現されています。 C++言語を利用する場合、COMはC++のclassを利用したオブジェクト指向のプログラミングコードとして実装できます。 COMとC++言語の対応は次のようになります。
    COMC++言語
    インターフェース純粋仮想関数のみのインターフェースクラス
    オブジェクトインターフェースクラスを継承し、それを実装したクラス
    COMシステムを用いたオブジェクトの確保と解放には、次の関数を用います。
    確保CoCreateInstance
    解放すべてのオブジェクトが持つRelease()メソッド
    COMのオブジェクトは参照カウントにより管理されているため、 Release()メソッドは実際には参照カウントを1減らすメソッドになります。
    すべてのオブジェクトはRelease()メソッドのほかに、AddRef()メソッドとQueryInterface()メソッドを持ちます。 AddRef()メソッドはオブジェクトの参照カウントを1増やすメソッドです。
    QueryInterface()メソッドは、多重継承など、オブジェクトが複数のインターフェースを実装している場合に、 オブジェクトが備える他のインターフェースを取得するために利用できます。 インターフェースを取得した場合、そのポインタを返すために参照カウントが1増やされています。

    COMとシステム

    COMではシステム全体の情報をレジストリで管理し、 プロセス固有の情報はApartmentと呼ばれるグループ単位で管理しています。 COMはスレッド単位で初期化でき、 初期化されたスレッドはApartmentに所属させることができます。 COMが管理するオブジェクトは、オブジェクト作成時にスレッドが所属するApartmentに所属します。
    スレッドやオブジェクトを複数のApartmentに所属させることはできません。 Apartmentに所属していないスレッドは、COMが管理するオブジェクトを作成できません。
    スレッド、Apartment、オブジェクトのルールをまとめると次のようになります。
    Apartment0〜複数のスレッド、0〜複数のオブジェクトを所有できる。
    スレッド0〜1つのApartmentに所属できる。
    オブジェクト1つのApartmentに必ず所属する。

    GUID

    各プログラマが自由にクラス名やインターフェース名を与えると、 同じ名前を使ってしまうことがあります。 この対策としてCOMでは、 クラス名やインターフェース名に128ビットの数値を割りあてて、 システムがクラス名やインターフェース名を識別する際に128ビットの数値を用います。
    クラス名やインターフェース名の識別に用いる128ビットの数値をGUID(Globally Unique Identifier)といいます。 クラス名を識別するIDはCLSID、インターフェース名を識別するIDはIIDと呼ばれます。 また、GUIDはUUID(Universally Unique Identifier)と呼ばれることもあります。
    GUIDの生成には、Visual Studioに付属しているguidgenか、 guidgenのコマンドライン版であるuuidgenというプログラムを利用します。 これらのプログラムはイーサネットアドレス等を利用して、 他の環境で生成されたGUIDも含めて一意となるような128ビットの数値を生成してくれます。
    C++のプログラミングではGUIDは構造体として扱われます。 説明のために構造体の要素には仮の名前を与えましたが、要素には直接アクセスできないと考えてください。
    typedef struct {
      unsigned long  __data32;
      unsigned short __data16_1;
      unsigned short __data16_2;
      unsigned char  __data8[8];
    } GUID;
    typdef GUID UUID;
    typdef GUID IID;
    typdef GUID CLSID;
    #define REFGUID const GUID &
    #define REFIID REFGUID
    #define REFCLSID REFGUID
    
    GUIDは128ビットの数値ですがC++では構造体として扱われるため、 多少の問題が起こります。
    構造体はそのままでは比較できませんが、 これについてはIDが等しいかを調べる関数が用意されています。
    inline bool operator==( REFGUID id1, REFGUID id2 )
    {
      return 0 == memcmp( &id1, &id2, sizeof(GUID) );
    }
    inline bool operator!=( REFGUID id1, REFGUID id2 )
    {
      return 0 != memcmp( &id1, &id2, sizeof(GUID) );
    }
    
    また、GUIDは構造体として初期化し、その値を参照する形で利用します。 具体的には、例えば、まずヘッダファイルで次のように定義します。
    extern const CLSID CLSID_Ichigo1;
    
    次に、いずれかの.cppファイルで次のように定義します。
    const CLSID CLSID_Ichigo1 = 
    { 0x227a0711, 0xd089, 0x42aa, { 0x84, 0x28, 0x52, 0xdd, 0x48, 0xa1, 0x41, 0x39 } };
    
    これでCLSID_Ichigo1というGUIDが利用できるようになります。
    Windowsでは、extern定義とcppでの実装の両方をまとめて1つのコードで表現するために、 DEFINE_GUIDというマクロを用意しています。DEFINE_GUIDマクロは次のように使います。
    DEFINE_GUID(CLSID_Ichigo1,
    0x227a0711, 0xd089, 0x42aa, 0x84, 0x28, 0x52, 0xdd, 0x48, 0xa1, 0x41, 0x39);
    

    DEFINE_GUIDマクロは、initguid.hをソースコードの先頭でインクルードした場合には、
    const CLSID CLSID_Ichigo1 = 
    { 0x227a0711, 0xd089, 0x42aa, { 0x84, 0x28, 0x52, 0xdd, 0x48, 0xa1, 0x41, 0x39 } };
    
    のように展開されます。 initguid.hをインクルードしなかった場合には、
    extern const CLSID CLSID_Ichigo1;
    
    の形で展開されます。 したがって、DEFINE_GUIDマクロをヘッダファイルに収めておき、 .cppファイルのうち1つだけ、
    #include <initguid.h>
    
    を入れた後でヘッダファイルを#includeし、 それ以外のコードではヘッダファイルをそのまま#includeすれば、 CLSID_Ichigo1はどのソースコードからでも利用できるようになります。
    どのソースコードからもinitguid.hをインクルードしなかった場合には、 CLSID_Ichigo1が存在しないため、リンクエラーになります。

    文字列とBSTR

    C++言語では文字列をchar(ANSI)やWCHAR(UNICODE)の配列として表しますが、 COMの多くのインターフェースでは、文字列をBSTRと呼ばれる長さつきの配列として扱います。 BSTRはCOMの管理するメモリ上で確保、解放されるUNICODEの文字列です。
    C++のUNICODE文字列は、SysAllocString()関数またはSysAllocStringLen()関数でBSTRに変換できます。 確保したBSTR文字列は、利用後にSysFreeString()関数で解放する必要があります。 これらのAPIは次の形をしています。
    BSTR SysAllocString( const WCHAR* pstr );
    BSTR SysAllocStringLen( const WCHAR* pstr, UINT len_in_wchar );
    void SysFreeString( BSTR bstr );
    
    SysAllocString()は文字列の最後にL'\0'がある場合に利用できます。 SysAllocStringLen()では len_in_wchar に(L'\0'を含まない)文字列の長さを与える関数で、 文字列の最後にL'\0'がなくても利用できます(長さは文字数で、バイト数ではありません)。
    COMのネイティブな文字列がUNICODEであるため、C++言語でも文字列をUNICODEで扱うと便利です。 UNICODE文字列はWCHARの配列で、ANSIの文字列とは次のメソッドで相互変換します。
    ANSI→UNICODEMultiByteToWideChar
    UNICODE→ANSIWideCharToMultiByte
    MultiByteToWideChar、WideCharToMultiByteは次の形をしています。
    int MultiByteToWideChar(
      UINT codepage, DWORD dwFlags,
      const char* pstr, int len_str,
      WCHAR* pwstrbuf, int len_wstrbuf);
    int WideCharToMultiByte(
      UINT codepage, DWORD dwFlags,
      const WCHAR* pwstr, int len_wstr,
      char* pstrbuf, int len_strbuf,
      const char* pdefault, BOOL* pdefault_flag
    
    システムネイティブ(日本語版WindowsであればShift JISコード)の文字列をUNICODEに変換したい場合は、codepageにCP_ACPを与えます。 dwFlagsは変換できない文字の扱いを指定するものですが、0としておくとWindowsデフォルトの動作をします。 WCHARの長さは文字数で与えます(バイト数ではありません)。 pdefaultは変換できない場合の置き換え文字を与えるものです。 NULLでかまいませんが、例えばpdefaultに"?"を与えると?になります。 pdefault_flag置き換え文字を使用したかを表すフラグを返します。NULLでもかまいません。
    UNICODE文字列を直接ソースコードに書きたい場合は、文字列の先頭にLをつけます。
    const WCHAR* wichigo = L"Ichigopack";
    
    wichigoをBSTRに変換する場合は、次のようにします。
    BSTR bstrichigo = SysAllocString( wichigo );
    BSTRを必要とするメソッド呼び出しを実行
    SysFreeString( bstrichigo );
    

    COMのエラーコード

    システムによって特別扱いされるAddRef()とRelease()を除き、 COMのメソッドはHRESULTと呼ばれるCOM全体で統一されたエラーコードを返します。 COMシステムでは、メソッド呼び出しインターフェースを介して毎回COMを呼び出します。 エラーコードを統一することにより、この呼び出し中にCOM部分でエラーが起こった場合でも、 メソッド呼び出しへの戻り値という形でエラーコードを返せることになります。
    HRESULTのコードのうちいくつかの例を以下に示します。
    エラーコードSUCCEEDEDマクロは真か内容
    S_OKYes成功し、その戻り値はOK
    S_FALSEYes成功し、その戻り値はFALSE
    E_FAILNo一般的な失敗
    E_NOTIMPLNo未実装
    E_OUTOFMEMORYNoメモリ不足
    このように、HRESULTには複数の成功コードと複数の失敗コードがあります。 また、呼び出し中にCOM部分でエラーが発生した場合には、別のエラーコードが返されます。
    そこで、成功か失敗かを判別するために、次の2つのマクロが用意されています。 hrはHRESULTコードとします。
    マクロ内容
    SUCCEEDED(hr)成功した場合に真、失敗した場合に偽
    FAILED(hr)失敗した場合に真、成功した場合に偽
    成功時のコードが複数ありますので、 成功かどうかを知りたいときにhrをS_OKと比較してはいけないとされています。 成功かどうかの判別には上記いずれかのマクロを用いてください。

    関連ページ

  • COM/ActiveXの解説ページ 目次
  • 次の項目: クラスの作成とその利用