Skip to content
/ rtcsdk Public

Lightweight C++/20 wrapper for utilizing, declaring and implementing of Windows COM interfaces.

License

Notifications You must be signed in to change notification settings

wxinix/rtcsdk

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

17 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

rtcsdk - Real Thin COM SDK

This library is a header-only lightweight C++/20 wrapper for utilizing, declaring and implementing of Windows COM interfaces. It is adapted from Alexander Bessonov's moderncom project, which was inspired by Kenny Kerr's moderncpp work.

Getting Started

This is the code required to define MyObject class that implements two COM interfaces, IFirstInterface and ISecondInterface:

#include <rtcsdk/interfaces.h>

class MyObject : public rtcsdk::object<MyObject, IFirstInterface,  ISecondInterface> 
{
  // Implement methods of IFirstInterface
  ...
  // Implement methods of ISecondInterface
  ...

  public:
    // MyObject may optionally have non-empty constructor that is
    // allowed to throw exceptions
    MyObject(int a, int b);
};

That's all! AddRef, Release and even QueryInterface methods are automatically generated by the library. The objects of class MyObject may be created on heap, on stack, as singleton or deeply integrated with COM to be automatically constructed via factory objects returned by DllGetClassObject function.

Read further to find out the details!

Features

  • Supports declaration and implementation of COM interfaces with pure C++ code
  • Provides automatic generation of QueryInterface, AddRef and Release methods
  • Supports definition of native C++ classes that implement any number of COM interfaces either directly or through "implementation proxies"
  • Supports aggregation, objects on stack and singleton objects
  • Provides interoperability with ATL and serves as a natural upgrade path when upgrading legacy projects that use ATL
  • Allows class that implements interfaces to have non-default constructors
  • Provides the COM "smart pointer" class, which can also be used independently of the rest of the library
  • Provides compile-time conversion of string GUIDs to GUID, which can be used independently of the rest of the library
  • Provides various customization points to simplify debugging or extend functionality of the library
  • Has built-in leak detection mechanism (that can be opted-in per class) to automatically search for leaked COM object references

Requirements

The library requires C++20 and has been tested on Microsoft Visual C++ compiler Version 14.34 (Visual Studio 2022 17.4.0).

See also FAQ section below for more information.

Installation

The library is header-only and does not require installation. Once brought into the project, the library's rtcsdk folder should be made visible to the rest of the project.

Documentation

Use the links for fast navigation:

GUID Helpers

In order to support backward compatibility and to simplify migration, the library can fetch interface and class identifiers (GUIDs) attached to classes via Microsoft Visual C++ extension __declspec(uuid("...")).

However, it also provides functions that parse string GUID into GUID structure at compile-time. Those functions are defined in rtcsdk/guid.h header and may be used independently of the rest of the library. However, you don't need to include this header if you include any other library's header.

The following functions are defined in the header:

template<size_t N>
constexpr GUID rtcsdk::make_guid(const char(&str)[N]) { ... }

make_guid converts a passed string ID to GUID. Supports the following formats: {XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX} or XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX.

The header also defines a user-defined literal (UDL) _guid, allowing the following code:

constexpr const GUID id = "{AB9A7AF1-6792-4D0A-83BE-8252A8432B45}"_guid;

Fetching Identifiers

To be able to successfully work with interface types, the library must be able to fetch interface ID (as GUID structure) from a type at compile time. By default, it looks for specialization of the following function:

template<typename Interface>
constexpr GUID get_guid(Interface *) { ... }

A specialization is found using unqualified-id grammar production (ADL). If specialization is not found, the default implementation tries to get an ID using Visual C++ extension uuidof(type).

get_guid may also be declared as a public static constexpr member of a class:

class MyClass ...
{
  public:
  static constexpr GUID get_guid() { ... }
};

A specialization of get_guid function is automatically added with RTCSDK_DEFINE_INTERFACE, RTCSDK_DEFINE_CLASS and other macros.

