Stay COM: WIL

Spread the love

(For this series, I’ll assume that you know the basics of COM. If you need a refresher, I highly recommend Kenny Kerr‘s excellent Pluralsight course, The Essentials of COM.)

Sooner or later, every Windows programmer has to deal with COM, the venerable Component Object Model. Love it or hate it, you gotta learn it and live it if you want programmatic access to key parts of Windows. For example, the Task Scheduler API is only directly exposed via COM interfaces; your alternatives are the schtasks.exe command line tool or scripting interfaces like the PowerShell ScheduledTasks module — all of which, of course, use the COM interfaces underneath.

But let’s say we’ll bite the bullet and write a C++ program to interact Task Scheduler. We might start with a sample application like the Time Trigger Example. Compile and run it, and hey, it works!
Task Scheduler screenshot showing a Time Trigger Test Task which starts notepad.exe on 1/1/2005.
The only problem is that it contains some highly questionable C++ coding practices, such as manual resource cleanup — not only that, but such code is also duplicated many times, on each exit point:

    //  Create the settings for the task
    ITaskSettings* pSettings{};
    hr = pTask->get_Settings(&pSettings);
    if (FAILED(hr))
    {
        printf("\nCannot get settings pointer: %x", hr);
        pRootFolder->Release();
        pTask->Release();
        CoUninitialize();
        return 1;
    }

    //  Set setting values for the task.
    hr = pSettings->put_StartWhenAvailable(VARIANT_TRUE);
    pSettings->Release();
    if (FAILED(hr))
    {
        printf("\nCannot put setting information: %x", hr);
        pRootFolder->Release();
        pTask->Release();
        CoUninitialize();
        return 1;
    }

    // Set the idle settings for the task.
    IIdleSettings* pIdleSettings{};
    hr = pSettings->get_IdleSettings(&pIdleSettings);
    if (FAILED(hr))
    {
        printf("\nCannot get idle setting information: %x", hr);
        pRootFolder->Release();
        pTask->Release();
        CoUninitialize();
        return 1;
    }

In fact, of the ~350 lines in this program, at least half of it is dedicated to checking error codes and cleaning up resources. That is already quite a lot of boilerplate, and we have touched little more than the basics of the Task Scheduler API. (Too much boilerplate is perhaps the top criticism of COM.)

But is all of this inevitable? Of course not! Today’s programmers have access to WIL, the Windows Implementation Libraries. WIL solves many pain points of general Win32 programming and offers particular niceties for COM usage patterns. Let’s see how we can transform this humble sample app into a modern C++ marvel.

First, we need a version of the sample app that compiles correctly with CMake, high warning levels, and slightly more modern C++ (using nullptr instead of NULL, brace initialization for structs, etc.): The starting point app (369 lines)

A good first step would be to remove the CoUninitialize everywhere by using an RAII pattern to “acquire” and “release” COM. WIL has us covered, and even uses this exact example in its unique_call RAII resource wrapper documentation:

#include <wil/resource.h>

using unique_couninitialize_call = wil::unique_call<decltype(&::CoUninitialize), ::CoUninitialize>;

