COM Aggregation

Home. > Contents.

COMコクラスには、「Aggregation(集成)」という生成オプションを持つものがあります。 Aggregationとは、コクラスを再利用するための仕組みですが、どのような仕掛けを施せばいいのでしょう?

オブジェクトの拡張方法

一度COMから離れてみると、クラスベースのオブジェクト指向プログラミングで既存のクラスを再利用するとすると、真っ先に思いつくのが継承でしょう。 COMの場合、コクラスの実体はC++のクラスだったりするわけですから、これを継承することができればコクラスを拡張することも不可能ではないということです。

しかし、実体はともかくとして、COMは言語中立のABIです。一方、継承というのは、言語ごとに仕様が異なるため、そのメモリレイアウトは言語に依存します。 したがって、継承では言語中立のABIを実現できないので、COMはコクラスの継承を仕様に含めていません。

一方、包含を用いることはできます。というのも、コクラスが別オブジェクトへのポインタを持つことは不可能ではないし、インターフェイスメソッドの定義をそのオブジェクトに委譲することもできます。 これであれば、バイナリを跨いで実装されたCOMオブジェクトを再利用することができます。しかし、このままでは包含先のQueryInterfaceを呼び出すことができません。 なぜなら、COMオブジェクトの実装者は、一つのオブジェクトは、QueryInterfaceでIUnknownを要求したときに常に同一のアドレスを返す必要があるからです

仮に、何も仕掛けを施さずに包含先のQueryInterfaceを呼び出してしまうと、包含先のQueryInterfaceが返したアドレスからQueryInterfaceを呼び出したときに包含先のオブジェクトのIUnknownが返ってしまいます。 これでは、「同一オブジェクトからIUnknownを要求して呼び出されたQueryInterfaceは同一のアドレスを返さなければならない」というルールを守れません。 ということは、包含先のオブジェクトのIUnknown.QueryInterfaceが包含元のIUnknownのアドレスを返すことができるなら、包含しながらも、包含先のオブジェクトのQueryInterfaceに対応することができます。

そこで、COMはAggregation(集成)というテクニックを用意しています。集成というのは、要するに包含をベースに、QueryInterfaceについて包含元と包含先で同一のオブジェクトとして振る舞うCOMオブジェクトを生成する手段です。

Aggregation(集成)の方法

Aggregationの肝は、「包含先のオブジェクトと包含元のオブジェクトのQueryInteraceがIUnknownを要求されたときに、同一のアドレスを返す」という要件を満たすために仕掛けを施すということにあります。 ただし、複数のオブジェクトが一つのCOMコンポーネントを構成するという特性上、参照カウンタの処理も考えなければなりません。つまり、IUnknownをいかに実装するかということがここでの主題です。

インナーオブジェクトとアウターオブジェクト

Aggregationにおいては、包含先のオブジェクトを「インナーオブジェクト」、包含元のオブジェクトを「アウターオブジェクト」と呼びます。通常の包含と異なるのは、外部からのQueryInterfaceの呼び出しに対して包含元のオブジェクトのアドレスが返ってくることがあるという点でしょう。 その点では、純粋な包含というよりは、むしろ複数のオブジェクトが群をなして一つのCOMオブジェクトであるかのようにふるまっていると言えますね。この場合の「インナー」「アウター」というのは、IUnknownの実装の内外関係を表しているという方が正確な表現でしょうか。

外部から見た場合、IUnknownはアウターオブジェクトの実装にアクセスすることになります。つまり、あるCOMオブジェクトがAggregationをサポートし、インナーオブジェクトになるためには、外部からのQueryInterfaceに対して、アウターオブジェクトのQueryInterfaceを呼び出さなければなりません。 しかし、インナーオブジェクト自身も自身のIUnknown実装を用意しなければなりません。さもなければアウターオブジェクトがインナーオブジェクトをCOMオブジェクトとして扱えなくなってしまいます。

インナーオブジェクトの実装

では、インナーオブジェクトを実装するためにはどのようにすればいいのでしょうか。Microsoftのリファレンスによると、インナーオブジェクトは、次の要件を満たさなければなりません。

