From 4320c3f3a6b86c7e8f471f491e659660ddcf4dad Mon Sep 17 00:00:00 2001 From: David Lechner Date: Thu, 1 Oct 2026 10:12:02 -0500 Subject: [PATCH 1/3] TestComponent: add AsyncActionSource and AsyncOperationSource These hand out an async operation whose cancellation, completion and completed handler the caller drives, and which reports whether it is still alive. Cancel() on the operation only counts the request, as an operation that takes time to stop does. This allows testing how a projection waits for an operation, propagates cancellation and lets go of the operation without relying on timing. --- TestComponent/AsyncActionSource.cpp | 11 + TestComponent/AsyncActionSource.h | 21 ++ TestComponent/AsyncOperationSource.cpp | 11 + TestComponent/AsyncOperationSource.h | 21 ++ TestComponent/ControlledAsync.h | 276 ++++++++++++++++++++ TestComponent/TestComponent.idl | 55 ++++ TestComponent/TestComponent.vcxproj | 5 + TestComponent/TestComponent.vcxproj.filters | 5 + 8 files changed, 405 insertions(+) create mode 100644 TestComponent/AsyncActionSource.cpp create mode 100644 TestComponent/AsyncActionSource.h create mode 100644 TestComponent/AsyncOperationSource.cpp create mode 100644 TestComponent/AsyncOperationSource.h create mode 100644 TestComponent/ControlledAsync.h diff --git a/TestComponent/AsyncActionSource.cpp b/TestComponent/AsyncActionSource.cpp new file mode 100644 index 0000000..4c118e1 --- /dev/null +++ b/TestComponent/AsyncActionSource.cpp @@ -0,0 +1,11 @@ +#include "pch.h" +#include "AsyncActionSource.h" +#include "AsyncActionSource.g.cpp" + +namespace winrt::TestComponent::implementation +{ + void AsyncActionSource::Complete() + { + finish(Windows::Foundation::AsyncStatus::Completed, {}, 0); + } +} diff --git a/TestComponent/AsyncActionSource.h b/TestComponent/AsyncActionSource.h new file mode 100644 index 0000000..007ffd6 --- /dev/null +++ b/TestComponent/AsyncActionSource.h @@ -0,0 +1,21 @@ +#pragma once +#include "AsyncActionSource.g.h" +#include "ControlledAsync.h" + +namespace winrt::TestComponent::implementation +{ + struct AsyncActionSource : AsyncActionSourceT, + controlled_async_source + { + AsyncActionSource() = default; + + void Complete(); + }; +} + +namespace winrt::TestComponent::factory_implementation +{ + struct AsyncActionSource : AsyncActionSourceT + { + }; +} diff --git a/TestComponent/AsyncOperationSource.cpp b/TestComponent/AsyncOperationSource.cpp new file mode 100644 index 0000000..bfb973f --- /dev/null +++ b/TestComponent/AsyncOperationSource.cpp @@ -0,0 +1,11 @@ +#include "pch.h" +#include "AsyncOperationSource.h" +#include "AsyncOperationSource.g.cpp" + +namespace winrt::TestComponent::implementation +{ + void AsyncOperationSource::Complete(int32_t result) + { + finish(Windows::Foundation::AsyncStatus::Completed, {}, result); + } +} diff --git a/TestComponent/AsyncOperationSource.h b/TestComponent/AsyncOperationSource.h new file mode 100644 index 0000000..5effca9 --- /dev/null +++ b/TestComponent/AsyncOperationSource.h @@ -0,0 +1,21 @@ +#pragma once +#include "AsyncOperationSource.g.h" +#include "ControlledAsync.h" + +namespace winrt::TestComponent::implementation +{ + struct AsyncOperationSource : AsyncOperationSourceT, + controlled_async_source, Windows::Foundation::AsyncOperationCompletedHandler> + { + AsyncOperationSource() = default; + + void Complete(int32_t result); + }; +} + +namespace winrt::TestComponent::factory_implementation +{ + struct AsyncOperationSource : AsyncOperationSourceT + { + }; +} diff --git a/TestComponent/ControlledAsync.h b/TestComponent/ControlledAsync.h new file mode 100644 index 0000000..ed93b46 --- /dev/null +++ b/TestComponent/ControlledAsync.h @@ -0,0 +1,276 @@ +#pragma once + +#include +#include + +// An async operation that finishes only when its source says so, and the +// source that says so. AsyncActionSource and AsyncOperationSource are the two +// instances of it that TestComponent exposes. + +namespace winrt::TestComponent::implementation +{ + // What an operation and its source both read and write. + struct controlled_async_state + { + slim_mutex lock; + Windows::Foundation::AsyncStatus status{ Windows::Foundation::AsyncStatus::Started }; + hresult error{}; + int32_t result{}; + uint32_t cancel_requests{}; + bool defers_handler{}; + bool handler_released{}; + }; + + // An IAsyncAction or IAsyncOperation that keeps its completed + // handler for as long as it exists, as C++/WinRT's coroutines do. + template + struct controlled_async : implements, Async, Windows::Foundation::IAsyncInfo> + { + explicit controlled_async(std::shared_ptr state) : + m_state(std::move(state)) + { + } + + uint32_t Id() const noexcept + { + return 1; + } + + Windows::Foundation::AsyncStatus Status() + { + slim_lock_guard const guard{ m_state->lock }; + return m_state->status; + } + + hresult ErrorCode() + { + slim_lock_guard const guard{ m_state->lock }; + + switch (m_state->status) + { + case Windows::Foundation::AsyncStatus::Error: + return m_state->error; + case Windows::Foundation::AsyncStatus::Canceled: + return impl::error_canceled; + default: + return {}; + } + } + + // A request, which the operation answers when its source finishes it. + void Cancel() + { + slim_lock_guard const guard{ m_state->lock }; + m_state->cancel_requests++; + } + + void Close() + { + slim_lock_guard const guard{ m_state->lock }; + + if (m_state->status == Windows::Foundation::AsyncStatus::Started) + { + throw hresult_illegal_state_change(); + } + } + + void Completed(Handler const& handler) + { + { + slim_lock_guard const guard{ m_state->lock }; + + if (m_completed) + { + throw hresult_illegal_delegate_assignment(); + } + + m_completed = handler; + } + + // An operation that has already finished calls its handler at once. + invoke_completed(); + } + + Handler Completed() + { + slim_lock_guard const guard{ m_state->lock }; + return m_completed; + } + + auto GetResults() + { + slim_lock_guard const guard{ m_state->lock }; + + switch (m_state->status) + { + case Windows::Foundation::AsyncStatus::Error: + throw hresult_error(m_state->error); + case Windows::Foundation::AsyncStatus::Canceled: + throw hresult_canceled(); + case Windows::Foundation::AsyncStatus::Started: + throw hresult_illegal_method_call(); + default: + break; + } + + if constexpr (!std::is_same_v) + { + return m_state->result; + } + } + + bool has_completed_handler() + { + slim_lock_guard const guard{ m_state->lock }; + return static_cast(m_completed); + } + + // Calls the completed handler once the operation has finished, it has + // a handler and the source is not holding the call back, and only once. + void invoke_completed() + { + Handler handler; + Windows::Foundation::AsyncStatus status{}; + + { + slim_lock_guard const guard{ m_state->lock }; + + if (m_invoked || !m_completed) + { + return; + } + + if (m_state->status == Windows::Foundation::AsyncStatus::Started) + { + return; + } + + if (m_state->defers_handler && !m_state->handler_released) + { + return; + } + + m_invoked = true; + handler = m_completed; + status = m_state->status; + } + + handler(*this, status); + } + + private: + std::shared_ptr m_state; + Handler m_completed; + bool m_invoked{}; + }; + + // The members AsyncActionSource and AsyncOperationSource share. The source + // holds the operation only weakly, so that a test can see when the + // consumer has let go of it. + template + struct controlled_async_source + { + using operation_type = controlled_async; + + Async Operation() + { + slim_lock_guard const guard{ m_lock }; + + if (auto operation = m_operation.get()) + { + return operation.template as(); + } + + if (m_made) + { + throw hresult_illegal_method_call(L"the operation has been released"); + } + + auto operation = make_self(m_state); + m_operation = operation->get_weak(); + m_made = true; + + return operation.template as(); + } + + bool IsOperationAlive() + { + return static_cast(m_operation.get()); + } + + uint32_t CancelRequestCount() + { + slim_lock_guard const guard{ m_state->lock }; + return m_state->cancel_requests; + } + + bool HasCompletedHandler() + { + auto operation = m_operation.get(); + return operation && operation->has_completed_handler(); + } + + bool DefersCompletedHandler() + { + slim_lock_guard const guard{ m_state->lock }; + return m_state->defers_handler; + } + + void DefersCompletedHandler(bool value) + { + slim_lock_guard const guard{ m_state->lock }; + m_state->defers_handler = value; + } + + void Fail(int32_t error) + { + finish(Windows::Foundation::AsyncStatus::Error, hresult(error), 0); + } + + void Cancel() + { + finish(Windows::Foundation::AsyncStatus::Canceled, {}, 0); + } + + void InvokeCompletedHandler() + { + { + slim_lock_guard const guard{ m_state->lock }; + m_state->handler_released = true; + } + + if (auto operation = m_operation.get()) + { + operation->invoke_completed(); + } + } + + protected: + void finish(Windows::Foundation::AsyncStatus status, hresult error, int32_t result) + { + { + slim_lock_guard const guard{ m_state->lock }; + + if (m_state->status != Windows::Foundation::AsyncStatus::Started) + { + throw hresult_illegal_state_change(); + } + + m_state->status = status; + m_state->error = error; + m_state->result = result; + } + + if (auto operation = m_operation.get()) + { + operation->invoke_completed(); + } + } + + private: + slim_mutex m_lock; + std::shared_ptr m_state{ std::make_shared() }; + weak_ref m_operation; + bool m_made{}; + }; +} diff --git a/TestComponent/TestComponent.idl b/TestComponent/TestComponent.idl index 1de2180..e118866 100644 --- a/TestComponent/TestComponent.idl +++ b/TestComponent/TestComponent.idl @@ -359,4 +359,59 @@ namespace TestComponent overridable void OnOverridable(); event Windows.Foundation.EventHandler OverridableCalled; } + + // An async action whose every step the caller drives, for testing how a + // projection waits for an operation, cancels it and lets go of it. + // + // Cancel() on the operation only counts the request, as an operation that + // takes time to stop does. The operation finishes when the caller calls + // Complete(), Fail() or Cancel() here. + runtimeclass AsyncActionSource + { + AsyncActionSource(); + + // The operation, made the first time it is read. The source does not + // keep it alive. + Windows.Foundation.IAsyncAction Operation{ get; }; + + // Whether the operation still exists, which it does until every + // reference to it has been released. + Boolean IsOperationAlive{ get; }; + + // How many times Cancel() has been called on the operation. + UInt32 CancelRequestCount{ get; }; + + // Whether a completed handler has been set on the operation. + Boolean HasCompletedHandler{ get; }; + + // When true, finishing the operation changes its status but leaves the + // completed handler uncalled until InvokeCompletedHandler(). + Boolean DefersCompletedHandler{ get; set; }; + + // Finishes the operation as Completed, Error or Canceled. Canceled is + // what the operation does whether or not Cancel() was called on it. + void Complete(); + void Fail(Int32 error); + void Cancel(); + + // Calls the completed handler that DefersCompletedHandler held back. + void InvokeCompletedHandler(); + } + + // AsyncActionSource for an IAsyncOperation. + runtimeclass AsyncOperationSource + { + AsyncOperationSource(); + + Windows.Foundation.IAsyncOperation Operation{ get; }; + Boolean IsOperationAlive{ get; }; + UInt32 CancelRequestCount{ get; }; + Boolean HasCompletedHandler{ get; }; + Boolean DefersCompletedHandler{ get; set; }; + + void Complete(Int32 result); + void Fail(Int32 error); + void Cancel(); + void InvokeCompletedHandler(); + } } diff --git a/TestComponent/TestComponent.vcxproj b/TestComponent/TestComponent.vcxproj index 2fc3ca6..2008b88 100644 --- a/TestComponent/TestComponent.vcxproj +++ b/TestComponent/TestComponent.vcxproj @@ -147,6 +147,9 @@ + + + @@ -156,6 +159,8 @@ + + diff --git a/TestComponent/TestComponent.vcxproj.filters b/TestComponent/TestComponent.vcxproj.filters index 008d317..9cbe815 100644 --- a/TestComponent/TestComponent.vcxproj.filters +++ b/TestComponent/TestComponent.vcxproj.filters @@ -18,6 +18,8 @@ + + @@ -27,6 +29,9 @@ + + + From 1198328b5b9807f3771799e3e6ede1bb4cd8120e Mon Sep 17 00:00:00 2001 From: David Lechner Date: Thu, 1 Oct 2026 10:12:02 -0500 Subject: [PATCH 2/3] v1.4.0 --- TestComponent/TestComponent.nuspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TestComponent/TestComponent.nuspec b/TestComponent/TestComponent.nuspec index e226573..ee54a53 100644 --- a/TestComponent/TestComponent.nuspec +++ b/TestComponent/TestComponent.nuspec @@ -2,7 +2,7 @@ PyWinRT.TestWinRT - 1.3.0 + 1.4.0 TestWinRT Microsoft MIT From b06c9a80ca7ebf543d1b5d4b69241f4add09730e Mon Sep 17 00:00:00 2001 From: David Lechner Date: Thu, 1 Oct 2026 10:41:54 -0500 Subject: [PATCH 3/3] ci: build on windows-2022 windows-latest is now the windows-2025-vs2026 image, which does not have the Windows SDK 10.0.22621.0 that the projects target, and whose MSVC STL refuses the /await coroutines they use. windows-2022 is the image that windows-latest was when the last release was built. --- .github/workflows/build.yml | 2 +- .github/workflows/publish.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4f57972..be71304 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,7 +7,7 @@ on: jobs: build: - runs-on: windows-latest + runs-on: windows-2022 steps: - name: checkout project uses: actions/checkout@v4 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 13f1163..feea3b1 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -7,7 +7,7 @@ on: jobs: build: - runs-on: windows-latest + runs-on: windows-2022 steps: - name: checkout project uses: actions/checkout@v4