int main()
{
    //  ------------------------------------------------------
    //  Initialize COM.
    HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
    if (FAILED(hr))
    {
        printf("\nCoInitializeEx failed: %x", hr);
        return 1;
    }

    unique_couninitialize_call cleanup;

// . . . then we remove all CoUninitialize calls below

The resulting app is now 351 lines: Integrate WIL, implement unique_couninitialize_call

Next, we’ll fix a minor annoyance with the environment variable reading. The original sample used an unsafe _wgetenv call, which I converted to the safer _wgetenv_s using a wchar_t vector-based buffer. That’s 10 lines of code right there. Can WIL help? Yes, they have many predefined adapters for these Win32 functions:

#include <wil/win32_helpers.h>
// . . .
    auto windir = wil::TryGetEnvironmentVariableW(L"WINDIR");
    std::wstring wstrExecutablePath(windir.get());

The app is now 342 lines: Use wil::TryGetEnvironmentVariableW instead of _wgetenv_s

Now let’s revisit these if (FAILED(hr)) checks. They dominate the whole sample app at this point. The modern C++ alternative would be to embrace exceptions instead of return codes. But the APIs we’re using will always return HRESULT instead of throwing. Furthermore, the app as of now is not exception safe — it must free all objects explicitly before returning an error. Let’s address this problem first by using smart pointers to wrap the COM objects. We’ll have to tread carefully, working on each pointer one-by-one, ensuring we use the “nothrow” error policy until we remove all explicit cleanup. Start with ITaskService, which uses CoCreateInstance to return the object:

#include <wil/com.h>
// . . .
    //  Create an instance of the Task Service.
    auto pService = wil::CoCreateInstanceNoThrow<ITaskService>(CLSID_TaskScheduler, CLSCTX_INPROC_SERVER);
    if (!pService)
    {
        printf("Failed to create an instance of ITaskService");
        return 1;
    }
// . . . then we remove all pService->Release() calls below

Note that we have lost one piece of functionality, the ability to determine the HRESULT of a failed call. We’ll revisit that before we’re done. This gets us a new app of 333 lines: Use smart pointer for ITaskService

The next COM pointer is ITaskFolder, which happens to be an output pointer. The semantics of COM output pointers are complex, but the WIL COM pointer type can abstract away all these details. It’s as simple as this:

    //  Get the pointer to the root task folder.  This folder will hold the
    //  new task that is registered.
    wil::com_ptr_nothrow<ITaskFolder> pRootFolder;
    hr = pService->GetFolder(_bstr_t(L"\\"), pRootFolder.put());
    if (FAILED(hr))
    {
        printf("Cannot get Root folder pointer: %x", hr);
        return 1;
    }
// . . . then we remove all pRootFolder->Release() calls below

Now our app has 314 lines: Use smart pointer for ITaskFolder

We can repeat a similar series of steps incrementally, keeping in mind that we need to use ptr.get() wherever we used to pass along raw interface pointers:

Once we get to ITrigger, we need QueryInterface functionality. WIL’s COM pointer handles this, too! We can use try_query, being careful to check for null at the end. Again, we don’t have an HRESULT so we can’t log the exact error (…yet):

    //  Add the time trigger to the task.
    wil::com_ptr_nothrow<ITrigger> pTrigger;
    hr = pTriggerCollection->Create(TASK_TRIGGER_TIME, pTrigger.put());
    if (FAILED(hr))
    {
        printf("\nCannot create trigger: %x", hr);
        return 1;
    }

    auto pTimeTrigger = pTrigger.try_query<ITimeTrigger>();
    if (!pTimeTrigger)
    {
        printf("\nQueryInterface call failed for ITimeTrigger");
        return 1;
    }

Now we are left with 287 lines: Use smart pointer for ITrigger/ITimeTrigger

We continue as before:

Now everything is safely wrapped in smart pointers! At this point, we could switch to exceptions and we would have no resource leaks. However, we still need to address that logging problem. How can we still indicate to an interactive user what is going wrong if we stop using HRESULT explicitly? As you might have guessed by now, WIL can handle this with its error logging support. To prepare for the next set of changes, we will set an error logging callback to print the result to stdout:

    // Print every log message to standard out.
    wil::SetResultLoggingCallback([](wil::FailureInfo const& failure) noexcept {
        constexpr std::size_t sizeOfLogMessageWithNul = 2048;
        wchar_t logMessage[sizeOfLogMessageWithNul];
        if (SUCCEEDED(wil::GetFailureLogString(logMessage, sizeOfLogMessageWithNul, failure)))
        {
            std::fputws(logMessage, stdout);
        }
    });

This brings us back to 288 lines, but we’ll make up for it soon: Add WIL error logging callback

Now we switch from nothrow to throwing smart pointers (no net line count change): Use exception throwing wil::com_ptr

After that, we can use the throwing versions for our interface queries. As a quick test, let’s try querying the wrong interface to see what happens:

    wil::com_ptr<ITrigger> pTrigger;
    hr = pTriggerCollection->Create(TASK_TRIGGER_TIME, pTrigger.put());
    // . . .
    // whoops, this is supposed to be ITimeTrigger, not IDailyTrigger...
    auto pTimeTrigger = pTrigger.query<IDailyTrigger>();

The console output helpfully gives us the error details:

...out\build\x64-debug\vcpkg_installed\x64-windows\include\wil\result_macros.h(6179)\tasksched-app.exe!00007FF7C7A3AB39: (caller: 00007FF7C7A37859) Exception(1) tid(12020) 80004002 No such interface supported
    [wil::err_exception_policy::HResult(hr)]

However, the app crashes hard after this, with that dreaded CRT error dialog:

---------------------------
Microsoft Visual C++ Runtime Library
---------------------------
Debug Error!

Program: ...\out\build\x64-debug\src\tasksched\tasksched-app.exe

abort() has been called

(Press Retry to debug the application)

---------------------------
Abort   Retry   Ignore   
---------------------------

To avoid this, we will split main into two functions. We rename the original main to run, and then create this function:

int main()
try
{
    return run();
}
CATCH_RETURN()

This error handling macro from WIL will ensure that no exceptions will escape our own program’s boundaries. The revised behavior for a failed interface query has the program exit cleanly with an error code:

...\out\build\x64-debug\vcpkg_installed\x64-windows\include\wil\result_macros.h(6179)\tasksched-app.exe!00007FF6AEA639B9: (caller: 00007FF6AEA57649) Exception(1) tid(13ec4) 80004002 No such interface supported
    [wil::err_exception_policy::HResult(hr)]
...\src\tasksched\exe\main.cpp(284)\tasksched-app.exe!00007FF6AEA6F56A: (caller: 00007FF6AEA69E99) ReturnHr(1) tid(13ec4) 80004002 No such interface supported
    Msg:[...\out\build\x64-debug\vcpkg_installed\x64-windows\include\wil\result_macros.h(6179)\tasksched-app.exe!00007FF6AEA639B9: (caller: 00007FF6AEA57649) Exception(1) tid(13ec4) 80004002 No such interface supported
    [wil::err_exception_policy::HResult(hr)]
] [main]

...\out\build\x64-debug\src\tasksched\tasksched-app.exe (process 71648) exited with code -2147467262.

We have changed the behavior slightly, since we now can return 0, 1, or some random HRESULT. But I think it’s still within the spirit of the app — as long as callers understand the well-known philosophy that any nonzero exit status (positive or negative) is a failure. We’re actually decreasing the line count slightly to 284 because we can eliminate some null pointer checking: Switch to throwing query, add exception handling

Now that we’re ready for exceptional behavior, we can switch to WIL’s CoInitializeEx wrapper, getting us down to 275 lines: Use wil::CoInitializeEx

We can revisit the environment variable code as well and use the throwing WIL wrapper there (269 lines): Use wil::GetEnvironmentVariableW

For our grand finale, we can replace all checks for FAILED(hr) with an appropriate WIL error handling macro. THROW_IF_FAILED_MSG will do nicely, as it allows a custom message to be provided with the standard WIL error details. The translation looks something like this:

    // BEFORE . . .
    hr = pService->Connect({}, {}, {}, {});
    if (FAILED(hr))
    {
        printf("ITaskService::Connect failed: %x", hr);
        return 1;
    }

    // AFTER . . .
    THROW_IF_FAILED_MSG(pService->Connect({}, {}, {}, {}), "ITaskService::Connect failed");

Essentially, we can make one-liners out of most COM calls! Since we fully rely on exceptions now and never propagate error codes, we can change run to be void and print the success message at the end of main. In fact, the error logging is far better than before. For example, if we purposely mess up the folder path:

...\src\tasksched\exe\main.cpp(63)\tasksched-app.exe!00007FF7D2604CDB: (caller: 00007FF7D2605FBB) Exception(1) tid(14fc4) 80070002 The system cannot find the file specified.
    Msg:[Cannot get Root folder pointer] [run(pService->GetFolder(_bstr_t(L"\\sdfdasfsdfds"), pRootFolder.put()))]

The error message is clear, has our custom message there (“Cannot get Root folder pointer”), and points to the exact source line which caused the problem. All told, we have a complete sample with equivalent functionality in just 156 lines of code, more than 50% reduction of the original: Use THROW_IF_FAILED_MSG, do not propagate HRESULT

Hopefully this gives you a sense of how much WIL can aid in COM usage. It’s becoming a de facto standard for Windows development, recommended by Microsoft over legacy approaches such as ATL and the MSVC utility classes. Try WIL on your next Win32 project (or adopt it in your current one), and you’ll wonder how you ever lived without it.

One thought on “Stay COM: WIL

  1. Pingback: Stay COM: C++ and encapsulation – WriteAsync .NET

Leave a Reply

Your email address will not be published. Required fields are marked *