まず、1つ目と2つ目の要件の意味ですが、インナーオブジェクトは、「外部から見たときにはアウターオブジェクトと一体として1つのオブジェクトとして振る舞わなければならない」という要求と、「アウターオブジェクトに対しては、1つのCOMオブジェクトとして振る舞わなければならない」 という要求から導き出される要件です。そして、すべてのCOMオブジェクトはIUnknownを実装していなければなりませんが、この2つの要求を同時に満たそうとすると、外部から見た時のIUnknownと、アウターオブジェクトが直接操作するIUnknownが異なる振る舞いをしなければなりません。

しかし、単純にIUnknownを実装してしまうと、アウターオブジェクトからの呼び出しなのか、それとも外部からの呼び出しなのかということを、メソッド側では判断できなくなってしまいます。したがって、アウターオブジェクトからの操作なのか、外部からの操作なのかを区別する必要があります。 そこで、COMは「アウターオブジェクトはインナーオブジェクトを必ずIUnknownを通して扱う」と取り決めました。というのは、外部からのIUnknownの呼び出しは、アウターオブジェクトの責任なので、アウターオブジェクト以外がインナーオブジェクトにIUnknownを要求することはありえません。 すなわち、インナーオブジェクトがIUnknownを要求されるとき、その要求元は必ずアウターオブジェクトなので、インナーオブジェクト自身のIUnknownを返すことができるということになり、一方で、IUnknown以外のインターフェイスは外部から操作されるものということになるため、別のアドレスを返して区別する、ということになります。

つまり、インナーオブジェクトは、インナーオブジェクト自身のIUnknown.QueryInterfaceの実装において、IUnknownが要求されたときと、そうでないときには、別のIUnknownを実装したアドレスを返すということになります。

4つ目の要件は、インナーオブジェクトは当然にアウターオブジェクトのアドレスを保持しなければなりません(さもないと、アウターオブジェクトに実装の委譲ができません)が、COMはオブジェクトの生存期間の制御に参照カウンタを利用しますから、 仮に、インナーオブジェクトがアウターオブジェクトのアドレスを受け取ったときにアウターオブジェクトの参照カウントを上げてしまうと、循環参照が生じ、アウターオブジェクトもろともメモリ/リソースリークしまいます。 したがって、アウターオブジェクトは当然インナーオブジェクトの参照カウンタを上げますが、インナーオブジェクトはアウターオブジェクトの参照カウンタを上げてはならないということになります。

5つ目の要件は、COMオブジェクトが別オブジェクトのインナークラスとして生成される場合、IUnknown以外の要求には応えてはならないということを意味します。「COMオブジェクトが別オブジェクトのインナークラスとして生成される場合」というのは、CoCreateInstanceの第2引数、あるいはIClassFactory.CreateInstanceの第1引数がNULLではないときのことです。このとき、IUnknown以外の要求に応えてしまうと、アウターオブジェクトに対してインナーオブジェクト自身のIUnknown実装を返すことができません。つまり1つ目の要件を満たせなくなってしまいます。

このような要件を満たすようにコクラスを実装すればいいのですが、ではどうやって実装すればいいのでしょうか?
特に、異なる2つのIUnknown実装をいかに用意するか、という点がキーポイントになります。

Microsoft公式リファレンスに沿った実装

サンプルコード

Microsoftが公式リファレンスで提示した実装方法は、「C++のクラスオブジェクトのレベルで、IUnknownとそれ以外のインターフェイスを実装するオブジェクトを別々にする」という方法です。 どういうことかというと、「単純に複数のIUnknownを実装しただけでは、IUnknownの実装を複数用意できないので、ならばC++のレベルでは複数のオブジェクトに実装させてしまえ」という考えに基づいた解決策です。

その結果、次のような実装となります(完全な実装は上のリンクからご覧ください)。
まずは、オブジェクト生成以外の部分を見ておきます。

class CInnerSample : public IUnknown{
    // 委譲オブジェクト
    class CInnerSampleImpl : public IComSample{
    public:
        IUnknown *pUnkOuter;
        // アウターオブジェクトへ丸投げするIUnknown実装(2つ目の要件に対応する)。
        STDMETHODIMP QueryInterface(REFIID riid,void** ppvObject){
            return pUnkOuter->QueryInterface(riid,ppvObject);
        }
        STDMETHODIMP_(ULONG) AddRef(){
            return pUnkOuter->AddRef();
        }
        STDMETHODIMP_(ULONG) Release(){
            return pUnkOuter->Release();
        }
        
