Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -334,3 +334,4 @@ ASALocalRun/

# CMake/Build output
build/
_codeql_detected_source_root
1 change: 1 addition & 0 deletions _codeql_detected_source_root
86 changes: 86 additions & 0 deletions include/wil/cppwinrt_authoring.h
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,92 @@ struct typed_event : wil::details::event_base<winrt::Windows::Foundation::TypedE
{
};

/**
* @brief Traits for wil::winrt_event that swallows exceptions thrown by event handlers.
* All handlers are called even if some throw; exceptions are discarded.
* This provides crash-resistant behavior and is the default for wil::winrt_event.
*/
struct swallow_event_errors_traits
{
static void on_handler_exception(std::exception_ptr) noexcept
{
// Intentionally discard the exception to allow remaining handlers to run.
}
};

/**
* @brief Traits for wil::winrt_event that propagates exceptions thrown by event handlers.
* If a handler throws, the exception propagates out of invoke() and remaining handlers
* are not called. This matches the behavior of winrt::event.
*/
struct propagate_event_errors_traits
{
static void on_handler_exception(std::exception_ptr ep)
{
std::rethrow_exception(ep);
}
};

/**
* @brief Traits for wil::winrt_event that calls FAIL_FAST when an event handler throws.
* Requires wil/result.h or wil/result_macros.h to be included.
*/
struct failfast_event_errors_traits
{
[[noreturn]] static void on_handler_exception(std::exception_ptr) noexcept
{
FAIL_FAST();
}
};

/**
* @brief A WinRT event with configurable exception handling for individual handlers.
* @tparam T The delegate type (e.g., winrt::Windows::Foundation::TypedEventHandler<S, A>).
* @tparam Traits Traits class controlling what happens when a handler throws an exception.
* Defaults to swallow_event_errors_traits for crash-resistant behavior where
* all handlers are invoked regardless of whether earlier ones threw.
* @details Usage example:
* @code
* // Crash-resistant event (default): exceptions from handlers are swallowed
* wil::winrt_event<winrt::Windows::Foundation::TypedEventHandler<IFoo, IBar>> MyEvent;
*
* // Event that propagates exceptions (same behavior as winrt::event):
* wil::winrt_event<winrt::Windows::Foundation::TypedEventHandler<IFoo, IBar>,
* wil::propagate_event_errors_traits> MyStrictEvent;
* @endcode
*/
template <typename T, typename Traits = swallow_event_errors_traits>
struct winrt_event
{
winrt::event_token operator()(T const& handler)
{
return m_handler.add([handler](auto&&... args) {
try
{
handler(std::forward<decltype(args)>(args)...);
}
catch (...)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot update this so the traits by-default use "wil::LogCaughtException" or whatever it's called, but that can be disabled with a flag.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in 808b460. swallow_event_errors_traits::on_handler_exception now calls LOG_CAUGHT_EXCEPTION() before returning false (keeping the handler). If you want to suppress logging, implement custom traits with an empty on_handler_exception — the doc comment notes this.

{
Traits::on_handler_exception(std::current_exception());
}
});
}

void operator()(winrt::event_token const& token) noexcept
{
m_handler.remove(token);
}

template <typename... TArgs>
void invoke(TArgs&&... args)
{
m_handler(std::forward<TArgs>(args)...);
}

private:
winrt::event<T> m_handler;
};

#endif // !defined(__WIL_CPPWINRT_AUTHORING_INCLUDED_FOUNDATION) && defined(WINRT_Windows_Foundation_H)

#if (!defined(__WIL_CPPWINRT_AUTHORING_INCLUDED_XAML_DATA) && (defined(WINRT_Microsoft_UI_Xaml_Data_H) || defined(WINRT_Windows_UI_Xaml_Data_H))) || \
Expand Down
96 changes: 96 additions & 0 deletions tests/CppWinRTAuthoringTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,102 @@ TEST_CASE("CppWinRTAuthoringTests::EventsAndCppWinRt", "[property]")
test.Closed(token);
}