The following function can be used to get GUID from the interface:

template<typename Interface>
constexpr GUID get_interface_guid() noexcept;

COM Interface Smart Pointer

#include <rtcsdk/com_ptr.h>

This header file provides the following template classes: rtcsdk::com_ptr and rtcsdk::ref. For convenience, they are also available in com namespace as com::ptr and com::ref correspondingly.

com::ptr

template<typename Interface>
class com_ptr<Interface>
{
  ...
};

com_ptr takes a single template argument, the interface class itself. The library must be able to fetch an interface ID (IID) from this type.

The following constructors are provided:

  1. Default or null constructors

    com_ptr();
    com_ptr(std::nullptr_t) noexcept;

    Default-construct or null-construct the smart pointer object. The object becomes empty.

  2. Raw interface pointer constructor

    com_ptr(Interface *) noexcept;

    Constructs smart pointer object from a raw interface pointer. Successful construction increments object's usage counter with a call to AddRef method.

  3. Attaching constructor

    com_ptr(rtcsdk::attach_t, Interface *);

    Constructs smart pointer object from a raw interface pointer. DOES NOT call AddRef method. Use a constant object rtcsdk::attach as a first parameter to constructor.

  4. Other raw pointer constructor

    template<typename OtherInterface>
    com_ptr(OtherInterface *punk) noexcept;

    Constructs smart pointer object from a raw interface pointer to other interface. This constructor checks if Interface and OtherInterface are related:

    • If OtherInterface is derived from Interface, a simple static_cast is performed and AddRef is called on Interface.
    • Otherwise, an Interface pointer is obtained via QueryInterface.
  5. Reference constructor

    com_ptr(ref<Interface> p) noexcept;

    See the com::ref class below.

  6. Copy constructor

    com_ptr(const com_ptr &) noexcept;
  7. Move constructor

    com_ptr(com_ptr &&) noexcept;
  8. Other smart pointer copy constructor

    template<typename OtherInterface>
    com_ptr(const com_ptr<OtherInterface> &punk) noexcept;

    Copy constructor from other smart pointer type. This constructor checks if Interface and OtherInterface are related:

    • If OtherInterface is derived from Interface, a simple static_cast is performed and AddRef is called on Interface.
    • Otherwise, an Interface pointer is obtained via QueryInterface.
  9. Other smart pointer move constructor

    template<typename OtherInterface>
    com_ptr(com_ptr<OtherInterface> &&punk) noexcept;

    Move constructor from other smart pointer type. This constructor checks if Interface and OtherInterface are related:

    • If OtherInterface is derived from Interface, no calls to AddRef and Release are made.
    • Otherwise, an Interface pointer is obtained via QueryInterface.

com_ptr also provides a symmetric set of assignment operators.

The following methods are provided:

Method Description
explicit operator bool() const noexcept Checks if the current smart pointer object not empty
release() noexcept Releases a current interface and empties smart pointer object
reset() noexcept Same asrelease
Interface *operator ->() const noexcept Dereferences the current smart pointer object
bool operator ==(const com_ptr &o) const noexcept Checks whether two smart pointer objects are equal
bool operator !=(const com_ptr &o) const noexcept Checks whether two smart pointer objects are not equal
bool operator <(const com_ptr &o) const noexcept Introduces ordering
void attach(Interface *p) noexcept Attaches a raw interface pointer. Asserts if smart pointer object is not empty
[[nodiscard]] Interface *detach() noexcept Detaches the currently stored raw interface pointer
Interface *get() const noexcept Retrieves the currently stored raw interface pointer
Interface **put() noexcept Provides a write access to the stored raw interface pointer. Asserts if the object is not empty
template<class OtherInterface> auto as() const noexcept Constructs another smart pointer object with a given interface type
template<class OtherInterface> HRESULT QueryInterface(OtherInterface **ppresult) const Calls QueryInterface to get raw result
HRESULT CoCreateInstance(const GUID &clsid, IUnknown *pUnkOuter = nullptr, DWORD dwClsContext = CLSCTX_ALL) noexcept Calls::CoCreateInstance with provided parameters and stores the result in the current smart pointer object
HRESULT create_instance(const GUID &clsid, IUnknown *pUnkOuter = nullptr, DWORD dwClsContext = CLSCTX_ALL) noexcept Same asCoCreateInstance
static com_ptr create(const GUID &clsid, IUnknown *pUnkOuter = nullptr, DWORD dwClsContext = CLSCTX_ALL) Static method that calls::CoCreateInstance and returns a smart pointer object if successful. Otherwise, throws an instance of corsl::hresult_error.

