20 Sep 2003 mikehearn   » (Journeyer)

COM and the COMpetition

So pphaneuf was wondering what was up with COM. I'm not the guy who wrote the original rant, but I've worked quite a bit with the COM/OLE subsystem of Wine lately, so perhaps I can provide some insights.

COM attempts to solve a simple problem - providing a stable, language neutral in process ABI that allows components from many providers to work together, without the source code.

Well. That's the theory. In fact, COM has been used to do all kinds of things over the years. So pretty quickly we reach the first problem with COM:

1) When all you have is a hammer, everything looks like a nail.

COM and the various technologies built upon it (which are often referred to just as COM) such as OLE/ActiveX/DCOM and so on, have been used for the following things over the years:

  • To allow Excel to embed itself in Word and vice-versa
  • To try and make the Win32 API object oriented
  • As a replacement for designing network protocols
  • As a system for IDE plugins/OCX controls (which is really just a specialisation of OLE compound documents)
  • As a Java-applet killer
  • As a way to expose code to Visual Basic. In fact OLE Automation was originally developed for the VB engine (which is why it uses BSTRs, the B stands for BASIC)

That's just off the top of my head. In all these things, a technology like COM can be used, but that doesn't mean it's actually the best technology for that particular purpose. Unfortunately, because COM is so flexible, there has traditionally been a tendancy in Redmond to try and ram a square peg into a round hole.

COM is mind bogglingly complicated. That might surprise people who know the basics - and in a way it is surprising because the core of COM is very simple. Components are black boxes, that support interfaces. Interfaces can inherit from each other, and all interfaces inherit from IUnknown, which provides the basic services every COM object must implement.

So we reach the 2nd reason COM sucks:

2) COM is not actually object oriented.

You cannot inherit any functionality from IUnknown, only a contract, so you have to reimplement the IUnknown functionality each time. Because of that, IUnknown is very simple and provides almost no functionality.

Interfaces are basically vtables, ie a table of function pointers. IUnknown provides reference counting, and the ability to query for other interfaces. That's it. In comparison, for instance, the GObject base provides methods, properties, public variables, signals, arbitrary data association, reference counting and so on. So, COM objects tend to be quite primitive.

Actually I lied earlier. You can do inheritance in COM, by using a technique called aggregation. Unfortunately this is such a pain in the ass nobody does it.

Seems simple enough so far, right? To build a COM object, all you need to do is implement IUnknown and then whatever interfaces you need. Well, almost.

3) COM sucks because it forces often unnecessary optimizations upon you

All COM objects, if they are going to be instantiated via the standard CoCreateInstance API, must have an associated class factory. Because having COM instantiate an object for you is expensive, the designers decided that actually CoCreateInstance should return a class factory - ie an object which exists only to create the actual object you want. You can then create instances of the object you want very easily, just call the right method on the IClassFactory interface.

Of course, not all objects need creation efficiency. Many are one shots, you don't need to create them thousands of times per second, you create them perhaps once in the lifetime of the app. Unfortunately, COM forces the class factory paradigm on you. And of course, you have to implement it yourself.

Well, that isn't quite true. You can get toolkits, frameworks and IDEs which automate a lot of this boilerplate for you - unfortunately we just fell into Windows syndrome where problems with the underlying framework are disguised by having an IDE write code for you, rather than fixing the problem.

4) COM sucks because it starts simple, then rapidly gets complicated.

OK, so far it's all been conceptually easy, if rather a lot of typing. But at the moment, your object is not that useful. For it to be used from other languages/environments, you need to define the interface in IDL. MS IDL is not a simple language. It's the job of the midl compiler to turn IDL into header files that describe the object, so you can easily invoke its methods.

IDL can be compiled into several different things. Header files for C/C++ is one obvious target, but you can also make "type libraries", which are binary equivalents to the IDL. Sort of. Unfortunately, even though there are two different TLB file formats (with the same extension), neither of them can completely represent everything available in IDL.

