Search code examples
c++comabstraction

Helper class has hidden COM dependencies; who calls CoInitialize?


I have a helper class which provides some diagnostic APIs to a host application. The hidden implementation relies on WMI, which is accessed via Windows COM interfaces.

Implementing a class which is "COM aware" requires some overhead, in the form of calling CoInitialize/Ex, using an appropriate Apartment model (single-threaded/multi-threaded). I'm not sure who is responsible for setting that up - my helper class, or the consumer.

So, my question: who is responsible for calling CoInitialize and CoUninitialize: my helper class, or the host application? Aside from the helper class, there could be zero additional dependencies on COM in the host application.

Option A: the helper class calls CoInitialize and CoUninitialize in the constructor and destructor

This option is convenient, and effectively 'hides' the COM dependency. However, the parent application might or might not have already initialized COM, and it might or might not match the helper class's assumed apartment model. If the models do not line up, the helper class will receive an error from CoInitialize.

Option B: the helper class spawns a separate thread, and calls CoInitialize with a single-threaded apartment on the background thread. All interface calls are dispatched to the background thread and back.

This could help ensure the helper class has a 'clean slate' to work with, and avoid duplicate COM initializations on any single thread. It also increases the complexity of my helper class implementation, and adds overhead in the form of thread-switching and hand-shaking.

Option C: Make a note in the documentation, and require the host application to handle all calls to CoInitialize and CoUninitialize, before using the helper class

This one makes the class slightly less 'convenient' to use, because users have additional initialization steps before they can consume the class. It also requires the consumers of the class to actually read the documentation, which seems dangerous.


Solution

  • After some excellent points in the comments section, and after further thinking, I've opted for a new Option D:

    • Create a RAII helper class for initializing COM: class ApartmentContext
    • Require the ApartmentContext as a constructor argument in the helper class. This forces the user to make a promise that COM has been initialized, by the very existence of an ApartmentContext instance.

    This has a number of benefits:

    • It gives the consumer the flexibility to specify their own threading model.
    • It provides an API which clearly communicates the dependency on COM, and forces the user to provide proof of the apartment.
    • Bonus: the apartment is now RAII-friendly.

    Here is the listing for my ApartmentContext class:

    // Specifies a single-threaded or multi-threaded COM apartment.
    enum class Apartment
    {
        MultiThreaded = 0,
        SingleThreaded = 2
    };
    
    // A helper class used for initializing and uninitializing a COM Apartment.
    // The constructor of the ApartmentContext will initialize COM, using the 
    // specified apartment model: single-threaded or multi-threaded.
    // The destructor will automatically uninitialize COM.
    class ApartmentContext final
    {
    public:
        // Initialize COM using the specified apartment mode.
        ApartmentContext(Apartment mode);
    
        // Uninitialize COM
        ~ApartmentContext();
    
        // Get the current apartment type.
        Apartment Current() const;
    
    private:
        ApartmentContext& operator=(const ApartmentContext&) = delete;
        ApartmentContext(const ApartmentContext&) = delete;
    
        Apartment current_; // Store the current apartment type
    };
    

    Users will be forced to create one of these, prior to instantiating the helper class:

    // Somewhere in main...
    // Initialize COM:
    ApartmentContext context(Apartment::SingleThreaded);
    

    The helper class will require the user to provide a context in its constructor:

    public class MyHelperClass
    {
    public:
        // The apartment context isn't actually used; it is required by the 
        // constructor merely as a way to ensure that COM is initialized 
        // beforehand.
        // If the class requires a certain apartment mode,
        // it could also check the mode using the "Current" API, 
        // and throw on mismatch
        MyHelperClass(const ApartmentContext&);
    };
    

    Usage:

    // Create the helper class, providing the context as a constructor argument
    MyHelperClass helper(context);