Operators == and != are also provided to any combination of Interface * and const com_ptr<Interface> & pairs.

com::ref

This class is supposed to be used as a replacement for raw interface pointer in cases when interface pointer is used without adding a reference. Consider the following example:

void serialize(IStream *pStream)
{
  // work with stream object and never store it
  // therefore, we don't have to call AddRef and Release
  ...
}

void foo()
{
  com::ptr<IStream> stream { construct_stream() };

  serialize(stream.get());  // have to call get() here
}

serialize function may be rewritten to take an instance of bcom::ref<IStream> instead of a raw pointer. A benefit is additional lifetime checks in debug builds without any overhead in release builds:

void serialize(com::ref<IStream> pStream)
{
  // note pStream is passed by value
  // unmodified body of serialize function above
}

void foo()
{
  com::ptr<IStream> stream { construct_stream() };

  serialize(stream);  // implicitly construct com::ref<IStream> here
}
template<typename Interface>
class ref { ... };

com::ref takes a single template argument, the interface class itself. The library must be able to fetch an interface ID (IID) from this type.

The following constructors are provided:

  1. Default and null constructors

    ref() = default;
    ref(std::nullptr_t) noexcept;

    Construct an empty object.

  2. Constructor from a raw pointer

    ref(Interface *p) noexcept;
  3. Constructor from com_ptr<Interface>

    ref(const com_ptr<Interface> &o) noexcept;
  4. Constructor from com_ptr<Interface> temporary

    ref(com_ptr<Interface> &&o) noexcept;

    This constructor is allowed, however it introduces additional lifetime checks in debug builds, unless the RTCSDK_NO_CHECKED_REFS macro is defined before including the com_ptr.h header.

  5. Constructor from another smart pointer type:

    template<typename OtherInterface>
    ref(const com_ptr<OtherInterface> &o) noexcept;

    This constructor is allowed only if OtherInterface derives from Interface. Otherwise, the code is ill-formed.

  6. Constructor from another smart pointer type temporary:

    template<typename OtherInterface>
    ref(com_ptr<OtherInterface> &&o) noexcept;

    This constructor is allowed only if OtherInterface derives from Interface. Otherwise, the code is ill-formed. Additional lifetime checks are performed in debug builds, unless the RTCSDK_NO_CHECKED_REFS macro is defined before including the com_ptr.h header.

  7. Copy-constructor

    template<typename OtherInterface>
    ref(const ref<OtherInterface> &o) noexcept;

    This constructor is allowed only if OtherInterface derives from Interface. Otherwise, the code is ill-formed.

Assignment operators are prohibited for ref objects.

The same comparison operators are defined for ref class as for com_ptr class.

The following methods are available:

Method Description
Interface *operator ->() const noexcept Dereferences the current smart pointer object
Interface *get() const noexcept Retrieves the currently stored raw interface pointer
template<class OtherInterface> auto as() const noexcept Constructs another smart pointer object with a given interface type

COM Interface Support

A rtcsdk/interfaces.h header provides infrastructure for working with COM interfaces in native C++ code.

Declaring Interfaces

