diff --git a/CMakeLists.txt b/CMakeLists.txt index 626814c59..b59d9b261 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,5 +7,14 @@ if (NOT DEFINED WIL_BUILD_VERSION) set(WIL_BUILD_VERSION "0.0.0") endif() +# Detect the Windows SDK version. If we're using the Visual Studio generator, this will be provided for us. Otherwise +# we'll need to assume that this value comes from the command line (e.g. through the VS command prompt) +if (DEFINED CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION) + set(WIL_WINDOWS_SDK_VERSION ${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION}) +else() + # This has a trailing backslash for whatever reason... + string(REGEX REPLACE "\\\\$" "" WIL_WINDOWS_SDK_VERSION "$ENV{WindowsSDKVersion}") +endif() + add_subdirectory(packaging) add_subdirectory(tests) diff --git a/include/wil/cppwinrt.h b/include/wil/cppwinrt.h index 61c3a5785..bf846d76f 100644 --- a/include/wil/cppwinrt.h +++ b/include/wil/cppwinrt.h @@ -16,47 +16,70 @@ #include #include -#ifdef CPPWINRT_VERSION -#error Please include wil/cppwinrt.h before including any C++/WinRT headers -#endif +// WIL and C++/WinRT use two different exception types for communicating HRESULT failures. Thus, both libraries need to +// understand how to translate these exception types into the correct HRESULT values at the ABI boundary. Prior to +// C++/WinRT "2.0" this was accomplished by injecting the WINRT_EXTERNAL_CATCH_CLAUSE macro - that WIL defines below - +// into its exception handler (winrt::to_hresult). Starting with C++/WinRT "2.0" this mechanism has shifted to a global +// function pointer - winrt_to_hresult_handler - that WIL sets automatically when this header is included and +// 'RESULT_SUPPRESS_STATIC_INITIALIZERS' is not defined. -#ifdef __WIL_RESULTMACROS_INCLUDED -#error Please include wil/cppwinrt.h before including result_macros.h -#endif +/// @cond +namespace wil::details +{ + // Since the C++/WinRT version macro is a string... + inline constexpr int major_version_from_string(const char* versionString) + { + int result = 0; + auto str = versionString; + while ((*str >= '0') && (*str <= '9')) + { + result = result * 10 + (*str - '0'); + ++str; + } -#ifdef WINRT_EXTERNAL_CATCH_CLAUSE -#error C++/WinRT external catch clause already defined outside of WIL -#endif + return result; + } +} +/// @endcond -// WIL and C++/WinRT use two different exception types for communicating HRESULT failures. Thus, both libraries need to -// understand how to translate these exception types into the correct HRESULT values at the ABI boundary. C++/WinRT -// accomplishes this by injecting the WINRT_EXTERNAL_CATCH_CLAUSE macro - that WIL defines below - into its exception -// handler. WIL accomplishes this by detecting this file's inclusion in result_macros.h and modifies its behavior to -// account for the C++/WinRT exception type. +#ifdef CPPWINRT_VERSION +// Prior to C++/WinRT "2.0" this header needed to be included before 'winrt/base.h' so that our definition of +// 'WINRT_EXTERNAL_CATCH_CLAUSE' would get picked up in the implementation of 'winrt::to_hresult'. This is no longer +// problematic, so only emit an error when using a version of C++/WinRT prior to 2.0 +static_assert(::wil::details::major_version_from_string(CPPWINRT_VERSION) >= 2, + "Please include wil/cppwinrt.h before including any C++/WinRT headers"); +#endif +// NOTE: Will eventually be removed once C++/WinRT 2.0 use can be assumed +#ifdef WINRT_EXTERNAL_CATCH_CLAUSE +#define __WI_CONFLICTING_WINRT_EXTERNAL_CATCH_CLAUSE 1 +#else #define WINRT_EXTERNAL_CATCH_CLAUSE \ catch (const wil::ResultException& e) \ { \ return winrt::hresult_error(e.GetErrorCode(), winrt::to_hstring(e.what())).to_abi(); \ } - -namespace wil::details -{ - // Due to header dependency issues, result_macros.h cannot reference winrt::hresult_error, so instead declare - // functions that we can define after including base.h - HRESULT __stdcall ResultFromCppWinRTException( - _Inout_updates_opt_(debugStringChars) PWSTR debugString, - _When_(debugString != nullptr, _Pre_satisfies_(debugStringChars > 0)) size_t debugStringChars) WI_NOEXCEPT; -} +#endif #include "result_macros.h" #include +#if __WI_CONFLICTING_WINRT_EXTERNAL_CATCH_CLAUSE +static_assert(::wil::details::major_version_from_string(CPPWINRT_VERSION) >= 2, + "C++/WinRT external catch clause already defined outside of WIL"); +#endif + +// In C++/WinRT 2.0 and beyond, this function pointer exists. In earlier versions it does not. It's much easier to avoid +// linker errors than it is to SFINAE on variable existence, so we declare the variable here, but are careful not to +// use it unless the version of C++/WinRT is high enough +extern std::int32_t(__stdcall* winrt_to_hresult_handler)(void*) noexcept; + +/// @cond namespace wil::details { inline HRESULT __stdcall ResultFromCppWinRTException( _Inout_updates_opt_(debugStringChars) PWSTR debugString, - _When_(debugString != nullptr, _Pre_satisfies_(debugStringChars > 0)) size_t debugStringChars) WI_NOEXCEPT + _When_(debugString != nullptr, _Pre_satisfies_(debugStringChars > 0)) size_t debugStringChars) noexcept { try { @@ -78,9 +101,40 @@ namespace wil::details } } } +/// @endcond namespace wil { + inline std::int32_t __stdcall winrt_to_hresult(void* returnAddress) noexcept + { + // C++/WinRT only gives us the return address (caller), so pass along an empty 'DiagnosticsInfo' since we don't + // have accurate file/line/etc. information + return static_cast(details::ReportFailure_CaughtException(__R_DIAGNOSTICS_RA(DiagnosticsInfo{}, returnAddress), FailureType::Return)); + } + + inline void WilInitialize_CppWinRT() + { + details::g_pfnResultFromCaughtException_CppWinRt = details::ResultFromCppWinRTException; + if constexpr (details::major_version_from_string(CPPWINRT_VERSION) >= 2) + { + WI_ASSERT(winrt_to_hresult_handler == nullptr); + winrt_to_hresult_handler = winrt_to_hresult; + } + } + + /// @cond + namespace details + { +#ifndef RESULT_SUPPRESS_STATIC_INITIALIZERS + WI_HEADER_INITITALIZATION_FUNCTION(WilInitialize_CppWinRT, [] + { + ::wil::WilInitialize_CppWinRT(); + return 1; + }); +#endif + } + /// @endcond + // Provides an overload of verify_hresult so that the WIL macros can recognize winrt::hresult as a valid "hresult" type. inline long verify_hresult(winrt::hresult hr) noexcept { diff --git a/include/wil/result_macros.h b/include/wil/result_macros.h index cc2601873..22c2795d4 100644 --- a/include/wil/result_macros.h +++ b/include/wil/result_macros.h @@ -1204,6 +1204,9 @@ namespace wil __declspec(selectany) void(__stdcall *g_pfnThrowResultException)(const FailureInfo& failure) = nullptr; extern "C" __declspec(selectany) HRESULT(__stdcall *g_pfnResultFromCaughtExceptionInternal)(_Out_writes_opt_(debugStringChars) PWSTR debugString, _When_(debugString != nullptr, _Pre_satisfies_(debugStringChars > 0)) size_t debugStringChars, _Out_ bool* isNormalized) WI_PFN_NOEXCEPT = nullptr; + // C++/WinRT additions + extern "C" __declspec(selectany) HRESULT(__stdcall *g_pfnResultFromCaughtException_CppWinRt)(_Out_writes_opt_(debugStringChars) PWSTR debugString, _When_(debugString != nullptr, _Pre_satisfies_(debugStringChars > 0)) size_t debugStringChars) WI_PFN_NOEXCEPT = nullptr; + // C++/cx compiled additions extern "C" __declspec(selectany) void(__stdcall *g_pfnThrowPlatformException)(FailureInfo const &failure, PCWSTR debugString) = nullptr; extern "C" __declspec(selectany) _Always_(_Post_satisfies_(return < 0)) HRESULT(__stdcall *g_pfnResultFromCaughtException_WinRt)(_Inout_updates_opt_(debugStringChars) PWSTR debugString, _When_(debugString != nullptr, _Pre_satisfies_(debugStringChars > 0)) size_t debugStringChars, _Out_ bool* isNormalized) WI_PFN_NOEXCEPT = nullptr; @@ -2481,6 +2484,24 @@ namespace wil return hr; } + inline HRESULT ResultFromKnownException_CppWinRT(const DiagnosticsInfo& diagnostics, void* returnAddress) + { + if (g_pfnResultFromCaughtException_CppWinRt) + { + wchar_t message[2048]; + message[0] = L'\0'; + auto hr = g_pfnResultFromCaughtException_CppWinRt(message, ARRAYSIZE(message)); + if (FAILED(hr)) + { + ReportFailure(__R_DIAGNOSTICS_RA(diagnostics, returnAddress), FailureType::Log, hr, message); + return hr; + } + } + + // Indicate that this either isn't a C++/WinRT exception or a handler isn't configured by returning success + return S_OK; + } + inline HRESULT RecognizeCaughtExceptionFromCallback(_Inout_updates_opt_(debugStringChars) PWSTR debugString, _When_(debugString != nullptr, _Pre_satisfies_(debugStringChars > 0)) size_t debugStringChars) { HRESULT hr = g_pfnResultFromCaughtException(); @@ -2587,13 +2608,15 @@ namespace wil catch (...) { HRESULT hr; -#ifdef __WIL_CPPWINRT_INCLUDED - hr = ResultFromCppWinRTException(debugString, debugStringChars); - if (FAILED(hr)) + + if (g_pfnResultFromCaughtException_CppWinRt) { - return hr; + hr = g_pfnResultFromCaughtException_CppWinRt(debugString, debugStringChars); + if (FAILED(hr)) + { + return hr; + } } -#endif hr = RecognizeCaughtExceptionFromCallback(debugString, debugStringChars); if (FAILED(hr)) @@ -2631,13 +2654,14 @@ namespace wil } catch (...) { -#ifdef __WIL_CPPWINRT_INCLUDED - auto hr = ResultFromCppWinRTException(debugString, debugStringChars); - if (FAILED(hr)) + if (g_pfnResultFromCaughtException_CppWinRt) { - return hr; + auto hr = g_pfnResultFromCaughtException_CppWinRt(debugString, debugStringChars); + if (FAILED(hr)) + { + return hr; + } } -#endif } } @@ -2675,21 +2699,13 @@ namespace wil } catch (...) { - HRESULT hr; - wchar_t message[2048] = L""; - -#ifdef __WIL_CPPWINRT_INCLUDED - hr = ResultFromCppWinRTException(message, ARRAYSIZE(message)); + auto hr = ResultFromKnownException_CppWinRT(diagnostics, returnAddress); if (FAILED(hr)) { - ReportFailure(__R_DIAGNOSTICS_RA(diagnostics, returnAddress), FailureType::Log, hr, message); return hr; } -#endif // Unknown exception - UNREFERENCED_PARAMETER(hr); - UNREFERENCED_PARAMETER(message); throw; } break; @@ -2792,13 +2808,14 @@ namespace wil catch (...) { HRESULT hr; -#ifdef __WIL_CPPWINRT_INCLUDED - hr = ResultFromCppWinRTException(debugString, debugStringChars); - if (FAILED(hr)) + if (g_pfnResultFromCaughtException_CppWinRt) { - return hr; + hr = g_pfnResultFromCaughtException_CppWinRt(debugString, debugStringChars); + if (FAILED(hr)) + { + return hr; + } } -#endif hr = RecognizeCaughtExceptionFromCallback(debugString, debugStringChars); if (FAILED(hr)) @@ -2831,13 +2848,14 @@ namespace wil } catch (...) { -#ifdef __WIL_CPPWINRT_INCLUDED - auto hr = ResultFromCppWinRTException(debugString, debugStringChars); - if (FAILED(hr)) + if (g_pfnResultFromCaughtException_CppWinRt) { - return hr; + auto hr = g_pfnResultFromCaughtException_CppWinRt(debugString, debugStringChars); + if (FAILED(hr)) + { + return hr; + } } -#endif } } @@ -2882,21 +2900,13 @@ namespace wil } catch (...) { - HRESULT hr; - wchar_t message[2048] = L""; - -#ifdef __WIL_CPPWINRT_INCLUDED - hr = ResultFromCppWinRTException(message, ARRAYSIZE(message)); + auto hr = ResultFromKnownException_CppWinRT(diagnostics, returnAddress); if (FAILED(hr)) { - ReportFailure(__R_DIAGNOSTICS_RA(diagnostics, returnAddress), FailureType::Log, hr, message); return hr; } -#endif // Unknown exception - UNREFERENCED_PARAMETER(hr); - UNREFERENCED_PARAMETER(message); throw; } diff --git a/tests/CppWinRT20Tests.cpp b/tests/CppWinRT20Tests.cpp new file mode 100644 index 000000000..1c6f69ccc --- /dev/null +++ b/tests/CppWinRT20Tests.cpp @@ -0,0 +1,28 @@ + +#include "common.h" + +// Prior to C++/WinRT 2.0 this would cause issues since we're not including wil/cppwinrt.h in this translation unit. +// However, since we're going to link into the same executable as 'CppWinRTTests.cpp', the 'winrt_to_hresult_handler' +// global function pointer should be set, so these should all run successfully + +#include +#include + +TEST_CASE("CppWinRTTests::CppWinRT20Test", "[cppwinrt]") +{ + auto test = [](HRESULT hr) + { + try + { + THROW_HR(hr); + } + catch (...) + { + REQUIRE(hr == winrt::to_hresult()); + } + }; + + test(E_OUTOFMEMORY); + test(E_INVALIDARG); + test(E_UNEXPECTED); +} diff --git a/tests/CppWinRTTests.cpp b/tests/CppWinRTTests.cpp index a2e69e06f..56bfad6c4 100644 --- a/tests/CppWinRTTests.cpp +++ b/tests/CppWinRTTests.cpp @@ -3,6 +3,25 @@ #include +// HRESULT values that C++/WinRT throws as something other than winrt::hresult_error - e.g. a type derived from +// winrt::hresult_error, std::*, etc. +static const HRESULT cppwinrt_mapped_hresults[] = +{ + E_ACCESSDENIED, + RPC_E_WRONG_THREAD, + E_NOTIMPL, + E_INVALIDARG, + E_BOUNDS, + E_NOINTERFACE, + CLASS_E_CLASSNOTAVAILABLE, + E_CHANGED_STATE, + E_ILLEGAL_METHOD_CALL, + E_ILLEGAL_STATE_CHANGE, + E_ILLEGAL_DELEGATE_ASSIGNMENT, + HRESULT_FROM_WIN32(ERROR_CANCELLED), + E_OUTOFMEMORY, +}; + TEST_CASE("CppWinRTTests::WilToCppWinRTExceptionTranslationTest", "[cppwinrt]") { auto test = [](HRESULT hr) @@ -17,11 +36,13 @@ TEST_CASE("CppWinRTTests::WilToCppWinRTExceptionTranslationTest", "[cppwinrt]") } }; + for (auto hr : cppwinrt_mapped_hresults) + { + test(hr); + } + + // A non-mapped HRESULT test(E_UNEXPECTED); - test(E_ACCESSDENIED); - test(E_INVALIDARG); - test(E_HANDLE); - test(E_OUTOFMEMORY); } TEST_CASE("CppWinRTTests::CppWinRTToWilExceptionTranslationTest", "[cppwinrt]") @@ -38,11 +59,13 @@ TEST_CASE("CppWinRTTests::CppWinRTToWilExceptionTranslationTest", "[cppwinrt]") } }; + for (auto hr : cppwinrt_mapped_hresults) + { + test(hr); + } + + // A non-mapped HRESULT test(E_UNEXPECTED); - test(E_ACCESSDENIED); - test(E_INVALIDARG); - test(E_HANDLE); - test(E_OUTOFMEMORY); } TEST_CASE("CppWinRTTests::ResultFromExceptionDebugTest", "[cppwinrt]") @@ -56,19 +79,44 @@ TEST_CASE("CppWinRTTests::ResultFromExceptionDebugTest", "[cppwinrt]") REQUIRE(hr == result); }; - // Anything from SupportedExceptions::Known or SupportedExceptions::All should give back the same HRESULT - test(E_UNEXPECTED, wil::SupportedExceptions::Known); - test(E_ACCESSDENIED, wil::SupportedExceptions::Known); - test(E_INVALIDARG, wil::SupportedExceptions::All); - test(E_HANDLE, wil::SupportedExceptions::All); + for (auto hr : cppwinrt_mapped_hresults) + { + test(hr, wil::SupportedExceptions::Known); + test(hr, wil::SupportedExceptions::All); + } - // OOM gets translated to bad_alloc, which should always give back E_OUTOFMEMORY - test(E_OUTOFMEMORY, wil::SupportedExceptions::All); - test(E_OUTOFMEMORY, wil::SupportedExceptions::Known); - test(E_OUTOFMEMORY, wil::SupportedExceptions::ThrownOrAlloc); + // A non-mapped HRESULT + test(E_UNEXPECTED, wil::SupportedExceptions::Known); + test(E_UNEXPECTED, wil::SupportedExceptions::All); // Uncomment any of the following to validate SEH failfast //test(E_UNEXPECTED, wil::SupportedExceptions::None); //test(E_ACCESSDENIED, wil::SupportedExceptions::Thrown); //test(E_INVALIDARG, wil::SupportedExceptions::ThrownOrAlloc); } + +TEST_CASE("CppWinRTTests::CppWinRTConsistencyTest", "[cppwinrt]") +{ + // Since setting 'winrt_to_hresult_handler' opts us into _all_ C++/WinRT exception translation handling, we need to + // make sure that we preserve behavior, at least with 'check_hresult', especially when C++/WinRT maps a particular + // HRESULT value to a different exception type + auto test = [](HRESULT hr) + { + try + { + winrt::check_hresult(hr); + } + catch (...) + { + REQUIRE(hr == winrt::to_hresult()); + } + }; + + for (auto hr : cppwinrt_mapped_hresults) + { + test(hr); + } + + // A non-mapped HRESULT + test(E_UNEXPECTED); +} diff --git a/tests/cpplatest/CMakeLists.txt b/tests/cpplatest/CMakeLists.txt index 989f3dd96..a1e143819 100644 --- a/tests/cpplatest/CMakeLists.txt +++ b/tests/cpplatest/CMakeLists.txt @@ -6,6 +6,11 @@ set(CMAKE_CXX_STANDARD 17) project(witest.cpplatest) add_executable(witest.cpplatest) +# Semi-arbitrary insiders SDK version selected that uses C++/WinRT "2.0" +if ("${WIL_WINDOWS_SDK_VERSION}" VERSION_GREATER_EQUAL "10.0.18878.0") + target_sources(witest.cpplatest PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/../CppWinRT20Tests.cpp) +endif() + target_sources(witest.cpplatest PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/../main.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../CppWinRTTests.cpp