From daf1cf1bdbd41cbf06151498d63b8ff2820a3c0a Mon Sep 17 00:00:00 2001 From: "Chris Guzak (WINDOWS)" Date: Sun, 12 Apr 2026 14:55:21 -0700 Subject: [PATCH 1/3] Add get_only_safe_from_non_presenting_sta() to async operations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a peer to .get() on IAsyncAction, IAsyncOperation, IAsyncActionWithProgress, and IAsyncOperationWithProgress that skips the _DEBUG-only STA blocking assert. The existing .get() asserts !is_sta_thread() to guard against blocking UI threads. However, not all STAs are UI threads — some never present UI, haven't presented yet, or never will. The assert is also _DEBUG-only, making it invisible to codebases that don't build with _DEBUG (e.g. the Windows OS). The new method get_only_safe_from_non_presenting_sta() is functionally identical to .get() but omits the STA check. The intentionally long name communicates the risk to callers. Changes: - strings/base_coroutine_foundation.h: Add wait_get_bypass_sta_check() impl helper and get_only_safe_from_non_presenting_sta() for all 4 async consume templates - cppwinrt/code_writers.h: Add declaration to generated code for all 4 async types - test/test_nocoro: Add test calling the new method from an STA thread using a real WinRT async operation (PathIO::ReadTextAsync on C:\Windows\win.ini) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cppwinrt/code_writers.h | 4 ++++ strings/base_coroutine_foundation.h | 33 +++++++++++++++++++++++++++++ test/test_nocoro/get.cpp | 33 +++++++++++++++++++++++++++++ test/test_nocoro/pch.h | 1 + 4 files changed, 71 insertions(+) diff --git a/cppwinrt/code_writers.h b/cppwinrt/code_writers.h index b3855c097..d8800f212 100644 --- a/cppwinrt/code_writers.h +++ b/cppwinrt/code_writers.h @@ -1475,24 +1475,28 @@ namespace cppwinrt else if (type_name == "Windows.Foundation.IAsyncAction") { w.write(R"( auto get() const; + auto get_only_safe_from_non_presenting_sta() const; auto wait_for(Windows::Foundation::TimeSpan const& timeout) const; )"); } else if (type_name == "Windows.Foundation.IAsyncOperation`1") { w.write(R"( auto get() const; + auto get_only_safe_from_non_presenting_sta() const; auto wait_for(Windows::Foundation::TimeSpan const& timeout) const; )"); } else if (type_name == "Windows.Foundation.IAsyncActionWithProgress`1") { w.write(R"( auto get() const; + auto get_only_safe_from_non_presenting_sta() const; auto wait_for(Windows::Foundation::TimeSpan const& timeout) const; )"); } else if (type_name == "Windows.Foundation.IAsyncOperationWithProgress`2") { w.write(R"( auto get() const; + auto get_only_safe_from_non_presenting_sta() const; auto wait_for(Windows::Foundation::TimeSpan const& timeout) const; )"); } diff --git a/strings/base_coroutine_foundation.h b/strings/base_coroutine_foundation.h index 5cefad836..f9dbd2880 100644 --- a/strings/base_coroutine_foundation.h +++ b/strings/base_coroutine_foundation.h @@ -99,6 +99,19 @@ WINRT_EXPORT namespace winrt::impl return async.GetResults(); } + template + auto wait_get_bypass_sta_check(Async const& async) + { + auto status = async.Status(); + if (status == Windows::Foundation::AsyncStatus::Started) + { + status = wait_for_completed(async, 0xFFFFFFFF); // INFINITE + } + check_status_canceled(status); + + return async.GetResults(); + } + #ifdef WINRT_IMPL_COROUTINES struct ignore_apartment_context {}; @@ -220,6 +233,11 @@ WINRT_EXPORT namespace winrt::impl impl::wait_get(static_cast(static_cast(*this))); } template + auto consume_Windows_Foundation_IAsyncAction::get_only_safe_from_non_presenting_sta() const + { + impl::wait_get_bypass_sta_check(static_cast(static_cast(*this))); + } + template auto consume_Windows_Foundation_IAsyncAction::wait_for(Windows::Foundation::TimeSpan const& timeout) const { return impl::wait_for(static_cast(static_cast(*this)), timeout); @@ -231,6 +249,11 @@ WINRT_EXPORT namespace winrt::impl return impl::wait_get(static_cast const&>(static_cast(*this))); } template + auto consume_Windows_Foundation_IAsyncOperation::get_only_safe_from_non_presenting_sta() const + { + return impl::wait_get_bypass_sta_check(static_cast const&>(static_cast(*this))); + } + template auto consume_Windows_Foundation_IAsyncOperation::wait_for(Windows::Foundation::TimeSpan const& timeout) const { return impl::wait_for(static_cast const&>(static_cast(*this)), timeout); @@ -242,6 +265,11 @@ WINRT_EXPORT namespace winrt::impl impl::wait_get(static_cast const&>(static_cast(*this))); } template + auto consume_Windows_Foundation_IAsyncActionWithProgress::get_only_safe_from_non_presenting_sta() const + { + impl::wait_get_bypass_sta_check(static_cast const&>(static_cast(*this))); + } + template auto consume_Windows_Foundation_IAsyncActionWithProgress::wait_for(Windows::Foundation::TimeSpan const& timeout) const { return impl::wait_for(static_cast const&>(static_cast(*this)), timeout); @@ -253,6 +281,11 @@ WINRT_EXPORT namespace winrt::impl return impl::wait_get(static_cast const&>(static_cast(*this))); } template + auto consume_Windows_Foundation_IAsyncOperationWithProgress::get_only_safe_from_non_presenting_sta() const + { + return impl::wait_get_bypass_sta_check(static_cast const&>(static_cast(*this))); + } + template auto consume_Windows_Foundation_IAsyncOperationWithProgress::wait_for(Windows::Foundation::TimeSpan const& timeout) const { return impl::wait_for(static_cast const&>(static_cast(*this)), timeout); diff --git a/test/test_nocoro/get.cpp b/test/test_nocoro/get.cpp index 11339e10b..5fa558e6a 100644 --- a/test/test_nocoro/get.cpp +++ b/test/test_nocoro/get.cpp @@ -2,6 +2,7 @@ using namespace winrt; using namespace Windows::Foundation; +using namespace Windows::Storage; template struct async_completion_source : implements, IAsyncOperation, IAsyncInfo> @@ -72,3 +73,35 @@ TEST_CASE("get") REQUIRE(acs.as>().get() == 0xDEADBEEF); } + +TEST_CASE("get_only_safe_from_non_presenting_sta") +{ + // Call a real WinRT async operation from an STA thread. + // This is the scenario the new API is designed for: an STA that is not + // presenting UI, where a synchronous blocking wait is safe. + std::exception_ptr failure{}; + std::thread sta_thread([&failure] + { + try + { + winrt::init_apartment(winrt::apartment_type::single_threaded); + + auto content = PathIO::ReadTextAsync(L"C:\\Windows\\win.ini").get_only_safe_from_non_presenting_sta(); + + REQUIRE(content.size() > 0); + + winrt::uninit_apartment(); + } + catch (...) + { + failure = std::current_exception(); + } + }); + + sta_thread.join(); + + if (failure) + { + std::rethrow_exception(failure); + } +} diff --git a/test/test_nocoro/pch.h b/test/test_nocoro/pch.h index 7ff48a37c..30a6d3079 100644 --- a/test/test_nocoro/pch.h +++ b/test/test_nocoro/pch.h @@ -2,5 +2,6 @@ #include "catch.hpp" #include "winrt/Windows.Foundation.h" +#include "winrt/Windows.Storage.h" using namespace std::literals; From 090a37f1feaf3f727b2f23b0c2d30c3e9a0a9c34 Mon Sep 17 00:00:00 2001 From: "Chris Guzak (WINDOWS)" Date: Tue, 25 Aug 2026 22:45:51 -0700 Subject: [PATCH 2/3] Rename STA bypassing async get method Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cppwinrt/code_writers.h | 16 ++++++++++++---- strings/base_coroutine_foundation.h | 8 ++++---- test/test_nocoro/get.cpp | 4 ++-- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/cppwinrt/code_writers.h b/cppwinrt/code_writers.h index d8800f212..a9aa5bc78 100644 --- a/cppwinrt/code_writers.h +++ b/cppwinrt/code_writers.h @@ -1475,28 +1475,36 @@ namespace cppwinrt else if (type_name == "Windows.Foundation.IAsyncAction") { w.write(R"( auto get() const; - auto get_only_safe_from_non_presenting_sta() const; + // Synchronously waits without asserting that the calling thread is not an STA. + // Use only when the STA is not presenting UI and blocking is known to be safe. + auto get_unchecked() const; auto wait_for(Windows::Foundation::TimeSpan const& timeout) const; )"); } else if (type_name == "Windows.Foundation.IAsyncOperation`1") { w.write(R"( auto get() const; - auto get_only_safe_from_non_presenting_sta() const; + // Synchronously waits without asserting that the calling thread is not an STA. + // Use only when the STA is not presenting UI and blocking is known to be safe. + auto get_unchecked() const; auto wait_for(Windows::Foundation::TimeSpan const& timeout) const; )"); } else if (type_name == "Windows.Foundation.IAsyncActionWithProgress`1") { w.write(R"( auto get() const; - auto get_only_safe_from_non_presenting_sta() const; + // Synchronously waits without asserting that the calling thread is not an STA. + // Use only when the STA is not presenting UI and blocking is known to be safe. + auto get_unchecked() const; auto wait_for(Windows::Foundation::TimeSpan const& timeout) const; )"); } else if (type_name == "Windows.Foundation.IAsyncOperationWithProgress`2") { w.write(R"( auto get() const; - auto get_only_safe_from_non_presenting_sta() const; + // Synchronously waits without asserting that the calling thread is not an STA. + // Use only when the STA is not presenting UI and blocking is known to be safe. + auto get_unchecked() const; auto wait_for(Windows::Foundation::TimeSpan const& timeout) const; )"); } diff --git a/strings/base_coroutine_foundation.h b/strings/base_coroutine_foundation.h index f9dbd2880..82582ee64 100644 --- a/strings/base_coroutine_foundation.h +++ b/strings/base_coroutine_foundation.h @@ -233,7 +233,7 @@ WINRT_EXPORT namespace winrt::impl impl::wait_get(static_cast(static_cast(*this))); } template - auto consume_Windows_Foundation_IAsyncAction::get_only_safe_from_non_presenting_sta() const + auto consume_Windows_Foundation_IAsyncAction::get_unchecked() const { impl::wait_get_bypass_sta_check(static_cast(static_cast(*this))); } @@ -249,7 +249,7 @@ WINRT_EXPORT namespace winrt::impl return impl::wait_get(static_cast const&>(static_cast(*this))); } template - auto consume_Windows_Foundation_IAsyncOperation::get_only_safe_from_non_presenting_sta() const + auto consume_Windows_Foundation_IAsyncOperation::get_unchecked() const { return impl::wait_get_bypass_sta_check(static_cast const&>(static_cast(*this))); } @@ -265,7 +265,7 @@ WINRT_EXPORT namespace winrt::impl impl::wait_get(static_cast const&>(static_cast(*this))); } template - auto consume_Windows_Foundation_IAsyncActionWithProgress::get_only_safe_from_non_presenting_sta() const + auto consume_Windows_Foundation_IAsyncActionWithProgress::get_unchecked() const { impl::wait_get_bypass_sta_check(static_cast const&>(static_cast(*this))); } @@ -281,7 +281,7 @@ WINRT_EXPORT namespace winrt::impl return impl::wait_get(static_cast const&>(static_cast(*this))); } template - auto consume_Windows_Foundation_IAsyncOperationWithProgress::get_only_safe_from_non_presenting_sta() const + auto consume_Windows_Foundation_IAsyncOperationWithProgress::get_unchecked() const { return impl::wait_get_bypass_sta_check(static_cast const&>(static_cast(*this))); } diff --git a/test/test_nocoro/get.cpp b/test/test_nocoro/get.cpp index 5fa558e6a..baf3c5122 100644 --- a/test/test_nocoro/get.cpp +++ b/test/test_nocoro/get.cpp @@ -74,7 +74,7 @@ TEST_CASE("get") REQUIRE(acs.as>().get() == 0xDEADBEEF); } -TEST_CASE("get_only_safe_from_non_presenting_sta") +TEST_CASE("get_unchecked") { // Call a real WinRT async operation from an STA thread. // This is the scenario the new API is designed for: an STA that is not @@ -86,7 +86,7 @@ TEST_CASE("get_only_safe_from_non_presenting_sta") { winrt::init_apartment(winrt::apartment_type::single_threaded); - auto content = PathIO::ReadTextAsync(L"C:\\Windows\\win.ini").get_only_safe_from_non_presenting_sta(); + auto content = PathIO::ReadTextAsync(L"C:\\Windows\\win.ini").get_unchecked(); REQUIRE(content.size() > 0); From e73140a3fa58e048ea1a2795986ab0ec2e2c4ed2 Mon Sep 17 00:00:00 2001 From: "Chris Guzak (WINDOWS)" Date: Wed, 26 Aug 2026 13:06:59 -0700 Subject: [PATCH 3/3] Fix STA test cleanup and assertions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/test_nocoro/get.cpp | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/test/test_nocoro/get.cpp b/test/test_nocoro/get.cpp index baf3c5122..4fd85fe3e 100644 --- a/test/test_nocoro/get.cpp +++ b/test/test_nocoro/get.cpp @@ -80,17 +80,22 @@ TEST_CASE("get_unchecked") // This is the scenario the new API is designed for: an STA that is not // presenting UI, where a synchronous blocking wait is safe. std::exception_ptr failure{}; - std::thread sta_thread([&failure] + bool content_available = false; + std::thread sta_thread([&failure, &content_available] { try { winrt::init_apartment(winrt::apartment_type::single_threaded); + struct apartment_guard + { + ~apartment_guard() + { + winrt::uninit_apartment(); + } + } guard; auto content = PathIO::ReadTextAsync(L"C:\\Windows\\win.ini").get_unchecked(); - - REQUIRE(content.size() > 0); - - winrt::uninit_apartment(); + content_available = content.size() > 0; } catch (...) { @@ -104,4 +109,6 @@ TEST_CASE("get_unchecked") { std::rethrow_exception(failure); } + + REQUIRE(content_available); }