        // IComSampleの実装
        STDMETHODIMP ShowMessage(LPCWSTR pszMessage){
            MessageBoxW(nullptr,pszMessage,L"Inner Sample!",MB_OK);
            return S_OK;
        }
    };
    
    CInnerSample();
    ~CInnerSample(){}
    
public:
    static HRESULT Create(LPUNKNOWN pUnkOuter,REFIID riid,void** ppvObject);
    
    // インナーオブジェクト自身のIUnknown実装(1つ目の要件に対応する)。
    STDMETHODIMP QueryInterface(REFIID riid,void** ppvObject){
        if(!ppvObject) return E_INVALIDARG;
        *ppvObject = nullptr;
        
        if(riid == IID_IUnknown){
            // IUnknownが要求されたときは、CInnerSampleのオブジェクト自身(すなわちインナーオブジェクト自身のIUnknown実装)を返す。
            *ppvObject = this;
        }else if(riid == IID_IComSample){
            // IUnknown以外が要求されたときは、IUnknownの実装をアウターオブジェクトに委譲するオブジェクトを返す(2つ目の要件に対応)。
            *ppvObject = &impl;
        }else{
            return E_NOINTERFACE;
        }
        ((IUnknown*)*ppvObject)->AddRef();
        return S_OK;
    }
    STDMETHODIMP_(ULONG) AddRef(){
        return ++refCount;
    }
    STDMETHODIMP_(ULONG) Release(){
        if(--refCount == 0){
            delete this;
            return 0;
        }
        return refCount;
    }
private:
    CInnerSampleImpl impl;
    ULONG refCount;
};

役割としては、CInnerSampleが非委譲側のIUnknown実装を提供し、CInnerSampleImplが委譲側のIUnknown実装を提供するという形になります。 そして、非委譲側のIUnknown実装は、インナーオブジェクトの参照カウントを管理し、QueryInterfaceはIUnknownかそうでないかで返すオブジェクトが異なるようにしておきます。
次に、オブジェクト生成のコードを見てみます。

/*
オブジェクト生成のコード
*/
CInnerSample::CInnerSample(LPUNKNOWN pUnkOuter):refCount(1){
    // Aggregationをしない場合、thisをアウターオブジェクトのポインタとして設定しておく。
    if(!pUnkOuter)pUnkOuter = this;
    
    // 委譲オブジェクトのアウターオブジェクトの設定。
    impl.pUnkOuter = pUnkOuter;
}

HRESULT CInnerSample::Create(LPUNKNOWN pUnkOuter,REFIID riid,void** ppvObject){
    if(!ppvObject) return E_INVALIDARG;
    *ppvObject = nullptr;
    
    // 引数検査:pUnkOuterがNULLでなく、かつ要求インターフェイスがIUnknownでない場合、失敗させる(5つ目の要件に対応する)。
    if((!pUnkOuter)&&(riid != IID_IUnknown))return E_NOINTERFACE;
    
    utils::com_ptr<CInnerSample> pObject;
    try{
        pObject = utils::make_comptr(new CInnerSample(pUnkOuter));
    }catch(std::bad_alloc&){
        return E_OUTOFMEMORY;
    }
    return pObject.QueryInterface(riid,ppvObject);
}

CInnerSample::Createは静的メソッドで、IClassFactory::CreateInstanceから呼ばれることを想定しています。 5番目の要件(AggregationモードのときはIID_IUnknownでなければならない)を満たしているかどうかをまず検査します。満たしていないければE_NOINTERFACEを返して失敗させます。 そのあとは基本的に通常のCOMオブジェクトの生成と同じです。リファレンスとは違い、例外やスマートポインタを使用していますが本質は同じことです。ただし、pUnkOuterはCInnerSampleのコンストラクタに渡します。

問題は、CInnerSampleのコンストラクタです。CInnerSampleはAggregationをサポートしますが、必ずしもAggregationモードでなければならないというわけではなく、単独のCOMオブジェクトとして生成される場合もあります。 ということは、何も考えずにpUnkOuterをimpl.pUnkOuterにセットしてしまうと、非Aggregationモードの場合にヌルポインタへのアクセスが発生してしまい、プログラムが落ちてしまいます。

