The COM interface contract rules exist for a reason

Raymond

Some people believe that the COM rules on interfaces are needlessly strict. But the rules are there for a reason.

Suppose you ship some interface in version N of your product. It’s an internal interface, not documented to outsiders. Therefore, you are free to change it any time you want without having to worry about breaking compatibility with any third-party plug-ins.

But remember that if you change an interface, you need to generate a new Interface Identifier (IID). Because an interface identifier uniquely identifies the interface. (That’s sort of implied by its name, after all.)

And this rule applies even to internal interfaces.

Suppose you decide to violate this rule and use the same IID to represent a slightly different interface in version N+1 of your program. Since this is an internal interface, you have no qualms about doing this.

Until you have to write a patch that services both versions.

Now your patch is in trouble. It can call IUnknown::QueryInterface and ask for that IID, and it will get something back. But you don’t know whether this is the version N interface or the version N+1 interface. If you’re not even aware that this has happened, your patch will probably just assume it has the version N+1 interface, and strange things happen when it is run on version N.

Debugging this problem is not fun. Neither is fixing it. Your patch has to use some other cues to decide which interface it actually got back. If your program has been patched previously, you need to have the version numbers of every single patch so that you can determine which version of the interface you have.

Note that this dependency can be hidden behind other interfaces. Consider:

[
    uuid(“ABC”)
]
interface IColorInfo
{
    HRESULT GetBackgroundColor([out] COLORREF *pcr);
    …
};

[ uuid(“XYZ”) ] interface IGraphicImage { … HRESULT GetColorInfo([out] IColorInfo **ppci); };

Suppose you want to add a new method to the IColorInfo interface:

[
    uuid(“DEF“)
]
interface IColorInfo
{
    HRESULT GetBackgroundColor([out] COLORREF *pcr);
    …
    HRESULT AdjustColor(COLORREF clrOld,
                        COLORREF clrNew);
};

[ uuid(“XYZ”) ] interface IGraphicImage { … HRESULT GetColorInfo([out] IColorInfo **ppci); };

You changed the interface, but you also changed the IID, so everything is just fine, right?

No, it isn’t.

The IGraphicImage interface is dependent upon the IColorInfo interface. When you changed the IColorInfo interface, you implicitly changed the IGraphicImage::GetColorInfo method, since the returned interface is now the version N+1 IColorInfo interface.

Consider a patch written with the version N+1 header files.

void AdjustGraphicColorInfo(IGraphicImage* pgi,
                            COLORREF clrOld, COLORREF clrNew)
{
 IColorInfo *pci;
 if (SUCCEEDED(pgi->GetColorCount(&pci)) {
  pci->AdjustColor(clrOld, clrNew);
  pci->Release();
 }
}

If run against version N, the call to IGraphicImage::GetColorCount will return a version N IColorInfo, and that version doesn’t support the IColorInfo::AdjustColor method. But you’re going to call it anyway. Result: Walking off the end of the version N vtable and calling into space.

The quick solution is to change the IID for the IGraphicImage function to reflect the change on the IColorInfo interface on which it depends.

[
    uuid(“UVW“)
]
interface IGraphicImage
{
    …
    HRESULT GetColorInfo([out] IColorInfo **ppci);
};

A more robust fix would be to change the IGraphicImage::GetColorInfo method so that you pass the interface you want to receive.

[
    uuid(“RST“)
]
interface IGraphicImage
{
    …
    HRESULT GetColorInfo([in] REFIID riid,
                         [iid_is(riid), out] void** ppv);
};

This allows interfaces on which IGraphicImage depends to change without requiring a change to the IGraphicImage interface itself. Of course, the implementation needs to change to respond to the new value of IID_IColorInfo. But now the caller can feel safe in the knowledge that when it asks for an interface, it’s actually getting it and not something else that coincidentally has the same name.

0 comments

Comments are closed. Login to edit/delete your existing comments