Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why exactly do I need an explicit upcast when implementing QueryInterface() in an object with multiple interfaces()

Assume I have a class implementing two or more COM interfaces:

class CMyClass : public IInterface1, public IInterface2 {
};

Almost every document I saw suggests that when I implement QueryInterface() for IUnknown I explicitly upcast this pointer to one of the interfaces:

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

The question is why can't I just copy this?

if( iid == __uuidof( IUnknown ) ) {
     *ppv = this;
     //call Addref(), return S_OK
}

The documents usually say that if I do the latter I will violate the requirement that any call to QueryInterface() on the same object must return exactly the same value.

I don't quite get that. Do they mean that if I QI() for IInterface2 and call QueryInterface() through that pointer C++ will pass this slightly different from if I QI() for IInterface2 because C++ will each time make this point to a subobject?

like image 498
sharptooth Avatar asked Nov 16 '09 15:11

sharptooth


2 Answers

The problem is that *ppv is usually a void* - directly assigning this to it will simply take the existing this pointer and give *ppv the value of it (since all pointers can be cast to void*).

This is not a problem with single inheritance because with single inheritance the base pointer is always the same for all classes (because the vtable is just extended for the derived classes).

However - for multiple inheritance you actually end up with multiple base pointers, depending on which 'view' of the class you're talking about! The reason for this is that with multiple inheritance you can't just extend the vtable - you need multiple vtables depending on which branch you're talking about.

So you need to cast the this pointer to make sure that the compiler puts the correct base pointer (for the correct vtable) into *ppv.

Here's an example of single inheritance:

class A {
  virtual void fa0();
  virtual void fa1();
  int a0;
};

class B : public A {
  virtual void fb0();
  virtual void fb1();
  int b0;
};

vtable for A:

[0] fa0
[1] fa1

vtable for B:

[0] fa0
[1] fa1
[2] fb0
[3] fb1

Note that if you have the B vtable and you treat it like an A vtable it just works - the offsets for the members of A are exactly what you would expect.

Here's an example using multiple inheritance (using definitions of A and B from above) (note: just an example - implementations may vary):

class C {
  virtual void fc0();
  virtual void fc1();
  int c0;
};

class D : public B, public C {
  virtual void fd0();
  virtual void fd1();
  int d0;
};

vtable for C:

[0] fc0
[1] fc1

vtable for D:

@A:
[0] fa0
[1] fa1
[2] fb0
[3] fb1
[4] fd0
[5] fd1

@C:
[0] fc0
[1] fc1
[2] fd0
[3] fd1

And the actual memory layout for D:

[0] @A vtable
[1] a0
[2] b0
[3] @C vtable
[4] c0
[5] d0

Note that if you treat a D vtable as an A it will work (this is coincidence - you can't rely on it). However - if you treat a D vtable as a C when you call c0 (which the compiler expects in slot 0 of the vtable) you'll suddenly be calling a0!

When you call c0 on a D what the compiler does is it actually passes a fake this pointer which has a vtable which looks the way it should for a C.

So when you call a C function on D it needs to adjust the vtable to point to the middle of the D object (at the @C vtable) before calling the function.

like image 177
Aaron Avatar answered Nov 16 '22 08:11

Aaron


You're doing COM programming, so there are a few things to recall about your code before looking at why QueryInterface is implemented the way it is.

  1. Both IInterface1 and IInterface2 descend from IUnknown, and let's assume neither is a descendant of the other.
  2. When something calls QueryInterface(IID_IUnknown, (void**)&intf) on your object, intf will be declared as type IUnknown*.
  3. There are multiple "views" of your object — interface pointers — and QueryInterface could be called through any one of them.

Because point #3, the value of this in your QueryInterface definition can vary. Call the function via an IInterface1 pointer, and this will have a different value than it would if it were called via an IInterface2 pointer. In either case, this will hold a valid pointer of type IUnknown* because of point #1, so if you simply assign *ppv = this, the caller will be happy, from a C++ point of view. You'll have stored a value of type IUnknown* into a variable of that same type (see point #2), so everything's fine.

However, COM has stronger rules than ordinary C++. In particular, it requires that any request for the IUnknown interface of an object must return the same pointer, no matter which "view" of that object was used to invoke the query. Therefore, it's not sufficient for your object to always assign mere this into *ppv. Sometimes callers would get the IInterface1 version, and sometimes they'd get the IInterface2 version. A proper COM implementation needs to make sure it returns consistent results. It will commonly have an if-else ladder checking for all supported interfaces, but one of the conditions will check for two interfaces instead of just one, the second being IUnknown:

if (iid == IID_IUnknown || iid == IID_IInterface1) {
  *ppv = static_cast<IInterface1*>(this);
} else if (iid == IID_IInterface2) {
  *ppv = static_cast<IInterface2*>(this);
} else {
  *ppv = NULL;
  return E_NOINTERFACE;
}
AddRef();
return S_OK;

It doesn't matter which interface the IUnknown check is grouped with as long as the grouping doesn't change while the object still exists, but you'd really have to go out of your way to make that happen.

like image 8
Rob Kennedy Avatar answered Nov 16 '22 07:11

Rob Kennedy