TEST_CASE("CppWinRTAuthoringTests::WinrtEvent", "[winrt_event]")
{
// Basic smoke test: register, invoke, unregister
struct Test
{
wil::winrt_event<winrt::Windows::Foundation::EventHandler<int>> MyEvent;
} test;

int value = 0;
auto token = test.MyEvent([&](const winrt::Windows::Foundation::IInspectable&, int args) {
value = args;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot This should have a test that verifies the "caught, not leaked" behavior. Be sure to verify the "no filter" and "some filter" and "all filter" behavior from above.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added comprehensive tests in 808b460:

  • WinrtEventSwallowExceptions: verifies exception is caught, not leaked (logged via TestFailureCache) and handlers are kept (no filter — all handlers invoked)
  • WinrtEventRpcResilienceRemovesBrokenHandler: verifies RPC errors remove the broken handler on next invoke (some filter — RPC errors removed)
  • WinrtEventRpcResilienceKeepsHandlerOnNonRpcError: verifies non-RPC errors keep the handler (some filter — only RPC filtered out)
  • WinrtEventCustomTraits: verifies custom traits can implement selective removal logic (custom filter with E_ABORT → remove, all others → swallow)

});
test.MyEvent.invoke(nullptr, 42);
REQUIRE(value == 42);
test.MyEvent(token);
}

TEST_CASE("CppWinRTAuthoringTests::WinrtEventSwallowExceptions", "[winrt_event]")
{
// Default (swallow) traits: all handlers are called even if some throw
struct Test
{
wil::winrt_event<winrt::Windows::Foundation::EventHandler<int>> MyEvent;
} test;

int callCount = 0;

// First handler throws
auto token1 = test.MyEvent([&](const winrt::Windows::Foundation::IInspectable&, int) {
++callCount;
throw winrt::hresult_not_implemented{};
});

// Second handler should still be called
auto token2 = test.MyEvent([&](const winrt::Windows::Foundation::IInspectable&, int args) {
++callCount;
REQUIRE(args == 42);
});

// invoke() must not throw even though first handler throws
REQUIRE_NOTHROW(test.MyEvent.invoke(nullptr, 42));

// Both handlers must have been called
REQUIRE(callCount == 2);

test.MyEvent(token1);
test.MyEvent(token2);
}

TEST_CASE("CppWinRTAuthoringTests::WinrtEventPropagateExceptions", "[winrt_event]")
{
// Propagate traits: first exception propagates and remaining handlers are skipped
struct Test
{
wil::winrt_event<winrt::Windows::Foundation::EventHandler<int>, wil::propagate_event_errors_traits> MyEvent;
} test;

int callCount = 0;

auto token1 = test.MyEvent([&](const winrt::Windows::Foundation::IInspectable&, int) {
++callCount;
throw winrt::hresult_not_implemented{};
});

// This handler should NOT be called because the first one throws
auto token2 = test.MyEvent([&](const winrt::Windows::Foundation::IInspectable&, int) {
++callCount;
});

// invoke() must throw
REQUIRE_THROWS_AS(test.MyEvent.invoke(nullptr, 42), winrt::hresult_not_implemented);

// Only the first handler ran
REQUIRE(callCount == 1);

test.MyEvent(token1);
test.MyEvent(token2);
}

TEST_CASE("CppWinRTAuthoringTests::WinrtEventTypedDelegate", "[winrt_event]")
{
// winrt_event works with TypedEventHandler delegates
struct Test
{
wil::winrt_event<winrt::Windows::Foundation::TypedEventHandler<winrt::Windows::Foundation::IInspectable, int>> MyEvent;
} test;

int value = 0;
auto token = test.MyEvent([&](const winrt::Windows::Foundation::IInspectable&, int args) {
value = args;
});
test.MyEvent.invoke(nullptr, 99);
REQUIRE(value == 99);
test.MyEvent(token);
}

#include <winrt/Windows.UI.Xaml.Hosting.h>

TEST_CASE("CppWinRTAuthoringTests::NotifyPropertyChanged", "[property]")
Expand Down