There are two ways of declaring COM interfaces, the hard way
and the easy way.
The easy way is to use an IDL file and let the MIDL compiler
generate your COM interface for you.
If you let MIDL do the work, then you also get
__uuidof support at no extra charge, which is a very nice bonus.
The hard way is to do it all by hand. If you choose this route,
your interface will look something like this:
#undef INTERFACE
#define INTERFACE ISample2
DECLARE_INTERFACE_(ISample2, ISample)
{
BEGIN_INTERFACE
// *** IUnknown methods ***
STDMETHOD(QueryInterface)(THIS_ REFIID riid, void **ppv) PURE;
STDMETHOD_(ULONG,AddRef)(THIS) PURE;
STDMETHOD_(ULONG,Release)(THIS) PURE;
// ** ISample methods ***
STDMETHOD(Method1)(THIS) PURE;
STDMETHOD_(int, Method2)(THIS) PURE;
// *** ISample2 methods ***
STDMETHOD(Method3)(THIS_ int iParameter) PURE;
STDMETHOD_(int, Method4)(THIS_ int iParameter) PURE;
END_INTERFACE
};
What are the rules?
- You must set
the
INTERFACE
macro to the name of the interface being
declared.
Note that you need to #undef
any previous value before you
#define
the new one.
-
You must use the
DECLARE_INTERFACE
and DECLARE_INTERFACE_
macros
to generate the preliminary bookkeeping for an interface.
Use DECLARE_INTERFACE
for interfaces that have no base class
and DECLARE_INTERFACE_
for interfaces that
derive from some other interface. In our example, we
derive the ISample2
interface from ISample
.
Note: In practice, you will never find
the plain DECLARE_INTERFACE
macro because all interfaces
derive from IUnknown if nothing else.
-
You must list all the methods of the base interfaces in exactly
the same order that they are listed by that base interface;
the methods that you are adding in the new interface must go last.
-
You must use the
STDMETHOD
or STDMETHOD_
macros to declare the
methods. Use STDMETHOD
if the return value is
HRESULT
and
STDMETHOD_
if the return value is some other type.
-
If your method has no parameters, then the argument list must
be
(THIS)
.
Otherwise, you must insert THIS_
immediately after
the open-parenthesis of the parameter list.
-
After the parameter list and before the semicolon,
you must say
PURE
.
-
Inside the curly braces, you must say
BEGIN_INTERFACE
and
END_INTERFACE
.
There is a reason for each of these rules. They have to do with
being able to use the same header for both C and C++ declarations
and with interoperability with different compilers and platforms.
-
You must set the
INTERFACE
macro because its value is used
by the THIS
and THIS_
macros later.
-
You must use one of the
DECLARE_INTERFACE*
macros to ensure that
the correct prologue is emitted for both C and C++.
For C, a vtable structure is declared, whereas for C++
the compiler handles the vtable automatically; on the other hand,
since C++ has inheritance, the macros need to specify the base
class so that upcasting will work.
-
You must list the base class methods in exactly the same order
as in the original declarations so that the C vtable structure
for your derived class matches the structure for the base class
for the extent that they overlap. This is required to preserve
the COM rule that a derived interface can be used as a base
interface.
-
You must use the
STDMETHOD
and
STDMETHOD_
macros to ensure that
the correct calling conventions are declared for the function
prototypes.
For C, the macro creates a function pointer in the vtable;
for C++, the macro creates a virtual function.
-
The
THIS
and THIS_
macros
are used so that the C declaration
explicitly declares the "this" parameter which in C++ is implied.
Different versions are needed depending on the number of parameters
so that a spurious trailing comma
is not generated in the zero-parameter case.
-
The word
PURE
ensures that the C++ virtual function is pure,
because one of the defining characteristics of COM interfaces
is that all methods are pure virtual.
-
The
BEGIN_INTERFACE
and
END_INTERFACE
macros
emit compiler-specific goo which the compiler vendor provides
in order to ensure that the generated interface matches
the COM vtable layout rules.
Different compilers have historically required different goo,
though the need for goo is gradually disappearing over time.
And you wonder why I called it "the hard way".
Similar rules apply when you are implementing an interface.
Use the STDMETHODIMP
and
STDMETHODIMP_
macros to declare your
implementations so that they get the proper calling convention
attached to them.
We'll see examples of this
next time.
a even easier way is to use #import
…or you could use attributed C++, which will generate the TLB and the header file (suitable for C and C++).
Note that the ATL wizard in VC 6 has a bug, it generates header files with STDMETHOD when it should be STDMETHODIMP. This isn’t a problem in practice because the difference is that STDMETHOD includes "virtual", which is harmless to repeat in the impl class.
Attributed C++ is about the worst thing I’ve ever encountered in my life. I have never gotten an attrib C++ ATL project to work in VC 7.1. Well, the wizard-generated code compiles, but as soon as I try to add my own stuff to the class, things break; and since the logic is hidden in attributes, there’s no way to debug it. (Things like: the class’s RGS file isn’t processed at registration time.)
However, this assumes that one wants a header that compiles both in C++ and C. Of course that’s a sensible thing to want. However, none of that macroing will give a declaration valid for other languages such as Object Pascal. Meanwhile, going the IDL way will likely yield a type library which is easily imported by other systems.
Borland Delphi and C++ Builder have a somewhat perverse way of defining interfaces; namely, you first make a type library by the use of a visual (and a pretty brittle at that) editor, and then the IDE generates or updates you the headers and the implementation. Also, you have the option of exporting the IDL for the type library.
Also, while generating the headers and the implementation, BCB insists on turning all interface pointer parameters into smart pointers that, among other things, automatically AddRef and Release. An unsuspecting victim is then forced to mentally step through different modes of a method call ([in], [out] and [in, out]) to understand why the object is suddenly gone after handing it to a method.
Mike Dunn: I think that the problem with Attributed C++ isn’t that it doesn’t work, but rather that the documentation is nearly nonexistant.
> I think that the problem with Attributed C++
> isn’t that it doesn’t work,
Actually, I’ve heard a lot of information that some parts of the generated code has memory leaks in some dispatch/invoke type stuff, and various other problems which of course are pretty hard to fix when they are automatically generated.
These are mentioned frequently on the microsoft.public.vc.atl newsgroup, such as this post for example:
http://groups.google.com/groups?q=attributed+bug+%22Alexander+Nickolov%22&hl=en&lr=&ie=UTF-8&group=microsoft.public.vc.atl&safe=off&selm=%23RNWlozzDHA.556%40TK2MSFTNGP11.phx.gbl&rnum=3
It seems like attributes were sort of a short-lived and unsuccessful experiment by the Visual Studio team. They haven’t even stood by them enough to fix these problems that are mentioned on the newsgroup.
Of course, to "help you out", attributed is turned on by default when you create a new ATL project…
Centaur: I’ve always wished that MIDL had on option to not generate the C compatible code (i.e. generate C++ only) OK-this is a minor point, and what does it hurt to generate both …
Isn’t it humorous that the visual studio team picked the same ‘perverse’ backwards method for generating wsdl code?
I’ve also been disappointed that microsoft has never provided an idl->.NET tool (relying instead on the lossy #import style tlbimp)
Raymond, could you please make sure that a copy of today’s post is sent to the people in charge of the Platform SDK’s TextServ.h? That file is a perfect example of why people declaring interfaces in C or C++ should use the COM macros. The ITextServices and ITextHost interfaces in that unit are declared using "unmacroed" C++, and that makes them totally incompatible with anything else, especially languages that don’t understand Microsoft’s default thiscall calling convention.
I agree that textserv.h is broken but it’s too late now. If you changed it to use the COM macros then you’d be changing the calling convention and existing clients would start crashing.
But how many compilers support #import and attributed C++? Remember, the point of these macros is to admit a *compiler-independent* way of expressing COM interfaces. It’s great that there are shortcuts you can use that work only on certain compilers, but that’s not the issue here.
This comment is not about the macros…
Your posts are cool!
Raymond, do you plan compile these technical information (posts from COM, Win32, Win64) in a book? A lot of real Microsoft developers will be grateful!
Raymond> But how many compilers support #import and attributed C++?
…or __uuidof?
It’s one thing to make a compiler-specific feature available. It’s another to require it.
If your compiler doesn’t support __uuidof then including the header file gets you the interfaces but you don’t get any __uuidof magic – but at least the interfaces work and you can use them. Whereas if your compiler doesn’t support #import and the header file uses it, then you’re stuck.
Why are IDL and ODL almost but not quite compatible? What are the criteria for choosing one or the other? What is the method for switching from one to the other if the programmer’s actions in VC++ happened to generate the less appropriate choice?
Beats me, I’ve never heard of ODL.