For instance, a type library cannot represent the IDispatch interface, which is a core COM interface. You need support for that built into the framework.

Type libraries can also be compiled into a DLL as a resource. At that point, you basically need an IDE to extract it, and create the boilerplate code necessary to represent the vTables in your language.

So back to IDispatch. What's that?

5) COM sucks because late binding is truly horrid

IDispatch allows you to perform "late binding" on an object. Rather than needing to know the layout of the vtable at compile time, you can pass IDispatch a string representing the method you wish to invoke, and a set of arguments. It'll then invoke that method, and return the result. It's needed if you want your COM object to be accessible from a scripting language like JScript, VBScript, or ... yes, Visual Basic itself (which internally makes heavy use of OLE Automation).

Unfortunately, IDispatch is the interface from hell. MSDN recommends that you don't attempt to implement it yourself, because you'll get it wrong. It over optimizes (again) - for instance you don't pass the method name to IDispatch::Invoke, you pass a numeric ID, which you must retrieve beforehand from IDispatch::GetIDsOfNames. Why? For speed. Unfortunately, Visual Basic doesn't actually use this optimization - it was decreed that keeping a cache of method names to member IDs was too complex and easy to screw up, so it does the lookup each time. You still have to implement it of course.

In order to make this situation suck less, Microsoft provide a few implementations of IDispatch for you. One of them, the one it's probably best to use, uses type libraries! Ah, that's better. You just have to write your IDL, compile it to a type library, and then delegate IDispatch to ... oh, wait. LoadTypeLibrary, then GetTypeInfoOfGuid, then CreateStdDispatch, but then you have even more arbitrary limitations placed on you.

Don't worry. If you're using Delphi, this is all sorted out. Just don't forget that you can only have one IDispatch per object. If you have three interfaces you'd like to expose via OLE automation, you have to choose one, or get into "dispinterfaces".

Oh yes. Finally, remember that IDispatch::Invoke works in terms of the variant API. The Win32 VARIANT is really scary. Variants can contain not just about a zillion types of numbers, but also things like BSTR, the most annoying string type in the world. Variants can also contain other dispatch interfaces.

6) COM sucks because DCOM is really complicated

DCOM lets you remote interfaces into other contexts. What is a context? COM defines an "apartment model", which typically means thread. If a COM object is STA threaded (single threaded apartment) then it's not thread safe (ie, most COM objects), so if you want to access it from another thread you must marshal the interface into that thread. Remember to run a message loop though! If you don't, DCOM will start sending window messages to that hidden window you just implicitly created, and your app will deadlock.

COM supports many different threading models, not all of which are supported by all versions of Windows. What the difference between the apartment, free, or both threading models is, is buried inside books like "Essential COM".

You can also marshal interfaces into other processes or machines of course. Of course we now come back to the thorny issue of exactly how the interfaces are marshalled. One way is to use type libraries again, but remember they can't represent everything that IDL can, so sometimes you'll have to generate marshalling code manually. I say manually, of course the RPC NDR APIs are so baroque - NdrUserMarshalMarshall() anybody? - that this isn't possible, so you have to let midl do it for you. The code MIDL generates does some extremely wierd things - look but don't touch, and don't ask questions whatever you do. The code is also messy, using a bizarre mix of indent styles.

I'm not going to continue, this entry/rant is already far too long. Just take a look through the (extensive) COM APIs some time, and note how many functions there are that appear to exist for no better reason than invoking a certain function on an interface, or creating a certain object while by passing the standard mechanisms to do so.

Next time - why COM doesn't suck, and what we can do in free software land to get the benefits of COM (which are many) while avoiding the suckyness of a system that's been grown over a period of a decade.

Latest blog entries     Older blog entries

New Advogato Features

New HTML Parser: The long-awaited libxml2 based HTML parser code is live. It needs further work but already handles most markup better than the original parser.

Keep up with the latest Advogato features by reading the Advogato status blog.

If you're a C programmer with some spare time, take a look at the mod_virgule project page and help us with one of the tasks on the ToDo list!