Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is using implicit conversion for an upcast instead of QueryInterface() legal with multiple inheritance?

Assume I have a class implementing two or more COM interfaces (exactly as here):

class CMyClass : public IInterface1, public IInterface2 { 
};

QueryInterface() must return the same pointer for each request of the same interface (it needs an explicit upcast for proper pointer adjustment):

if( iid == __uuidof( IUnknown ) ) { 
    *ppv = static_cast<IInterface1*>( this );
    //call Addref(), return S_OK 
} else if( iid == __uuidof( IInterface1 ) ) {
    *ppv = static_cast<IInterface1*>( this );
    //call Addref(), return S_OK 
} else if( iid == __uuidof( IInterface2 ) ) {
    *ppv = static_cast<IInterface2*>( this );
    //call Addref(), return S_OK 
} else {
    *ppv = 0;
    return E_NOINTERFACE;
}

now there're two IUnknowns in the object - one is the base of IInterface1 and the other is the base of IInterface2. And they are in different subobjects.

Let's pretend I called QueryInterface() for IInterface2 - the pointer returned will be different from the pointer returned when I call QueryInterface() for IUnknown. So far so good. Then I can pass the retrieved IInterface2* into any function accepting IUnknown* and thanks to C++ implicit conversion the pointer will be accepted, but it will be not the same pointer that QueryInterface() for IUnknown* would retrieve. In fact if that function calls QueryInterface() for IUnknown immediately upon being called it will retrieve a different pointer.

Is this legal in terms of COM? How do I handle situations when I have a pointer to a multiply-inherited object and I allow an implicit upcast?

like image 250
sharptooth Avatar asked Jul 06 '10 06:07

sharptooth


3 Answers

COM has no rules regarding interface identity, only of object identity. The first rule of QI says that a QI on IID_Unknown on two interface pointers must return the same pointer if they are implemented by the same object. Your QI implementation does this correctly.

Without a guarantee for interface identity, a COM method cannot assume that it gets the same IUnknown pointer passed that it will retrieve when it calls QI on that pointer. So if object identity needs to be proven then a separate QI is required.

like image 137
Hans Passant Avatar answered Oct 06 '22 18:10

Hans Passant


As Hans points out, your implementation of QueryInterface is correct.

It is the responsibility of the user of a COM object to use QueryInterface at all times. The reason is exactly to prevent the scenario you have pointed out: that casting an IInterface1* or IInterface2* pointer to the IUnknown* pointer will yield different physical values.

In C++, it is not possible for the implementer to prevent the user from doing wrong.

Whether it will cause failure in an application depends on whether the application cares about comparing COM objects for identity.

MSDN: The Rules of the Component Object Model

Object identity. It is required that any call to QueryInterface on any interface for a given object instance for the specific interface IUnknown must always return the same physical pointer value. This enables calling QueryInterface(IID_IUnknown, ...) on any two interfaces and comparing the results to determine whether they point to the same instance of an object (the same COM object identity).

As Oleg points out, the failure of object identity will have a rather limited effect, because the calling of COM interface members is essentially unaffected - each virtual table entry will point to the same function address if the function signature matches.

All COM smart pointer implementations use QueryInterface when casting to a different interface, or when the current interface is dubious. Their comparison operators automatically use QueryInterface(IID_IUnknown, ...) on each input pointer in order to get physical IUnknown* pointers which can be directly compared. The failure of object identity will start to affect your application if you opt to use raw pointers throughout your application.

One special case where the failure will not manifest is when the class does not have any diamond inheritance. However, implicit casting is always illegal in COM, regardless of whether it crashes an application or not.

like image 23
rwong Avatar answered Oct 06 '22 19:10

rwong


It seems there are a small misunderstanding. Interfaces IInterface1 and IInterface2 are pur abstract. There are no separate QueryInterface() for IInterface1 and IInterface2. There are only a declaration, that class CMyClass will implement all methods from IInterface1 and IInterface2 (the set of methods of CMyClass is the set of methods IInterface1 and IInterface1 together).

So you implement in the class CMyClass one QueryInterface(), one AddRef() and one Release() method. In the QueryInterface() you cast the pointer to instance of class CMyClass to static_cast<IUnknown*>.

UPDATED: Hi! I had to drive away immediately after writing my answer. Only now I could read all other answers and can add something to my answer.

OK. You say that if you cast IInterface1 to IUnknown and if you cast IInterface2 to IUnknown you receive two different pointers. You are right! But nevertheless it does not matter. If you compare the contains of both pointers (which addresses has QueryInterface(), AddRef() and Release() in both cases) you will see that both pointer has the same contain. So I am also right!

There are a lot of good examples how to implement COM in pure C. In this case you need define a static structs with pointers to the virtual functions like QueryInterface(), AddRef() and Release() and give the pointer of such struct back as a result of QueryInterface(). It shows one more time, that only the contain which you gives back is important for COM and not a pointer (it is not important which vtable you give back).

One more small remark. In some comments you write about the possibility to have many implementations of methods QueryInterface(), AddRef() and Release(). I disagree here. The reason is that interfaces are pure abstract classes and if you define a class which implement some interfaces you have no typical class inheritance. You have only one class with one implementation of all functions from all interfaces. If you do this in C++, then compiler create and fill static vtables with the corresponding pointers to the only implementation of the functions QueryInterface(), AddRef(), Release() and so on, but all vtables points to the same functions.

To reduce of the number of vtables Microsoft introduced __declspec(novtable) or ATL_NO_VTABLE, but it is not the part of your questions.

like image 2
Oleg Avatar answered Oct 06 '22 17:10

Oleg