(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!
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:
- Use smart pointer for ITaskDefinition (296 lines)
- Use smart pointer for IRegistrationInfo (295 lines)
- Use smart pointer for IPrincipal (294 lines)
- Use smart pointer for ITaskSettings (293 lines)
- Use smart pointer for IIdleSettings (292 lines)
- Use smart pointer for ITriggerCollection (291 lines)
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:
- Use smart pointer for IActionCollection (286 lines)
- Use smart pointer for IAction/IExecAction (281 lines)
- Use smart pointer for IRegisteredTask (278 lines)
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.
Pingback: Stay COM: C++ and encapsulation – WriteAsync .NET