したがって、非Aggregationモードでの生成も許容するならば、NULLでない場合でもimpl.pUnkOuterに有効なオブジェクトのアドレスをセットしなければならないわけですが、インナーオブジェクトそれ自身をセットすれば何とかなります。 というのも、impl.pUnkOuterがインナーオブジェクト自身の場合、委譲側の実装は非委譲側の実装にIUnknownメソッドの実装を丸投げするということになりますが、そうすると委譲側と非委譲側のIUnknownの実装が一致しますね。 すなわち、IUnknown.QueryInterfaceの要件たる、「同一オブジェクトのIUnknown要求は必ず同一アドレスを返さなければならない」というルールにぴったりとあてはまることになります。 というわけで、非Aggregationモードの場合、アウターオブジェクトとインナーオブジェクトを同一視することができるということになるわけです。

Aggregationモードの場合は、impl.pUnkOuterにコンストラクタ引数(=CoCreateInstanceの第2引数)をセットします。これによって、implはアウターオブジェクトにIUnknownの実装を委譲することができます。

NonDelegating-IUnknownと呼ばれるイディオムを利用した実装

サンプルコード

もう1つ解決策があります。それは、COMがABIレベルの仕様であることを利用して、「IUnknownとは別名の、全く同じレイアウトのインターフェイスを用意してしまえばいいじゃないか」という考えに基づいた解決策です。

// 非委譲のIUnknownインターフェイス。
interface INonDelegatingUnknown{
    virtual HRESULT STDMETHODCALLTYPE QueryInterfaceNonDelegate(REFIID riid,void** ppvObject) = 0;
    virtual ULONG STDMETHODCALLTYPE AddRefNonDelegate() = 0;
    virtual ULONG STDMETHODCALLTYPE ReleaseNonDelegate() = 0;
};

class CInnerSample : public IComSample, public INonDelegatingUnknown{
    CInnerSample();
    ~CInnerSample(){}
    
public:
    static HRESULT Create(LPUNKNOWN pUnkOuter,REFIID riid,void** ppvObject);
    
    // アウターオブジェクトへ丸投げするIUnknown実装(2つ目の要件に対応する)。
    STDMETHODIMP QueryInterface(REFIID riid,void** ppvObject){
        return pUnkOuter->QueryInterface(riid,ppvObject);
    }
    STDMETHODIMP_(ULONG) AddRef(){
        return pUnkOuter->AddRef();
    }
    STDMETHODIMP_(ULONG) Release(){
        return pUnkOuter->Release();
    }
    
    // IComSampleの実装
    STDMETHODIMP ShowMessage(LPCWSTR pszMessage){
        MessageBoxW(nullptr,pszMessage,L"Inner Sample!",MB_OK);
        return S_OK;
    }
    
    // インナーオブジェクト自身のIUnknown実装(1つ目の要件に対応する)。
    STDMETHODIMP QueryInterfaceNonDelegate(REFIID riid,void** ppvObject){
        if(!ppvObject) return E_INVALIDARG;
        *ppvObject = nullptr;
        
        if(riid == IID_IUnknown){
            // IUnknownが要求されたときは、非委譲側のIUnknown実装(すなわちインナーオブジェクト自身のIUnknown実装)を返す。
            *ppvObject = (INonDelegatingUnknown*)this; //キャストを忘れるととんでもない目に遭う。
        }else if(riid == IID_IComSample){
            // IUnknown以外が要求されたときは、委譲側のIUnknown実装を返す(2つ目の要件に対応)。
            *ppvObject = (IComSample*)this;
        }else{
            return E_NOINTERFACE;
        }
        return S_OK;
    }
    STDMETHODIMP_(ULONG) AddRefNonDelegate(){
        return ++refCount;
    }
    STDMETHODIMP_(ULONG) ReleaseNonDelegate(){
        if(--refCount == 0){
            delete this;
            return 0;
        }
        return refCount;
    }
private:
    IUnknown *pUnkOuter;
    ULONG refCount;
};

/*
オブジェクト生成のコード
*/
CInnerSample::CInnerSample(LPUNKNOWN pUnkOuter_):refCount(1){
    // Aggregationをしない場合、thisをアウターオブジェクトのポインタとして設定しておく。
    if(!pUnkOuter_)pUnkOuter_ = (IUnknown*)((INonDelegatingUnknown*)this); //キャストを忘れるととんでもない目に遭う。
    
    // 委譲オブジェクトのアウターオブジェクトの設定。
    pUnkOuter = pUnkOuter_;
}
// CInnerSample::Createは全く同じ。

この場合は、最初の実装でのCInnerSampleImplがなくて、これが実装していたインターフェイスを直接CInnerSampleが実装しています。 一番の注目点は、インナーオブジェクト自身のIUnknownの実装が、INonDelegateUnknownという自作のインターフェイスの実装に切り替わっているという点です。 このインターフェイスはIUnknownと全く同じレイアウトを持ちます。つまり名前以外の点では全く同じシグネチャを持っているということです。 言い換えれば、IUnknownのインターフェイス名とメソッド名を別のものに書き換えたものになっています。そうすれば、同一クラス内で2つの異なるIUnknownの実装を持つことができます。

それ以外はほとんど同じです。どちらを使うかのがいいかというのはよく分かりません。どちらでもうまくいくのでしっくりくる方を使えばいいのではないでしょうか。

アウターオブジェクトの実装

サンプルコード

次に、アウターオブジェクト側の実装について述べます。アウターオブジェクトは、IUnknownを通常通りに実装します。 ただし、IUnknown.QueryInterfaceの実装において、自身が実装していないインターフェイスの要求についてインナーオブジェクトのIUnknown.QueryInterfaceに丸投げすることができます。

MIDL_INTERFACE(/*IID*/) IComSample2 : IUnknown{
    virtual HRESULT STDMETHODCALLTYPE ShowMessage2(LPCWSTR pszMessage,BOOL *pIsYES);
};

class COuterSample : public IComSample2{
    IUnknown *pUnkInner;
    ULONG refCount;
    
    struct creation_error : std::runtime_error{
        creation_error(HRESULT hr_):hr(hr_){}
        HRESULT getHResult()const{
            return hr;
        }
    private:
        HRESULT hr;
    };
    
    COuterSample():refCount(1),pUnkInner(nullptr){
        HRESULT hr;
        if(FAILED((hr = CoCreateInstance(CLSID_CInnerSample,this,CLSCTX_INPROC_SERVER,IID_PPV_ARGS(&pUnkInner))))){
            throw creation_error(hr);
        }
    }
    ~COuterSample(){
        if(pUnkInner) pUnkInner->Release();
    }
public:
    static HRESULT Create(REFIID riid,void** ppvObject){
        utils::com_ptr<COuterSample> pObject;
        try{
            pObject = utils::make_comptr(new COuterSample);
        }catch(std::bad_alloc&){
            return E_OUTOFMEMORY;
        }catch(creation_error& e){
            return e.getHResult();
        }
        return pObject.QueryInterface(riid,ppvObject);
    }
    STDMETHODIMP QueryInterface(REFIID riid,void** ppvObject){
        if(!ppvObject)return E_INVALIDARG;
        ppvObject = nullptr;
        
        // IUnknownの要求については必ずアウターオブジェクト自身が責任をもって返さなければならない。
        // 仮にインナークラスに投げるようなことをしたら、COMの規約に反する。
        if((riid == IID_IUnknown)||(riid == IID_IComSample2)){
            *ppvObject = this;
        }else if(riid == IID_IComSample){
            // IID_IComSampleはインナーオブジェクトが実装しているので、インナーオブジェクトのQueryInterfaceに委譲する。
            return pUnkInner->QueryInterface(riid,ppvObject);
        }else{
            return E_NOINTERFACE;
        }
        this->AddRef();
        return S_OK;
    }
    STDMETHODIMP_(ULONG) AddRef(){
        return InterlockedIncrement((LONG*)&refCount);
    }
    STDMETHODIMP_(ULONG) Release(){
        if(InterlockedDecrement((LONG*)&refCount) == 0){
            delete this;
            return 0;
        }
        return refCount;
    }
    
    // IComSample2実装
    STDMETHODIMP ShowMessage2(LPCWSTR pszMessage,BOOL *pIsYES){
        switch(MessageBox(nullptr,pszMessage,L"COuterSample",MB_YESNO)){
            case IDYES:
                *pIsYes = TRUE;
                break;
            case IDNO:
                *pIsYes = FALSE;
                break;
            default:
                return E_FAIL;
        }
        return S_OK;
    }
};

