diff --git a/include/wil/cppwinrt_authoring.h b/include/wil/cppwinrt_authoring.h new file mode 100644 index 00000000..ee9de6d4 --- /dev/null +++ b/include/wil/cppwinrt_authoring.h @@ -0,0 +1,275 @@ +//********************************************************* +// +// Copyright (c) Microsoft. All rights reserved. +// This code is licensed under the MIT License. +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +// PARTICULAR PURPOSE AND NONINFRINGEMENT. +// +//********************************************************* + +namespace wil +{ +#ifndef __WIL_CPPWINRT_AUTHORING_INCLUDED +#define __WIL_CPPWINRT_AUTHORING_INCLUDED + namespace details + { + template + struct single_threaded_property_storage + { + T m_value{}; + single_threaded_property_storage() = default; + single_threaded_property_storage(const T& value) : m_value(value) {} + operator T& () { return m_value; } + operator T const& () const { return m_value; } + template auto operator=(Q&& q) + { + m_value = wistd::forward(q); + return *this; + } + }; + } + + template + struct single_threaded_ro_property : std::conditional_t, wil::details::single_threaded_property_storage, T> + { + single_threaded_ro_property() = default; + single_threaded_ro_property(const T& t) : base_type(t) { } + + const auto& operator()() + { + return *this; + } + template auto operator=(Q&& q) = delete; + private: + using base_type = std::conditional_t, wil::details::single_threaded_property_storage, T>; + }; + + template + struct single_threaded_rw_property : single_threaded_ro_property + { + template + single_threaded_rw_property(TArgs&&... args) : single_threaded_ro_property(std::forward(args)...) { } + + using single_threaded_ro_property::operator(); + + auto& operator()() + { + return *this; + } + template auto& operator()(Q&& q) + { + *this = std::forward(q); + return *this; + } + + + template auto operator=(Q&& q) + { + return static_cast(*this) = std::forward(q); + } + }; + +#endif // __WIL_CPPWINRT_AUTHORING_INCLUDED + +#if !defined(__WIL_CPPWINRT_AUTHORING_INCLUDED_FOUNDATION) && defined(WINRT_Windows_Foundation_H) // WinRT / XAML helpers +#define __WIL_CPPWINRT_AUTHORING_INCLUDED_FOUNDATION + namespace details { + template + struct event_base { + winrt::event_token operator()(T&& handler) { + return m_handler.add(std::forward(handler)); + } + auto operator()(const winrt::event_token& token) noexcept { return m_handler.remove(token); } + + template + auto invoke(TArgs&&... args) { + return m_handler(std::forward(args)...); + } + private: + winrt::event m_handler; + }; + } + + /** + * @brief A default event handler that maps to [Windows.Foundation.EventHandler](https://docs.microsoft.com/uwp/api/windows.foundation.eventhandler-1). + * @tparam T The event data type. + */ + template + struct simple_event : wil::details::event_base> {}; + + /** + * @brief A default event handler that maps to [Windows.Foundation.TypedEventHandler](https://docs.microsoft.com/uwp/api/windows.foundation.typedeventhandler-2). + * @tparam T The event data type. + * @details Usage example: + * @code + * // In IDL, this corresponds to: + * // event Windows.Foundation.TypedEventHandler OkClicked; + * wil::typed_event OkClicked; + * @endcode + */ + template + struct typed_event : wil::details::event_base> {}; + +#endif // !defined(__WIL_CPPWINRT_AUTHORING_INCLUDED_FOUNDATION) && defined(WINRT_Windows_Foundation_H) + +#if !defined(__WIL_CPPWINRT_AUTHORING_INCLUDED_XAML_DATA) // INotifyPropertyChanged helpers +#define __WIL_CPPWINRT_AUTHORING_INCLUDED_XAML_DATA +namespace details +{ +#ifdef WINRT_Microsoft_UI_Xaml_Data_H + using Xaml_Data_PropertyChangedEventHandler = winrt::Microsoft::UI::Xaml::Data::PropertyChangedEventHandler; + using Xaml_Data_PropertyChangedEventArgs = winrt::Microsoft::UI::Xaml::Data::PropertyChangedEventArgs; +#elif defined(WINRT_Windows_UI_Xaml_Data_H) + using Xaml_Data_PropertyChangedEventHandler = winrt::Windows::UI::Xaml::Data::PropertyChangedEventHandler; + using Xaml_Data_PropertyChangedEventArgs = winrt::Windows::UI::Xaml::Data::PropertyChangedEventArgs; +#endif +} + +#if defined(WINRT_Microsoft_UI_Xaml_Data_H) || defined(WINRT_Windows_UI_Xaml_Data_H) + + /** + * @brief Helper base class to inherit from to have a simple implementation of [INotifyPropertyChanged](https://docs.microsoft.com/uwp/api/windows.ui.xaml.data.inotifypropertychanged). + * @tparam T CRTP type + * @details When you declare your class, make this class a base class and pass your class as a template parameter: + * @code + * struct MyPage : MyPageT, wil::notify_property_changed_base { + * wil::single_threaded_notifying_property MyInt; + * + * MyPage() : INIT_PROPERTY(MyInt, 42) { } + * }; + * @endcode + */ + template> + > + struct notify_property_changed_base { + public: + using Type = T; + auto PropertyChanged(Xaml_Data_PropertyChangedEventHandler const& value) { + return m_propertyChanged.add(value); + } + void PropertyChanged(winrt::event_token const& token) { + m_propertyChanged.remove(token); + } + Type& self() { + return *static_cast(this); + } + + /** + * @brief Raises a property change notification event + * @param name The name of the property + * @return + * @details Usage example\n + * C++ + * @code + * void MyPage::DoSomething() { + * // modify MyInt + * // MyInt = ... + * + * // now send a notification to update the bound UI elements + * RaisePropertyChanged(L"MyInt"); + * } + * @endcode + */ + auto RaisePropertyChanged(std::wstring_view name) { + return m_propertyChanged(self(), Xaml_Data_PropertyChangedEventHandler{ name }); + } + protected: + winrt::event m_propertyChanged; + }; + + /** + * @brief Implements a property type with notifications + * @tparam T the property type + * @details Use the #INIT_NOTIFY_PROPERTY macro to initialize this property in your class constructor. This will set up the right property name, and bind it to the `notify_property_changed_base` implementation. + */ + template + struct single_threaded_notifying_property : single_threaded_rw_property { + using Type = T; + + using single_threaded_rw_property::operator(); + + void operator()(const T& value) { + if (value != this->m_value) { + single_threaded_rw_property::operator()(value); + Raise(); + } + } + template + single_threaded_notifying_property( + winrt::event* npc, + winrt::Windows::Foundation::IInspectable sender, + std::wstring_view name, + TArgs&&... args) : + single_threaded_rw_property(std::forward(args)...), + m_name(name), + m_npc(npc), + m_sender(sender) + {} + + single_threaded_notifying_property(const single_threaded_notifying_property&) = default; + single_threaded_notifying_property(single_threaded_notifying_property&&) = default; + std::wstring_view Name() const noexcept { return m_name; } + auto& Raise() + { + if (m_npc) + { + (*m_npc)(m_sender, Xaml_Data_PropertyChangedEventArgs{ m_name }); + } + return *this; + } + private: + std::wstring_view m_name; + winrt::event* m_npc; + winrt::Windows::Foundation::IInspectable m_sender; + }; + + /** + * @def INIT_NOTIFY_PROPERTY + * @brief use this to initialize a wil::single_threaded_notifying_property in your class constructor. + */ +#define INIT_NOTIFY_PROPERTY(NAME, VALUE) \ + NAME(&m_propertyChanged, *this, L#NAME, VALUE) + + namespace details + { + template + inline constexpr bool make_false(T) + { + return false; + } + } + +#if defined(_MSC_VER) +// Gets the name of a member (such as a property) as a constexpr string literal. +// This is commonly needed when checking which property changed after receiving a PropertyChanged event. +// It will enforce at compile time that propertyName is a member of typeName, then return propertyName as a string literal. +#define WIL_NAMEOF_MEMBER(typeName, propertyName) \ + __pragma(warning(push)) \ + __pragma(warning(disable:6237)) \ + std::wstring_view( \ + (false && wil::details::make_false(std::add_pointer_t{nullptr}->propertyName) \ + ? L"" : (L"" #propertyName))) \ + __pragma(warning(pop)) +#elif defined(__clang__) +// Gets the name of a member (such as a property) as a constexpr string literal. +// This is commonly needed when checking which property changed after receiving a PropertyChanged event. +// It will enforce at compile time that propertyName is a member of typeName, then return propertyName as a string literal. +#define WIL_NAMEOF_MEMBER(typeName, propertyName) \ + _Pragma("clang diagnostic push") \ + _Pragma("clang diagnostic ignored \"-Wtautological-constant-out-of-range-compare\"") + std::wstring_view( \ + (false && wil::details::make_false(std::add_pointer_t{nullptr}->propertyName) \ + ? L"" : (L"" #propertyName))) \ + _Pragma("clang diagnostic pop") +#endif + +#endif // defined(WINRT_Microsoft_UI_Xaml_Data_H) || defined(WINRT_Windows_UI_Xaml_Data_H) + +#endif // !defined(__WIL_CPPWINRT_AUTHORING_INCLUDED_XAML_DATA) +} // namespace wil diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 5020a6c3..ff5a1127 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -23,7 +23,7 @@ endif() set(CPPWINRT ${CMAKE_BINARY_DIR}/packages/Microsoft.Windows.CppWinRT.${CPPWINRT_VERSION}/bin/cppwinrt.exe) execute_process(COMMAND - ${CPPWINRT} -input sdk -output include + ${CPPWINRT} -input sdk+ -output include WORKING_DIRECTORY ${CMAKE_BINARY_DIR} RESULT_VARIABLE ret) if (NOT ret EQUAL 0) @@ -42,8 +42,8 @@ endif() # functions not marked 'inline'. The reason we prefer 'RelWithDebInfo' over 'Release' is to get debug info, so manually # revert to the desired (and default) inlining behavior as that exercises more interesting code paths if (${CMAKE_BUILD_TYPE} STREQUAL "RelWithDebInfo") - # TODO: This is currently blocked by an apparent Clang bug: https://github.com/llvm/llvm-project/issues/59690 - # replace_cxx_flag("/Ob1" "/Ob2") + # TODO: This is currently blocked by an apparent Clang bug: https://github.com/llvm/llvm-project/issues/59690 + # replace_cxx_flag("/Ob1" "/Ob2") endif() set(COMMON_SOURCES @@ -77,7 +77,7 @@ if (${CMAKE_BUILD_TYPE} STREQUAL "Debug") set(DEBUG_BUILD TRUE) set(HAS_DEBUG_INFO TRUE) elseif(${CMAKE_BUILD_TYPE} STREQUAL "RelWithDebInfo") - set(HAS_DEBUG_INFO TRUE) + set(HAS_DEBUG_INFO TRUE) endif() set(ASAN_AVAILABLE FALSE) diff --git a/tests/CppWinRTAuthoringTests.cpp b/tests/CppWinRTAuthoringTests.cpp new file mode 100644 index 00000000..54295bb1 --- /dev/null +++ b/tests/CppWinRTAuthoringTests.cpp @@ -0,0 +1,177 @@ +#define WINAPI_PARTITION_DESKTOP 1 // for RO_INIT_SINGLETHREADED +#include "common.h" +#undef GetCurrentTime +// check if at least C++17 +#if _MSVC_LANG >= 201703L +#include +#include +#endif + +#include +#include +#include + +TEST_CASE("CppWinRTAuthoringTests::ReadOnly", "[property]") +{ + int value = 42; + wil::single_threaded_ro_property prop(value); + REQUIRE(prop == value); + REQUIRE(prop() == value); + REQUIRE(prop == prop()); + REQUIRE(prop == prop); + + wil::single_threaded_ro_property prop2 = prop; + REQUIRE(prop2 == value); + REQUIRE(prop2() == value); + REQUIRE(prop2 == prop()); + REQUIRE(prop2 == prop); + + wil::single_threaded_ro_property prop3; + REQUIRE(prop3.empty()); +} + +TEST_CASE("CppWinRTAuthoringTests::ReadWrite", "[property]") +{ + int value = 42; + wil::single_threaded_rw_property prop(value); + REQUIRE(prop == value); + REQUIRE(prop() == value); + REQUIRE(prop == prop()); + REQUIRE(prop == prop); + + wil::single_threaded_rw_property prop2 = prop; + REQUIRE(prop2 == value); + REQUIRE(prop2() == value); + REQUIRE(prop2 == prop()); + REQUIRE(prop2 == prop); + + int value2 = 43; + prop2 = value2; + REQUIRE(prop2 == value2); + REQUIRE(prop2() == value2); + REQUIRE(prop2 == prop2()); + REQUIRE(prop2 == prop2); + + wil::single_threaded_rw_property prop3("foo"); + REQUIRE(prop3 == "foo"); + REQUIRE(prop3() == "foo"); + REQUIRE(prop3.length() == 3); + prop3 = "bar"; + REQUIRE(prop3 == "bar"); + auto& prop3alias = prop3("baz"); + REQUIRE(prop3 == "baz"); + prop3alias = "foo"; + REQUIRE(prop3 == "foo"); +} + +TEST_CASE("CppWinRTAuthoringTests::ReadWriteFromReadOnly", "[property]") +{ + int value = 42; + wil::single_threaded_ro_property prop(value); + REQUIRE(prop == value); + REQUIRE(prop() == value); + REQUIRE(prop == prop()); + REQUIRE(prop == prop); + + wil::single_threaded_rw_property prop2 = prop; + REQUIRE(prop2 == value); + REQUIRE(prop2() == value); + REQUIRE(prop2 == prop()); + REQUIRE(prop2 == prop); + + int value2 = 43; + prop2 = value2; + REQUIRE(prop2 == value2); + REQUIRE(prop2() == value2); + REQUIRE(prop2 == prop2()); + REQUIRE(prop2 == prop2); +} + +TEST_CASE("CppWinRTAuthoringTests::InStruct", "[property]") +{ + struct TestStruct + { + wil::single_threaded_ro_property Prop1{ 42 }; + wil::single_threaded_rw_property Prop2{ 1 }; + wil::single_threaded_ro_property Prop3{ 44 }; + }; + + TestStruct test; + static_assert(!std::is_assignable_v, int>, "cannot assign to a readonly property"); + + test.Prop2 = 43; + static_assert(!std::is_assignable_v, "cannot assign to a readonly property"); + + REQUIRE(test.Prop1 == 42); + REQUIRE(test.Prop2 == 43); + REQUIRE(test.Prop3 == 44); + + test.Prop2 = 45; + REQUIRE(test.Prop2 == 45); + + REQUIRE(test.Prop1() == 42); + test.Prop2(99); + REQUIRE(test.Prop2() == 99); + test.Prop2(22)(33); + REQUIRE(test.Prop2() == 33); +} + +#ifdef WINRT_Windows_Foundation_H +TEST_CASE("CppWinRTAuthoringTests::Events", "[property]") +{ + struct Test + { + wil::simple_event MyEvent; + + wil::typed_event MyTypedEvent; + } test; + + auto token = test.MyEvent([](winrt::Windows::Foundation::IInspectable, int args) { REQUIRE(args == 42); }); + test.MyEvent.invoke(nullptr, 42); + test.MyEvent(token); + + auto token2 = test.MyTypedEvent([](winrt::Windows::Foundation::IInspectable, int args) { REQUIRE(args == 42); }); + test.MyTypedEvent.invoke(nullptr, 42); + test.MyTypedEvent(token2); +} +#endif // WINRT_Windows_Foundation_H + +// if using MSVC since we need the embedded manifest bits to use XAML islands +#if _MSVC_LANG >= 201703L && defined(WINRT_Windows_UI_Xaml_Data_H) + +#include + +// This test cannot run in the same process as the malloc spies tests in wiTest.cpp +// MSFT_internal: https://task.ms/44191550 +TEST_CASE("CppWinRTAuthoringTests::NotifyPropertyChanged", "[property][LocalOnly]") +{ +#if defined(WIL_ENABLE_EXCEPTIONS) + auto uninit = wil::RoInitialize_failfast(RO_INIT_SINGLETHREADED); + // We need to initialize the XAML core in order to instantiate a PropertyChangedEventArgs. + auto manager = winrt::Windows::UI::Xaml::Hosting::WindowsXamlManager::InitializeForCurrentThread(); + + struct Test : winrt::implements, wil::notify_property_changed_base + { + wil::single_threaded_notifying_property MyProperty; + Test() : INIT_NOTIFY_PROPERTY(MyProperty, 42) {} + }; + auto test = winrt::make(); + auto testImpl = winrt::get_self(test); + + auto token = test.PropertyChanged([&](winrt::Windows::Foundation::IInspectable, winrt::Windows::UI::Xaml::Data::PropertyChangedEventArgs args) + { + REQUIRE(args.PropertyName() == L"MyProperty"); + REQUIRE(testImpl->MyProperty() == 43); + }); + + testImpl->MyProperty(43); + test.PropertyChanged(token); + REQUIRE(testImpl->MyProperty.Name() == L"MyProperty"); + + constexpr auto name = WIL_NAMEOF_MEMBER(Test, MyProperty); + static_assert(name == L"MyProperty"); + + manager.Close(); +#endif +} +#endif // msvc \ No newline at end of file diff --git a/tests/FileSystemTests.cpp b/tests/FileSystemTests.cpp index a6e8b5ca..d7d03de0 100644 --- a/tests/FileSystemTests.cpp +++ b/tests/FileSystemTests.cpp @@ -610,7 +610,7 @@ TEST_CASE("FileSystemTests::QueryFullProcessImageNameW and GetModuleFileNameW", #ifdef WIL_ENABLE_EXCEPTIONS auto procName = wil::QueryFullProcessImageNameW(); auto moduleName = wil::GetModuleFileNameW(); - REQUIRE(procName == moduleName); + REQUIRE(CompareStringOrdinal(procName.c_str(), -1, moduleName.c_str(), -1, TRUE) == CSTR_EQUAL); #endif } diff --git a/tests/cpplatest/CMakeLists.txt b/tests/cpplatest/CMakeLists.txt index 7ae64d54..f0259787 100644 --- a/tests/cpplatest/CMakeLists.txt +++ b/tests/cpplatest/CMakeLists.txt @@ -19,10 +19,12 @@ target_sources(witest.cpplatest PRIVATE ${COROUTINE_SOURCES} ${CMAKE_CURRENT_SOURCE_DIR}/../CppWinRTTests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../CppWinRT20Tests.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../CppWinRTAuthoringTests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../StlTests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../TokenHelpersTests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../UniqueWinRTEventTokenTests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../WatcherTests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../WinRTTests.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../WinVerifyTrustTest.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/app.manifest ) diff --git a/tests/cpplatest/app.manifest b/tests/cpplatest/app.manifest new file mode 100644 index 00000000..45127741 --- /dev/null +++ b/tests/cpplatest/app.manifest @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + +