Making COM nice to use

I write a lot of C++ code for Windows. There are a lot of Windows APIs which expose cool functionality and are implemented using COM; for me this currently means Direct2D, DirectWrite, Direct3D, DXGI, and Windows Imaging Component. In terms of presenting a compatible ABI, COM is a good thing (and is certainly nicer than straight flattening a C++ API to a C API). That said, COM looks extremely clunky in modern C++, for the following reasons:

  • Exceptional behaviour is indicated as a non-successful HRESULT return value rather than by throwing an exception. In almost all cases, the correct thing to do upon seeing a non-successful HRESULT is to throw an exception, and so the default behaviour should be to throw an exception (rather than the current default of disregarding the return value).
  • As a result of return values being used for HRESULTs, actual return values end up becoming out-parameters. This precludes the use of method chaining, the use of auto in C++11, and other nice things.
  • As an extension of the previous point, there are cases where returning a std::pair or a std::tuple would be preferable to the current situation of having multiple out-parameters.
  • As out-parameters are usually at the last of a method's parameters, the non-out-parameters cannot have default values.
  • Interface pointers need to have AddRef and Release called at all appropriate points in the program, which is an additional burden on the programmer. With C++11's move semantics, there is almost no reason for manual reference counting.
  • There are often cases where ranges are passed as two parameters: a pointer (void or typed) along with a count (of either bytes or elements). In a lot of usage, a complete container gets passed, which should be accepted as-is. For example, given std::wstring str, it should be allowable to say obj->DrawText(str) rather than obj->DrawText(str.c_str(), str.size()), and likewise obj->DrawText(L"Message") rather than obj->DrawTect(L"Message", sizeof(L"Message") / sizeof(wchar_t) - 1).
  • As another case of coupled parameter pairs, a range of unrelated types will be handled as a least-common-denominator pointer and a REFIID. I should be able to write rt->CreateSharedBitmap(surface) rather than having to write rt->CreateSharedBitmap(__uuidof(surface), surface).
  • Non-optional POD structures get passed by const* rather than const&. My personal opinion is that in modern C++, if a pointer shouldn't ever be null (i.e. is non-optional) and doesn't change what it points to (which for all intents and purposes, is true for method parameters), then it should always be a reference rather than a pointer.
  • As an elaboration on the previous point, a method which optionally accepts a POD structure should have two overloads (one without the parameter, and one with a const&) rather than accepting a const* and having a default value of null.
  • Namespaces aren't used. For example, all the Direct2D interfaces are called ID2D1Foo rather than D2::Foo.
  • The default method naming convention is CamelCase, which looks slightly out of place alongside my default of camelCase.

Some of those reasons are fairly minor, others are a major nuisance, but in aggregate, their overall effect makes COM programming rather unpleasant. Some people take small measures to attack one of the reasons individually, such as CHECK_HRESULT macro for throwing an exception upon an unsuccessful HRESULT (but you still need to wrap each call in this macro), or a com_ptr<T> templated smart pointer which at least does AddRef and Release automatically (but you then need to unwrap the smart pointer when passing it as a parameter to a COM method). I think that a much better approach is to attack all of the problems simultaneously. It isn't as easy as writing a single macro or a single smart pointer template, but the outcome is nice-to-use COM rather than COM-with-one-less-problem.

As with switching on Lua strings, my answer is code generation. I have a tool which:

  1. Takes as input a set of COM headers (for example d3d10.h, d3d10_1.h, d2d1.h, dwrite.h, dxgi.h, and wincodec.h).
  2. Identifies every interface which is defined across this set of headers.
  3. For each interface, it writes out a new class (in an appropriate namespace) which is like a smart pointer on steroids:
    1. If the interface inherits from a base interface, then the smart class inherits from the smart class corresponding to the base interface.
    2. Just like a smart pointer, AddRef and Release are handled automatically by copy construction, move construction, copy assignment, and move assignment.
    3. For each method of the interface, a new wrapper method (or set of overloaded methods) is written for the class:
      1. If the return type was HRESULT, then the wrapper checks for failure and throws an exception accordingly.
      2. Out-parameters become return values (using a std::tuple if there was more than one out-parameter, or an out-parameter on a method which already had a non-void and non-HRESULT return type).
      3. Coupled parameter pairs (such as pointer and length, or pointer and IID) become a single templated parameter.
      4. Pointers to POD structures get replaced with references to POD structures, with optional pointers becoming an overload which omits the parameter entirely.
      5. Pointers to COM objects get replaced with (references to) their corresponding smart class (in the case of in-parameters and also out-parameters).

As an example, consider the following code which uses raw Direct2D to create a linear gradient brush:

// rt has type ID2D1RenderTarget*
// brush has type ID2D1LinearGradientBrush*
D2D1_GRADIENT_STOP stops[] = {
  {0.f, colour_top},
  {1.f, colour_bottom}};
ID2D1GradientStopCollection* stops_collection = nullptr;
HRESULT hr = rt->CreateGradientStopCollection(
  stops,
  sizeof(stops) / sizeof(D2D1_GRADIENT_STOP),
  D2D1_GAMMA_2_2,
  D2D1_EXTEND_MODE_CLAMP,
  &stops_collection);
if(FAILED(hr))
  throw Exception(hr, "ID2D1RenderTarget::CreateGradientStopCollection");
hr = rt->CreateLinearGradientBrush(
  LinearGradientBrushProperties(Point2F(), Point2F())
  BrushProperties(),
  stops_collection,
  &brush);
stops_collection->Release();
stops_collection = nullptr;
if(FAILED(hr))
  throw Exception(hr, "ID2D1RenderTarget::CreateLinearGradientBrush");
With the nice-COM headers, the exact same behaviour is expressed in a much more concise manner:
// rt has type C6::D2::RenderTarget
// brush has type C6::D2::LinearGradientBrush
D2D1_GRADIENT_STOP stops[] = {
  {0.f, colour_top},
  {1.f, colour_bottom}};
brush = rt.createLinearGradientBrush(
  LinearGradientBrushProperties(Point2F(), Point2F()),
  BrushProperties(),
  rt.createGradientStopCollection(
    stops,
    D2D1_GAMMA_2_2,
    D2D1_EXTEND_MODE_CLAMP));

Comments

I like this post because it

I like this post because it is very very interesting.Thanks you very much for sharing this article.

Post new comment

The content of this field is kept private and will not be shown publicly.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Allowed HTML tags: <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd> <span>
  • Lines and paragraphs break automatically.
  • You can enable syntax highlighting of source code with the following tags: <code>, <blockcode>, <brainfuck>, <c>, <cpp>, <haskell>, <lua>, <php>.

More information about formatting options