The following macros may be used to declare interfaces: {#BELT_DEFINE_INTERFACE}

RTCSDK_DEFINE_INTERFACE(name, guid)
{
  // declare interface members here as normal C++ abstract methods, for example
  virtual int sum(int a, int b) = 0;
};

or

RTCSDK_DEFINE_INTERFACE_BASE(name, baseInterfaceName, guid)
{
  // declare interface members here as normal C++ abstract methods, for example
  virtual int sum(int a, int b) = 0;
};

First macro declares interface name derived from IUnknown and second macro declares interface name derived from baseInterfaceName.

The library is also capable of working with "legacy" interfaces. This basically means that any abstract class that is derived from another legacy interface or IUnknown and for which library can fetch interface id can be directly used by the library.

Implementing Interfaces

Imagine you want to implement a class MyObject that implements two interfaces, IFirstInterface and ISecondInterface. Here's the full class declaration:

#include <rtcsdk/interfaces.h>

class __declspec(novtable) MyObject : public rtcsdk::object<MyObject, IFirstInterface, ISecondInterface>
{
  // Implement methods of IFirstInterface
  ...
  // Implement methods of ISecondInterface
  ...
};

Constructing object on heap:

com_ptr<IFirstInterface> construct_object()
{
  return MyObject::create_instance().to_ptr();
}

com_ptr<ISecondInterface> construct_object_second()
{
  return MyObject::create_instance().to_ptr<ISecondInterface>();
}

object

object is a variadic template class declared as

template<typename Derived, typename... Interfaces>
class object;

Derived should be the name of the class that derives from object.

Interfaces is a non-empty list of interfaces the class implements. Every entry in the list must be one of the following:

  • COM interface class, for which library is able to fetch IID (see above). Must not be IUnknown.
  • also<SomeInterface>. See below for more information.
  • eats_all<Derived> class. See below for more information.
  • aggregates<Derived, OtherInterfaces...>. See below for more information.
  • A class that derives from intermediate.

Important note: Derived class declaration follows the so-called Curiously Recurring Template Pattern (CRTP) and causes the resulting class to effectively derive from all COM interfaces directly or indirectly listed.

object has the following members:

  • using DefaultInterface = unspecified;

    Returns the "default" (usually first) interface. Never equals IUnknown.

  • IUnknown *GetUnknown() noexcept;

    Can be used inside or outside derived class to obtain a direct pointer to IUnknown interface. Does not call AddRef.

  • virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObject) noexcept override;

    Implements IUnknown::QueryInterface.

  • template<typename... Args> 
    static object_holder<unspecified> create_instance(Args &&...args);

    Static method that should be used to create an instance of Derived class on heap. Arguments, if passed are perfect-forwarded to Derived's constructor. Returns a special wrapper object. See object_holder below.

    See also object customization points section below.

  • template<typename... Args>
    static com_ptr<IUnknown> create_aggregate(IUnknown *pOuterUnknown, Args &&...args);

    Create new instance of Derived aggregating pOuterUnknown. Derived must has supports_aggregation trait (see below).

    Arguments, if passed are perfect-forwarded to Derived's constructor.

    See also object customization points section below.

  • template<typename OtherInterface = FirstInterface>
    com_ptr<OtherInterface> create_copy() const;

    Creates a copy of the current object (invoking Derived's copy constructor) and queries a copy for a given interface. Derived must derive from OtherInterface or OtherInterface must be IUnknown.

  • auto addref() noexcept;

    This protected method may be called by Derived class if it needs to explicitly add an object reference.

  • auto release() noexcept;

    This protected method may be called by Derived class if it needs to explicitly release an object reference.

also

also class template should be used whenever Derived implements "legacy" interface that itself derives from another legacy interface. Legacy interfaces are those interfaces that are not declared with RTCSDK_DEFINE_INTERFACE macro.

Consider the following example. IDispatchEx interface is defined in Platform SDK and derives from IDispatch interface. It is a legacy interface. We declare MyClass the following way:

class MyClass : public rtcsdk::object<MyObject, IDispatchEx>
{
  // Implement IDispatch
  ...
  // Implement IDispatchEx
  ...
};

The generated QueryInterface automatically supports querying for IDispatchEx interface, but does not support querying for IDispatch interface. This example must be fixed in the following way:

class MyClass : public RTCSDK::object<MyObject, IDispatchEx, rtcsdk::also<IDispatch>>  // fix
{
  // Implement IDispatch
  ...
  // Implement IDispatchEx
  ...
};

If interface is declared using RTCSDK_DEFINE_INTERFACE macro, this fix is not required, even if declared interface derives from "legacy" interface:

RTCSDK_DEFINE_INTERFACE_BASE(IMyDispatch, IDispatch, "{AB9A7AF1-6792-4D0A-83BE-8252A8432B45}")
{
  ...
};

class MyClass : public RTCSDK::object<MyObject, IMyDispatch>
{
  // Implement IDispatch
  ...
  // Implement IMyDispatch
  ...
};

eats_all

One of the QueryInterface customization points is an eats_all<Derived> special entry in interface list. If the class contains this entry, it must implement the following public method:

void *on_eat_all(const IID &id) noexcept
{
  ...
}

If a class supports a given interface, it must obtain a pointer to this interface, call an AddRef method through the obtained pointer and return it, cast to void *. Otherwise, it must return nullptr.

aggregates

COM objects may support aggregation. That is, an object may advertise an interface that it does not directly implement. It then "forwards" the query for this particular interface to its class member variable, for example, or obtains the pointer using other ways.

The library supports this scenario with a aggregates<Derived, Interfaces...> entry in interface list. For each aggregate interface listed, the Derived class must implement the following public method:

void *on_query(rtcsdk::interface_wrapper<ISomeInterface>) noexcept
{
  ...
}

on_query must obtain a pointer to requested interface, call an AddRef method through the obtained pointer and return it, cast to void *:

class MyClass : public rtcsdk::object<MyClass, IDirectlySupportedInterface, rtcsdk::aggregates<MyClass, IAggregateInterface>>
{
  com::ptr<ISomeOtherInterface> member { initialize_member() };
  
  // Implement IDirectlySupportedInterface
  ...
  // Implement aggregation
  void *on_query(rtcsdk::interface_wrapper<IAggregateInterface>) noexcept
  {
    IAggregateInterface *result{};
    member->QueryInterface(&result);
    return result;
  }
};

intermediate

It is often convenient to create classes or template classes that provide (partial) implementation of a given interface or interfaces and then use them when implementing final classes.

The library provides a machinery for such "implementation proxy" classes with a help of intermediate class template:

template<typename ProxyClass, typename... Interfaces>
struct intermediate;

This class template looks similar to object and should be used the same way. However, intermediate does not implement members declared in object.

RTCSDK_DEFINE_INTERFACE(IMyInterface, "{AB9A7AF1-6792-4D0A-83BE-8252A8432B45}")
{
  virtual void Method1() =0;
  virtual void Method2() =0;
};

// Provide a partial implementation of IMyInterface
class MyInterfaceImpl :  public rtcsdk::intermediate<MyInterfaceImpl, IMyInterface>
{
  // Partially implement Method1
  virtual void Method1() override { ... }
};

// MyClass implements IMyInterface with a help of MyInterfaceImpl:
class MyClass : public rtcsdk::object<MyClass, MyInterfaceImpl>
{
  // We still have to implement IMyInterface
  virtual void Method2() override { ... }
};

object_holder

object_holder is a temporary object holder class template that is returned by object<Derived, ...>::create_instance method:

template<typename T>
class object_holder;

Where T is an unspecified class derived from Derived.

The class has the following members:

  • com_ptr<Derived::DefaultInterface> to_ptr() && noexcept;

    May only be invoked on temporary object_holder object and "converts" it to com_ptr<Derived::DefaultInterface>.

    The object is considered "moved-out" after this method returns.

  • template<typename Interface>
    com_ptr<Interface> to_ptr() && noexcept;

    May only be invoked on temporary object_holder object and "converts" it to com_ptr<Interface>. Derived must derive from Interface or Interface must be IUnknown.

    The object is considered "moved-out" after this method returns.

  • Derived *obj() const noexcept;

    This special-purpose method is supposed to be used when additional initialization is required on constructed object:

    class __declspec(novtable) MyObject : public rtcsdk::object<MyObject, IMyInterface>
    {
      public:
        void additional_initialization_method(...);
    };
    
    com_ptr<IMyInterface> construct_object()
    {
      auto my_object = MyObject::create_instance();
      my_object.obj()->additional_initialization_method();
      return std::move(my_object).to_ptr();
    }

Traits

A trait class is a special class that Derived must directly derive from to change various defaults. The following trait classes are available:

Some of these trait classes automatically "propagate" down on inheritance chain. That is, if an implementation proxy class or even an interface class specifies a trait, it will also be present in any derived final class.

singleton_factory

When object is constructed using library's default construction mechanism, use the single global instance.

Singleton object is created at the time it is first requested and lives until the program is finished. There is no way to destroy the object before the program ends.

Access to object creation is thread-safe.

single_cached_instance

A special case of a singleton. An object is created at the time it is first requested and cached for all subsequent create requests. If the created object's reference count reaches zero, it is destroyed.

Access to object creation and destruction is thread-safe.

supports_aggregation

Marks the class as supporting aggregation. A class must be marked so if it is supposed to be used in aggregation (that is, constructed via create_aggregate or via default construction mechanism with non-null pOuterUnknown).

If the class is never to be used in aggregation, you can get more efficient implementation by not including this trait.

implements_module_count

Increment global module reference count whenever this class object's is referenced. Can be used in conjunction with DllCanUnloadNow implementation.

enable_leak_detection

Turn on leak detection for this class. See Automatic Leak Detection section for more information.

Object Customization Points

Customization points allow the class to execute additional code at various object lifetime events. They are all completely optional.

A customization point is a public method declared in the Derived class. The following customization points are supported:

final_construct

When constructor of the Derived class executes, reference-counting machinery is not yet initialized. Therefore, constructor cannot make any external calls that expect the current object to be a valid COM object.

For such cases, a class may provide the following public method:

template<typename... Args>
HRESULT final_construct(Args &&...args)
{
  ...
}

The final_construct method is invoked when reference-counting machinery is fully initialized. If arguments are present, the user is supposed to pass rtcsdk::delayed object as a first argument to create_instance or create_aggregate methods and Derived constructor must take no parameters.

final_construct is allowed to throw exceptions or return non-zero error codes. If final_construct returns an error code, an instance of bad_hresult holding this error code is thrown.

final_release

Correspondingly, there is a symmetric final_release method. It must have one of the following signatures:

static void final_release(std::unique_ptr<Derived> ptr) noexcept
{
  ...
}

template<typename D>
static void final_release(std::unique_ptr<D> ptr) noexcept
{
  ...
}

Note that final_release method is a static member, and it takes a unique pointer to the current object in the heap. By default, the object will be destroyed at the end of the final_release method, but the customization is free to do anything it needs with a passed pointer.

The second variant is used with aggregated objects. An implementation may use if constexpr (std::is_same_v<D, Derived>) to check if it is called with an object or aggregate value of the object. In the latter case, it can obtain a pointer to an object itself with by calling the pointed object's get() method:

class MyObject : public rtcsdk::object<MyObject, ...>, public rtcsdk::supports_aggregation
{
  void final_release()
  {
    ...
  }
public:
  // customization point
  template<typename D>
  static void final_release(std::unique_ptr<D> ptr) noexcept
  {
    if constexpr (std::is_same_v<D, MyObject>) {
      ptr->final_release();
    } else {
      ptr->get()->final_release();
    }
    // Object will auto-destruct here
  }
};

on_add_ref

This customization point is invoked each time an object's reference counter is incremented.

void on_add_ref(int new_counter_value);

on_release

This customization point is invoked each time an object's reference counter is decremented.

void on_release(int new_counter_value);

pre_query_interface

This customization point is invoked before standard QueryInterface machinery:

HRESULT pre_query_interface(REFIID iid, void **ppresult) noexcept;

If method is capable of producing result, it must store it at *ppresult and return S_OK. Otherwise, it must return E_NOINTERFACE. If this method returns any other value, QueryInterface processing immediately stops and the given error code is returned to the caller. In this case, the method must store nullptr at *ppresult.

If successful result is produced, implementation must call AddRef on obtained interface.

post_query_interface

This customization point is invoked after standard QueryInterface machinery was unable to find a requested interface:

HRESULT post_query_interface(REFIID iid, void **ppresult) noexcept;

If method is capable of producing result, it must store it at *ppresult and return S_OK. Otherwise, it must store nullptr at *ppresult and return E_NOINTERFACE.

If successful result is produced, implementation must call AddRef on obtained interface.

Constructing Objects

The library provides several ways to construct COM objects:

Simple Construction in the Heap

To construct an object of a given class Derived in the heap, call the static method Derived::create_instance, passing any number of arguments for class's constructor.

If the first argument is rtcsdk::delayed, then the default constructor is used instead and the rest of arguments are passed to the final_construct customization point.

If object construction succeeds, the method returns a proxy object. This proxy object is usually kept temporary, and you will immediately invoke its to_ptr() method, optionally passing an interface you want to query from the created object.

Advanced usages of a proxy object are described above in object_holder section.

Note that the class's constructor or final_construct customization point are allowed to throw exceptions.

Simple Construction on the Stack

For short-lived COM objects or for COM objects for which lifetime can be synchronized with a specific scope, you can use stack-based construction:

class MyClass : public rtcsdk::object<MyClass, IMyInterface> {...};

void bar(com::ref<IMyInterface> p)
{
  ...
}

void foo()
{
  rtcsdk::value_on_stack<MyClass> obj{/* arguments to constructor or final_construct */};
  bar(&obj);
}

AddRef and Release methods for objects constructed on stack are no-op, however, in debug builds the object's destructor will assert if there were unmatched number of calls to AddRef and Release.

Default Construction Mechanism

This generic mechanism for constructing COM objects can be used in cases when the calling code does not know specific implementation class, or for runtime object construction.

First of all, a class that wants to participate in default construction must register itself using one of the following macros:

RTCSDK_OBJ_ENTRY_AUTO(classname)
RTCSDK_OBJ_ENTRY_AUTO2(classguid, classname)

The RTCSDK_OBJ_ENTRY_AUTO macro registers classname class for which library can automatically fetch class ID.

The RTCSDK_OBJ_ENTRY_AUTO2 macro registers classname with a given CLSID:

RTCSDK_OBJ_ENTRY_AUTO2("{FFDBB4B7-8ECB-42FE-BF68-163B1E0829A2}"_guid, MyClass);

As described above, an ability to automatically fetch the class ID means that either get_guid was specialized for the class, or __declspec(uuid("...")) was added to class's declaration.

The library also provides a macro to attach an ID to a class: {#BELT_DEFINE_CLASS}

RTCSDK_DEFINE_CLASS(classGuidName, guid)

or inside a class

class MyClass ... 
{
public:
  RTCSDK_CLASS_GUID(guid)
};

After the class is registered, instances of this class may be created using one of the following functions:

  • template<typename Interface>
    HRESULT create_object(const GUID &clsid, const GUID &iid, void **ppv, IUnknown *pOuterUnknown = nullptr) noexcept;

    Create an instance of a class with a given CLSID and query an interface with a given IID. Pass a non-null pOuterUnknown if you want a created object to be aggregated.

    This function never throws. It returns a non-zero error code. If object creation throws an instance of corsl::hresult_error exception, exception's error code is returned. If object creation throws any other exception, E_FAIL is returned.

  • template<typename Interface>
    HRESULT create_object(const GUID &clsid, com::ptr<Interface> &result, IUnknown *pOuterUnknown = nullptr) noexcept;

    The same as above, but automatically takes IID from Interface and fills result on success.

  • template<typename Interface>
    com::ptr<Interface> create_object(const GUID &clsid, IUnknown *pOuterUnknown = nullptr);

    Directly returns a smart pointer to a given Interface or throws an instance of bad_hresult when object creation fails.

create_object respects the singleton and single cached instance traits when creating objects.

Implementing COM DLL Server

create_object function described above serves as a foundation for implementing DllGetClassObject.

All you need to do to implement DLL COM server is to add the following code to one of your CPP files:

#include <rtcsdk/factory.h>

HRESULT_export CALLBACK DllCanUnloadNow()
{
  return rtcsdk::DllCanUnloadNow();
}

HRESULT_export CALLBACK DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID * ppvObj)
{
  return rtcsdk::DllGetClassObject(rclsid, riid, ppvObj);
}

Automatic Leak Detection

The library provides a built-in mechanism to search for leaked object references. It not only shows you which objects were leaked, but also provides detailed stack traces for the corresponding leaked AddRef calls!

Leak detection is only enabled in debug builds.

The following pre-requisites must be done in order for the leak detection to work:

  1. Boost.StackTrace library is a dependency. If you cannot use boost, you must disable automatic leak detection by defining the following macro before including any library header:

    #define RTCSDK_COM_NO_LEAK_DETECTION
  2. rtcsdk::init_leak_detection() function must be called once before using com_ptr class or creating objects.

    Note that this function must be called no matter if you actually use automatic leak detection, unless you completely disable leak detection as described above.

    This function is empty in release builds.

  3. Classes that you want to participate in leak detection must opt in by including rtcsdk::enable_leak_detection trait.

    The recommendation is to only enable leak detection for those classes whose object references are found to be leaking.

Once all pre-requisites are met, you should run your program under debugger. Currently, this mechanism does not detect leaked objects, you should use other facilities to detect leaked objects. For example, you can use tools built into Visual Studio or use tracing to find leaked objects. Alternatively, you can combine automatic leak detection with objects constructed on stack, because in debug builds library automatically asserts when destructor for such object is called with mismatched number of calls to AddRef and Release.

Once leaked objects are found, add them to the Watch window in Visual Studio and expand until you find umb_usages member. It will contain a list of stack traces of calls to AddRef that were not matched with corresponding calls to Release.

Limitations

  1. Leak detection is built into com_ptr and object classes and therefore is unable to track calls to AddRef and Release made by other components. In other words, it always assumes that only com_ptr class makes calls to AddRef and Release.
  2. Leak detection does not currently find leaked objects. Once a leaked object is found by other means, it can be viewed in the debugger to see a list of stack traces.

FAQ

  1. Why Windows and MSVC only?

    COM is a Windows technology that is continued to be used even in modern OS components. COM can be considered a technology to provide binary interconnectivity between native components with a stable C++ ABI and therefore, may theoretically be used on other platforms. It can also be used as a way to establish Inversion of Control principles in code.

    There are a few Windows bindings in the code that may be relatively easy decoupled from the rest of the code. After that, the library may be used on other platforms to establish a solid connectivity between application components.

  2. What served as inspiration for this library?

    The library was initially created as a way to "renovate" old code base that used ATL for COM support. It was inspired by early works by Kenny Kerr on his moderncpp project (which later became C++/WinRT library). The library may share some common ideas (but not implementation) with C++/WinRT. It also does not have dependency on WinRT and does not require Windows 10.

About

Lightweight C++/20 wrapper for utilizing, declaring and implementing of Windows COM interfaces.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published