Fitting COM into C++ System Error Handling
If you are programming on Windows, chances are high you are using COM. Be it for XML (MSXML), shell interaction, directshow, enumerating system devices or active directory objects, and a myriad of other functionality offered via COM interfaces.
One thing you have to deal with when using COM are return error codes of almost all interface methods, which they do in form of returning a HRESULT (typedef’d as long). An example of such would be:
virtual HRESULT IXMLDOMParseError::get_line(long* lineNumber);
Many times, those codes represent traditional Windows system error codes, and you can get to the windows error code via the HRESULT_CODE macro. That’s why in my opinion it makes sense to be able to turn them into system errors and simplify error handling.
Native C++ Compiler COM Support
I find it frightening when I see examples all over the internet, including MSDN, with C-style COM programming. All the object lifetime and error handling is done by hand, which clutters up the real task and understanding of what a COM library offers.
If you are like me, you really appreciate the Native C++ compiler COM support providing classes like _variant_t, _bstr_t, _com_ptr_t and _com_error. It’s a simple, thin and lightweight C++ wrapper around COM interfaces, which you don’t want to miss out. The C++ wrappers not only manage the lifetime of memory or objects but also simplify error handling, because you can wrap error codes in an error class and throw that around.
Unless you have to use the ‘raw’ COM interface methods, you get those wrappers for free when importing type libraries via VC’s #import directive.
Microsoft even updated _com_ptr_t with move semantics not so long ago, meaning it’s still mainstream.
#import’ing a type library usually creates wrapper methods around the raw functions, which perform the boiler plate of error checking
The wrapper checks for a FAILED operation and simply throws a _com_error, usually via a call to _com_issue_error_ex. E.g. the implementation of IXMLDOMParseError::get_line looks like this:
long IXMLDOMParseError::Getline ( ) { long _result = 0; HRESULT _hr = get_line(&_result); if (FAILED(_hr)) _com_issue_errorex(_hr, this, __uuidof(this)); return _result; }
Simplify
_com_error is just one of many within the whole ecosystem of C++ error classes; dealing with an ever-growing list of error classes can make try/catch blocks a pain. If you have already your own application-wide error class you probably convert library errors into your own.
I prefer using standard components because the standard usually makes programming simpler. It turns out that std::system_error, available since C++11, is really useful for dealing with system-like errors, because it was designed with extensibility in mind.
COM even offers the option to set your own translation handler via _set_com_error_handler, so we can directly throw system errors and translating is encapsulated at one single point.
System Error
Polymorphic, Extensible Error Closure
The real power of std::system_error stems from the fact that it only provides certain logic to deal with error codes, but is otherwise agnostic to the kind of system error. It’s nothing more than a std exception class additionally storing an error code object. This error code object in turn holds an error number and a pointer to an error category.
I won’t go into further detail as Christopher Kohlhoff describes at length the rationale behind std::system_error and how to extend it with your own types on his blog. The bottom line is that the error category allows us to easily extend the system error handling.
Implementation
Outline
So what do we need?
An enum type classifying a COM error
An error category class capable of translating HRESULT codes into readable messages, and connecting COM error types to system error’s polymorphism.
Translation functions from COM error to system error; this would be also the point to capture any additional description provided by the COM layer, which isn’t available anymore at a later point.
Automatic translation
Code - Declarations
Step 1 - Classification
// Enables classifying HRESULT error codes; // @note We don't bother listing all possible values; // this also allows for compatibility with // ‘Construction rules for enum class values (P0138R0)’ enum class com_error_enum {}; namespace std { // Make com_error_enum known to std library template<> struct is_error_code_enum<com_error_enum> : true_type {}; } // std library overload for constructing a std::error_code from a classified com error. // @note This function is found by ADL from within std::error_code(com_error_enum) std::error_code make_error_code(com_error_enum e) noexcept; // Access one and only com category const error_category& com_category() noexcept;
Step 2 - Error Category
// Categorize a com error. // Translates a HRESULT to a message string and std::error_condition class com_error_category: public std::error_category { public: using error_category::error_category; const char* name() const noexcept override { return "com"; } // @note If _UNICODE is defined the error description gets // converted to an ANSI string using the CP_ACP codepage. std::string message(int hr) const override; // Make error_condition for error code (generic if possible) // @return system's default error condition if error value can be mapped to a Windows error, error condition with com category otherwise std::error_condition default_error_condition(int hr) const noexcept override; };
Step 3 - Translation (manual)
// Factory function creating a std::system_error from a HRESULT error code // @param msg Description to prepend to the error code message; must not be nullptr. std::system_error com_to_system_error(HRESULT code, const char* msg = ""); // Factory function creating a std::system_error from a HRESULT error code // @param msg Description to prepend to the error code message. std::system_error com_to_system_error(HRESULT code, const std::string& msg); // Factory function creating a std::system_error from a HRESULT error code // and an optional error message // @param msg Optional description to prepend to the error code message; // must not be nullptr; // gets converted to an ANSI string using the CP_ACP codepage. std::system_error com_to_system_error(HRESULT hr, const wchar_t* msg); // Factory function creating a std::system_error from a HRESULT error code // and an optional error message // @param msg Optional description to prepend to the error code message; // gets converted to an ANSI string using the CP_ACP codepage. std::system_error com_to_system_error(HRESULT hr, const std::wstring& msg); // Factory function creating a std::system_error from a _com_error. // If an error description is available, removes trailing newline or dot (end of last sentence) and prepends it to the error code message. // @note The error description gets converted to an ANSI string using the CP_ACP codepage. std::system_error com_to_system_error(const _com_error& e);
Step 3a - Desktop Family Apps
// Factory function creating a std::system_error from a HRESULT error code and optionally from an error information. // If an error description is available, removes trailing newline or dot (end of last sentence) and prepends it to the error code message. // @note The error description gets converted to an ANSI string using the CP_ACP codepage. std::system_error com_to_system_error(HRESULT hr, IErrorInfo* help);
Step 3b - Non-Desktop Apps
There’s no IErrorInfo interface available, see Implementation
Step 4 - Translation (automatic)
Step 4a - Desktop Family Apps
// Translate COM error and throw std::system_error. // Suitable as alternative COM error handler, settable with _set_com_error_handler // @note The error description gets converted to an ANSI string using the CP_ACP codepage. [[noreturn]] void __stdcall throw_translated_com_error(HRESULT hr, IErrorInfo* help = nullptr);
Step 4b - Non-Desktop Apps
// Translate COM error and throw std::system_error. // Suitable as alternative COM error handler, settable with _set_com_error_handler // @note The error description gets converted to an ANSI string using the CP_ACP codepage. [[noreturn]] void __stdcall throw_translated_com_error(HRESULT hr, const wchar_t* msg);
Implementation
It’s almost this simple. Extracting the error code message (step 2) and error description (step 3) is a bit tricky, but not hard either.
Things to be aware of:
Take care about unicode-to-ANSI string conversion:
The presented approach utilizes _com_util::ConvertBSTRToString, which in turn utilizes WideCharToMultiByte with the system codepage CP_ACPMitigate reference-counting _bstr_t at the point when getting the description for the error message
Trim error description at end-of-sentence (that’s because the description is prepended to the error code message).
The IErrorInfo interface isn’t available in non-desktop environments. It gets replaced by a unicode C-String.
There’s a preprocessor macro _COMDEF_NOT_WINAPI_FAMILY_DESKTOP_APP that we can use to tell the difference.
I think I thought about every detail for a correct, concise and convenient usage. Let’s take a look at some code again. Note that using namespace std; is assumed for readability.
Code - Implementation
// wide-to-ANSI string conversion helper namespace detail { inline unique_ptr<char[]> to_narrow(BSTR msg) { return unique_ptr<char[]>{ _com_util::ConvertBSTRToString(msg) }; } inline std::unique_ptr<char[]> to_narrow(const wchar_t* msg) { static_assert(std::is_same_v<wchar_t*, BSTR>); // const_cast is fine: // BSTR is a wchar_t*; // ConvertBSTRToString internally uses _wcslen and WideCharToMultiByte; return to_narrow(const_cast<wchar_t*>(msg)); }
Step 1 - Classification
inline error_code make_error_code(com_error_enum e) noexcept { return { int(e), com_category() }; } inline const error_category& com_category() noexcept { // immortalized object would be perfect, but isn’t subject of this post static com_error_category ecat; return ecat; }
Step 2 - Error Category
inline string com_error_category::message(int hr) const { // leverage _com_error::ErrorMessage #ifdef _UNICODE auto narrow = detail::to_narrow(_com_error{ hr }.ErrorMessage()); return narrow.get(); #else return _com_error{ hr }.ErrorMessage(); #endif } inline error_condition com_error_category::default_error_condition(int hr) const noexcept { if (HRESULT_CODE(hr) || hr == 0) // system error condition return system_category().default_error_condition(HRESULT_CODE(hr)); else // special error condition return { hr, com_category() }; }
Step 3 - Translation (manual)
inline system_error com_to_system_error(HRESULT hr, const string& msg) { // simply forward to com_to_system_error taking a C string return com_to_system_error(hr, msg.c_str()); } inline system_error com_to_system_error(HRESULT hr, const char* msg) { // construct from error_code and message return { { com_error_enum(hr) }, msg }; } inline system_error com_to_system_error(HRESULT hr, const wchar_t* msg) { return com_to_system_error(hr, *msg ? detail::to_narrow(msg).get() : ""); } inline system_error com_to_system_error(HRESULT hr, const wstring& msg) { return com_to_system_error(hr, msg.c_str()); }
Step 3a - Desktop Family Apps
inline system_error com_to_system_error(const _com_error& e) { // note: by forwarding to com_to_system_error(HRESULT, IErrorInfo*) // we benefit from hoisting _com_error::Description because _bstr_t wraps // up both unicode/ascii strings in a ref counter allocated on the heap return com_to_system_error(e.Error(), IErrorInfoPtr{ e.ErrorInfo(), false }); } inline system_error com_to_system_error(HRESULT hr, IErrorInfo* help) { using com_cstr = unique_ptr<OLECHAR[], decltype(SysFreeString)*>; auto getDescription = [](IErrorInfo* help) -> com_cstr { BSTR description = nullptr; if (help) help->GetDescription(&description); return { description, &SysFreeString }; }; com_cstr&& description = getDescription(e); // remove trailing newline or dot (end of last sentence) if (unsigned int length = description ? SysStringLen(description) : 0) { unsigned int n = length; // place sentinel, other than [\r\n.] OLECHAR ch0 = exchange(description[0], L'\0'); for (;;) { switch (description[n - 1]) { case L'\r': case L'\n': case L'.': continue; } break; } // note: null-terminating is less ideal than just finding the new EOS, // but system_error copies its description string argument, // hence we don't gain anything from range-construction if (n < length && n) description[n] = L'\0'; // reestablish 1st character if (n) description[0] = ch0; return com_to_system_error(e.Error(), description.get()); } // no description available return com_to_system_error(e.Error()); }
Step 3b - Non-Desktop Apps
std::system_error com_to_system_error(const _com_error& e) { // no description available; // wait, actually there is, but we don't know whether // _com_error has been constructed with a custom message or whether // _com_error::ErrorMessage() retrieves a formatted error code message. // Because the description message gets prepended when making system_error // we might end up with twice the same message. // That's why we cross-check with a newly constructed _com_error. bool hasCustomMessage; { _com_error e2{ e.Error() }; const wchar_t* msg = e.ErrorMessage(), msg2 = e2.ErrorMessage(); hasCustomMessage = msg && (!msg2 || std::wcscmp(msg, msg2)); } return com_to_system_error(e.Error(), hasCustomMessage ? detail::to_narrow(msg) : ""); }
Step 4 - Translation (automatic)
Step 4a - Desktop Family Apps
inline void __stdcall throw_translated_com_error(HRESULT hr, IErrorInfo* help) { throw com_to_system_error(hr, help); }
Step 4b - Non-Desktop Apps
inline void __stdcall throw_translated_com_error(HRESULT hr, const wchar_t* msg) { throw com_to_system_error(hr, msg ? msg : L””); }
Happy coding!
Klaus Triendl