Our resident Microsoft refugee describes some of the local lingo stuff like COM and MAPI to be specific.
Copyright © 1997 Robert H. Schmidt
This month I resume my abstraction series with an overview of the Microsoft world we're invading.
Disclaimer
To reach the El Dorado of increased abstraction, I've chosen a route through the murky swamp of Microsoft's COM and Extended MAPI. I'm taking this route not because I'm especially enamored of these technologies, but rather because they offer (wittingly or otherwise) a wonderful demonstration of why we want to get to El Dorado in the first place.
Before stepping into the mire, I want to issue a disclaimer: This is not a treatise on Microsoft programming. I will explain just enough Microsoftese to get us through, but not so much that you become expert on the local terrain. I will purposely gloss over much of the Microsoft-specific detail, and generalize the API to suit my needs.
If you find that I've cut corners on some aspect of COM or MAPI programming, picked a route that's longer than it needs to be, or made some sweeping generalization that's not 100% true, please do not write me. I'm using Microsoft's system as a means to a larger end, and don't want to get bogged down in that means. I'm sure there are heaps of bored Microsoft Systems Journal readers lurking on comp.os.ms-windows.programmer.ole just dying to debate this [1] .
You C programmers may read what follows and wonder, "Hey, what about your promise to include C abstractions? This stuff is all C++!" Trust me, this is all relevant, if somewhat obliquely. Microsoft's program interface standard is designed to be implemented in and called by both C and C++. However, the underlying design of that interface is object-oriented, and in fact has been since the earliest days of the Windows SDK (when all we had was C and assembler). Since C++ is a more natural expressive medium for OOP discussion, I've chosen to focus on C++ below but everything I show you is accessible by C, admittedly with more work [2] .
COM
COM == Component Object Model, Microsoft's standard for run-time objects and the foundation of their much-touted ActiveX. While you can write COM code with any compiler that supports the COM binary standard, C++ is typically the language of choice here. C++ compilers generate much of the code (especially v-tables and this pointers) that the COM binary standard requires. Also, C++ supports many of the object notions at compile time that COM supports at run time, letting you more easily model your intent in the code. In fact, I'm going to explain the essence of COM from the perspective of C++, since the latter is presumably a more familiar object paradigm. Purists be warned: I'm changing the real COM names, data types, and implementation options to streamline this overview.
Suppose you're writing a C++ project, and have designed all the public class interfaces. If you are truly a Diligent Reader, these interfaces have nothing but function members. For every class, separate the definition into two pieces, the public member function and everything else. The public members become the class interface, and the others become the class implementation.
As an example, if you have a circle class such as
class circle { public: ~circle(); circle(); circle(circle const &); circle &operator=(circle const &); double get_area() const; void set_radius(double); private: double radius_; };separate it into
class circle_interface { public: ~circle_interface(); circle_interface(); circle_interface( circle_interface const &); circle_interface &operator= (circle_interface const &); double get_area() const; void set_radius(double); }; class circle_implementation { private: double radius_; };Now make all the interface members pure virtual, moving the constructors, destructors, and assignment operator to the implementation class:
class circle_interface { public: virtual double get_area() const = 0; virtual void set_radius(double) = 0; }; class circle_implementation { public: ~circle_implementation(); circle_implementation(); circle_implementation( circle_implementation const &); circle_implementation &operator= (circle_implementation const &); private: double radius_; };Why remove these members from interface classes? Several reasons:
- Constructors can't be virtual.
- Interface objects have no data members (defined or inherited), so the language-synthesized members work correctly (by doing nothing).
- You will never create or destroy an interface object, so you will never need an interface constructor or destructor.
- Interface objects are designed to be written in and called by languages other than C++, languages that might not understand references or overloading.
A COMmon Base
Assume that every interface class has some magic compile-time number that uniquely identifies it. Next, have each interface class directly or indirectly derive from a common abstract base:
class common_interface { public: virtual void add_reference() = 0; virtual void release() = 0; virtual common_interface *query_interface (unsigned) = 0; }; class circle_interface : public common_interface { public: virtual double get_area() const = 0; virtual void set_radius(double) = 0; // // Derived from // 'common_interface'. // virtual void add_reference() = 0; virtual void release() = 0; virtual common_interface *query_interface (unsigned) = 0; };where
- add_reference increments the interface's internal reference count. As more clients reference the interface, this count increases.
- release decrements the interface's reference count; if the count goes to zero, the interface object self-destructs.
- query_interface accepts the unique ID number of an interface class, returning a pointer to an object of that class (if the called object's underlying implementation supports the requested interface) or NULL. The reference counts of returned interface objects are automatically incremented, under the assumption the caller will call release when it no longer uses the interface.
Since you can't directly create your first object of an interface class, add an implementation class member to create them for you:
class circle_implementation { public: ~circle_implementation(); circle_implementation(); circle_implementation( circle_implementation const &); circle_implementation &operator= (circle_implementation const &); // // New interface object // creation member. // virtual common_interface *create_interface (unsigned); private: double radius_; };Once you obtain an interface from create_interface, you can call that interface's query_interface to get other interfaces. Because all interfaces provide query_interface, from one interface you can bootstrap your way to all interfaces the implementation supports (by calling query_interface for each interface of interest).
Putting It All Together
Quite often, the easiest way for an implementation object to return a corresponding interface object is if the implementation object is an interface object:
class circle_implementation : public circle_interface { public: ~circle_implementation(); circle_implementation(); circle_implementation (circle_implementation const &); circle_implementation &operator= (circle_implementation const &); common_interface *create_interface(unsigned); private: double radius_; unsigned reference_count_; // // Derived from 'common_interface'. // virtual void add_reference(); virtual void release(); virtual common_interface *query_interface (unsigned); // // Derived from 'circle_interface'. // virtual double get_area() const; virtual void set_radius(double); };Such derivation allows the implementation object to simply return a pointer to itself in response to both query_interface and create_interface, and to manage one reference count for all supported interfaces. Further, because the base (interface) objects lack data members, implementations supporting multiple interfaces can use multiple inheritance without the hassles of virtual bases.
Listing 1 shows the full circle_implementation, while Listing 2 shows an example of how circle_interface and circle_implementation work together with client code.
Notice that when you copy one circle_implementation object to another (via copy constructor or copy assignment), you don't copy the reference_count_ data member. That member counts the number of interface object references, not implementation object references. Copying an implementation object doesn't change the number of references to the interfaces that object provides.
Also notice that you don't call interface members directly on the implementation object. Rather, you ask the implementation object to give you an interface object (via create_interface), and call the interface members on that interface object. Because they will never be called directly, I've declared those interface members private in the implementation object [3] .
Microsoft has several principle motivations for dissociating an interface from its implementing object, as I have done above even given the resulting level of indirection:
- You can write interface clients and interface implementations in different languages. In our example, you could write a circle_implementation object in C that returns a circle_interface object referenced by a C++ client. As long as the implementation object returns an interface pointer that the client can correctly call into, the calling code is ignorant of the actual implementing language.
- Implementation objects don't have to live in the same process, or processor space, or even on the same machine, as the client calling into them. The interface pointer could reference an object that exists on a PowerPC machine in Huber Heights, Ohio, but is called by an 8086 machine on the Mir space station [4] .
- You can add interfaces without recompiling existing interfaces and clients only the implementing objects need be aware of the additional interfaces. If a client doesn't know about an interface, it simply doesn't ask for it; or if it does ask for it, and the interface doesn't exist, it receives a null pointer. In either case, the existence of an interface changes only run-time behavior, not compile-time behavior.
As I mentioned, I've made some gross generalizations here, and changed the names to protect the guilty. As we work through this series, I'll point out relevant differences between my smoke-and-mirrors explanation and the actual Microsoft implementation.
Extended MAPI
Extended MAPI == Extended Messaging API, Microsoft's baroque support for mail clients and servers. This API is truly a swamp. I find it instructive that Microsoft has not enveloped MAPI in MFC. I wonder if that's because almost nobody is using MAPI (other than Microsoft itself), or because Microsoft has found impossible the task of adding order to the MAPI chaos.
I'm not interested in giving a full-blown discourse on the interrelationship among different MAPI components. At this stage, what you need to know about MAPI distills out to a few key generalities:
- Every mail message lives in some container, typically a mail folder.
- These folders live in some message store, on either a local computer or a mail server.
- Each mail message is composed of properties (e.g., subject line, submission date, number of attachments) which you can query or set.
- Most of the API is or manipulates COM interface objects, which is why I've labored so much over my COM explanation.
- Because the API is large and supports C, it is not highly abstracted as seen from a C++ perspective, making it a brilliant catalyst for this series.
To be fair to Microsoft, I suspect they crafted MAPI at a time when they considered C compatibility paramount. However, it's taken so long for MAPI and COM to propagate that, had they the chance to do it over, they might well scrap the C compatibility and make the interface truly objectified from the start.
Next Steps
In coming months, I expect to follow this route:
- Create abstract building blocks that will be useful in pretty much any large project.
- Identify what I deem interesting aspects of a MAPI mail client, where "interesting" means both "grokked by most anyone who's used e-mail" and "catalyzes the discovery of abstraction."
- Abstract and arguably improve these interesting aspects.
What you won't see is a single large working program. Again, if you are after 8,000-line Windows apps, check out MSJ.
Erratica
Dugald A. Taylor was the first of several diligent readers who note that that my March '97 chemistry example
H2O2 --> H2 + O2would create a fairly combustible mix in those brown peroxide bottles (think about the Hindenburg). Assuming there is no other agent (like to-be-bleached hair) present, the real net reaction is
2H2O2 --> 2H2O + O2And to think, I had a concentration in Chemistry during college.o
Notes
[1] . Before you get your knickers in a knot, know that the technical and acquisitions editor of MSJ is a close friend. He would be gravely disappointed were I to pass up such an easy shot at his mag.
[2] Microsoft's COM headers include macros that let you write the same source for both languages. These macros expand to little or nothing in C++, but expand to magic "stuff" in C to emulate language features C++ supports directly. (Although it's still a chore to emulate C++'s virtual function tables in C.)
[3] . Remember last month, when I had you ponder a derived class decreasing a base class member's access? Here's a variation on that same notion.
[4] . The Microsoft magic performing this so-called marshaling is way beyond this column's scope. I'm not even sure Microsoft has it all working correctly yet.
Bobby Schmidt is a freelance writer, teacher, consultant, and programmer. He is also a member of the ANSI/ISO C standards committee, an alumnus of Microsoft, and an original "associate" of (Dan) Saks & Associates. In other career incarnations, Bobby has been a pool hall operator, radio DJ, private investigator, and astronomer. You may summon him at 14518 104th Ave NE Bothell WA 98011; by phone at +1-206-488-7696, or via Internet e-mail as rschmidt@netcom.com.