さて、この実装の場合、インナーオブジェクトが新たなインターフェイスを実装した場合に、アウターオブジェクトのQueryInterfaceがそれを返すことができません。 そこで、インナーオブジェクトが実装するインターフェイスのすべてに対応するため、次のような実装にするということが考えられます。

STDMETHODIMP QueryInterface(REFIID riid,void** ppvObject){
    if(!ppvObject)return E_INVALIDARG;
    ppvObject = nullptr;
    
    // IUnknownの要求については必ずアウターオブジェクト自身が責任をもって返さなければならない。
    // 仮にインナークラスに投げるようなことをしたら、COMの規約に反する。
    if((riid == IID_IUnknown)||(riid == IID_IComSample2)){
        *ppvObject = this;
    }else{
        // アウターオブジェクトが実装していないインターフェイスに関しては、インナーに投げてみる。
        return pUnkInner->QueryInterface(riid,ppvObject);
    }
    this->AddRef();
    return S_OK;
}

しかし、MSDNにはこのように書いてあります。

The outer object must not blindly delegate a query for any unrecognized interface to the inner object, unless that behavior is specifically the intention of the outer object.

つまり、意図的にそのようなコードを書いたのでない限り、無闇にインナーオブジェクトのQueryInterfaceに投げてはいけませんと書いてあるわけです。 しかし、IUnknown.QueryInterfaceは、次の要件を満たさなければなりません。

これを満たすようにアウターオブジェクトのQueryInterfaceを実装するとすれば、アウターオブジェクトが実装していないインターフェイスについてインナーオブジェクトにまとめて委譲する方が簡便ではないかと思われるのですが、どうなのでしょうか。 ちょっとこの辺りに関しては自信がありません。他に詳しい人に当たってください(追記あり)。

追記(1): 参照カウンタについての注意事項

アウターオブジェクトがインナーオブジェクトの実装を利用するために、インナーオブジェクトのQueryInterfaceによってIUnknown以外のインターフェイスを取得した場合、アウターオブジェクト自身のReleaseを呼び出して、自身の参照カウンタを1つ下げなければなりません。 また、そのときに取得したインターフェイスポインタをReleaseするときは、アウターオブジェクト自身のAddRefを呼び出して、参照カウンタを1つ上げなければなりません。これをしなかった場合、メモリリークが発生します

その理由は、インナーオブジェクトのQueryInterfaceは、IUnknown以外の要求に対して委譲側のアドレスを返しますが、QueryInterfaceは参照カウンタを1つ上げるため、委譲側のAddRefが呼び出されます。 委譲側のAddRefが呼び出されるということは、アウターオブジェクトの参照カウンタが1つ上がるということを意味します。取得したメソッド内で参照カウンタを下げるならこれでも問題ありません。

しかし、仮にアウターオブジェクトがこのアドレスをメンバとして保持すると、自身と参照カウンタを共有するオブジェクトへの参照を保持するということになるので、自己参照が生じてしまいます。参照カウンタ方式では自己参照は禁忌ですから、これを避けるためには、意識的に参照カウンタを下げなければならないのです。

追記(2): QueryInterfaceの満たすべき要件とAggregation。

QueryInterfaceの実装を委譲した場合に、対称性、推移性を満たせないのではないかと書いてしまいましたが、インナークラスが外部からQueryInterfaceの呼び出しを受けると、これ自体の実装がアウタークラスに委譲しています。 したがって、アウタークラスが委譲したインターフェイス以外の要求に応えることはありません。というわけで、アウタークラスは、安心して特定のインターフェイスだけをインナーフェイスに丸投げすることができます。

利用例

Aggregationを利用している例としては、FreeThreadedMarshaler(FTM)の集成が挙げられます(FTMについては、「Agile Objectまたはフリースレッドマーシャラーの集成 - イグトランスの頭の中(のかけら)」が詳しいです)。 Aggregationの特徴からすると、単純なコクラスの拡張というよりも、横断的な機能拡張に採用するというのが用途として念頭に置かれているのではないかと思います。

参考資料


Home. > Contents.