From 2eab28ae088b66a77a571535bfe5f1a3a62e2976 Mon Sep 17 00:00:00 2001 From: Jon Wiswall Date: Thu, 16 Jul 2026 15:13:03 -0700 Subject: [PATCH 01/12] Add _hs literal for compile-time fast-pass HSTRING Passing a wide string literal to a WinRT API that takes an hstring runs wcslen and fills a seven-field HSTRING_HEADER on the stack on every call (via param::hstring -> create_hstring_on_stack). For a literal, all of that is knowable at compile time and only needs to happen once. Add a `_hs` user-defined literal that builds the fast-pass reference header as a constexpr static, so `L"value"_hs` reduces to a single pointer load at the call site with no per-call wcslen or header fill. The literal-operator-template is keyed on the characters themselves via a C++20 non-type template parameter (hstring_literal_storage), so each distinct literal gets its own static header with static lifetime. It returns a non-owning winrt::hstring_reference (a plain winrt::hstring would assert/free the reference header in its destructor). A matching param::hstring constructor lets the result bind to projected setters in a single user-defined conversion. A bare `param::hstring(wchar_t const(&)[N])` overload is deliberately not added: it would also bind non-literal arrays (e.g. a partially-filled wchar_t buf[260]) and infer length N-1 past the null, silently corrupting; and a runtime constructor cannot produce a content-specific static anyway. `_hs` is the explicit, safe opt-in; bare literal calls keep their unchanged wcslen path. Gated on __cpp_nontype_template_args >= 201911L. Tested under C++20 in test_cpp20/hstring_literal.cpp; the non-gated types build under C++17. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 83914e59-f7cd-4284-ad4b-4cb7b79f28c1 --- strings/base_string.h | 74 +++++++++++++++++++++++++++++ strings/base_string_input.h | 4 ++ test/test_cpp20/hstring_literal.cpp | 58 ++++++++++++++++++++++ test/test_cpp20/test_cpp20.vcxproj | 1 + 4 files changed, 137 insertions(+) create mode 100644 test/test_cpp20/hstring_literal.cpp diff --git a/strings/base_string.h b/strings/base_string.h index 6b1fb37b5..4daac531c 100644 --- a/strings/base_string.h +++ b/strings/base_string.h @@ -155,6 +155,24 @@ WINRT_EXPORT namespace winrt::impl return nullptr; } }; + + template + struct hstring_literal_storage + { + static constexpr std::size_t size = N; + wchar_t value[N]; + + constexpr hstring_literal_storage(wchar_t const (&str)[N]) noexcept + { + for (std::size_t i = 0; i != N; ++i) + { + value[i] = str[i]; + } + } + }; + + template + hstring_literal_storage(wchar_t const (&)[N]) -> hstring_literal_storage; } WINRT_EXPORT namespace winrt @@ -386,6 +404,30 @@ WINRT_EXPORT namespace winrt handle_type m_handle; }; + struct hstring_reference + { + hstring_reference() noexcept = default; + + explicit hstring_reference(impl::hstring_header const* header) noexcept : + m_handle(const_cast(header)) + { + } + + operator hstring const&() const noexcept + { + return *reinterpret_cast(this); + } + + private: + + void* m_handle{}; + }; + + inline void* get_abi(hstring_reference const& object) noexcept + { + return *(void**)(&object); + } + inline void* get_abi(hstring const& object) noexcept { return *(void**)(&object); @@ -437,6 +479,38 @@ WINRT_EXPORT namespace winrt } } +#if defined(__cpp_nontype_template_args) && __cpp_nontype_template_args >= 201911L + +WINRT_EXPORT namespace winrt +{ + inline namespace literals + { + template + hstring_reference operator ""_hs() noexcept + { + if constexpr (Literal.size <= 1) + { + return hstring_reference{}; + } + else + { + static constexpr impl::hstring_header header + { + impl::hstring_reference_flag, + static_cast(Literal.size - 1), + 0, + 0, + Literal.value + }; + + return hstring_reference{ &header }; + } + } + } +} + +#endif + #ifdef __cpp_lib_format template<> struct std::formatter : std::formatter {}; diff --git a/strings/base_string_input.h b/strings/base_string_input.h index 71cd5f3c0..ccf955547 100644 --- a/strings/base_string_input.h +++ b/strings/base_string_input.h @@ -18,6 +18,10 @@ WINRT_EXPORT namespace winrt::param { } + hstring(winrt::hstring_reference const& value) noexcept : m_handle(get_abi(value)) + { + } + hstring(std::wstring_view const& value) noexcept { create_string_reference(value.data(), value.size()); diff --git a/test/test_cpp20/hstring_literal.cpp b/test/test_cpp20/hstring_literal.cpp new file mode 100644 index 000000000..12a501307 --- /dev/null +++ b/test/test_cpp20/hstring_literal.cpp @@ -0,0 +1,58 @@ +#include "pch.h" + +using namespace winrt; +using namespace winrt::literals; +using namespace std::literals; + +#if defined(__cpp_nontype_template_args) && __cpp_nontype_template_args >= 201911L + +namespace +{ + // Exercises the hstring_reference -> param::hstring conversion that projected + // setters rely on, and duplicates into an owning hstring on the way out. + winrt::hstring copy_via_param(winrt::param::hstring const& value) + { + winrt::hstring const& as_hstring = value; + return as_hstring; + } +} + +TEST_CASE("hstring_literal") +{ + // Content and length match the literal. + { + winrt::hstring_reference const lit = L"kittens"_hs; + winrt::hstring const& value = lit; + REQUIRE(value == L"kittens"sv); + REQUIRE(value.size() == 7); + REQUIRE(wcslen(value.c_str()) == 7); + } + + // Built as a fast-pass reference string (no heap allocation). + { + winrt::hstring_reference const lit = L"puppies"_hs; + auto const header = static_cast(winrt::get_abi(lit)); + REQUIRE(header != nullptr); + REQUIRE((header->flags & winrt::impl::hstring_reference_flag) != 0); + REQUIRE(header->length == 7); + } + + // Empty literal projects as the empty (null) HSTRING. + { + winrt::hstring_reference const lit = L""_hs; + winrt::hstring const& value = lit; + REQUIRE(value.empty()); + REQUIRE(value.size() == 0); + REQUIRE(winrt::get_abi(value) == nullptr); + } + + // Binds to a projected setter parameter in a single conversion, and copying + // into an owning hstring duplicates correctly. + { + winrt::hstring const copied = copy_via_param(L"waffles"_hs); + REQUIRE(copied == L"waffles"sv); + REQUIRE(copied.size() == 7); + } +} + +#endif diff --git a/test/test_cpp20/test_cpp20.vcxproj b/test/test_cpp20/test_cpp20.vcxproj index 4eaee0a18..b13becd23 100644 --- a/test/test_cpp20/test_cpp20.vcxproj +++ b/test/test_cpp20/test_cpp20.vcxproj @@ -239,6 +239,7 @@ + NotUsing From a889be09d8166917ccff6494a4805e5020e37744 Mon Sep 17 00:00:00 2001 From: Jon Wiswall Date: Thu, 16 Jul 2026 15:36:55 -0700 Subject: [PATCH 02/12] Add make_ready for pre-completed async operations Returning an already-available value through an async-typed API (a cache hit, a fast path, or an API async-typed only for interface uniformity) still pays the full coroutine cost when written as `co_return value`: a heap-allocated coroutine frame plus a promise that is a complete COM object implementing IAsyncOperation and IAsyncInfo, carrying a slim_mutex, an agile completed-handler slot, an atomic status, and cancel machinery, all built and torn down on every call. Add winrt::make_ready(value) and winrt::make_ready() that return a minimal already-completed IAsyncOperation / IAsyncAction: it holds just the value with a fixed Completed status and no coroutine frame, no slim_mutex, no handler slot, and no cancel machinery. Setting a Completed handler on it invokes immediately, and Status()/GetResults() satisfy both the .get() and co_await consumer paths, so it is a drop-in for the synchronous-result case. `co_return` remains correct for genuinely suspending work, where the frame is doing real work and the overhead is amortized to noise. The one-shot Completed assignment is guarded with a lock-free atomic flag rather than a lock. Tested in test/make_ready.cpp. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 83914e59-f7cd-4284-ad4b-4cb7b79f28c1 --- strings/base_coroutine_foundation.h | 91 +++++++++++++++++++++++++++++ test/test/make_ready.cpp | 62 ++++++++++++++++++++ test/test/test.vcxproj | 1 + 3 files changed, 154 insertions(+) create mode 100644 test/test/make_ready.cpp diff --git a/strings/base_coroutine_foundation.h b/strings/base_coroutine_foundation.h index 5cefad836..7f381d9e8 100644 --- a/strings/base_coroutine_foundation.h +++ b/strings/base_coroutine_foundation.h @@ -837,6 +837,86 @@ WINRT_IMPL_STD_EXPORT namespace std }; } +WINRT_EXPORT namespace winrt::impl +{ + template + struct ready_async_base : implements + { + using AsyncStatus = Windows::Foundation::AsyncStatus; + + void Completed(async_completed_handler_t const& handler) + { + if (m_completed_assigned.exchange(true)) + { + throw hresult_illegal_delegate_assignment(); + } + + if (handler) + { + winrt::impl::invoke(handler, *static_cast(this), AsyncStatus::Completed); + } + } + + auto Completed() noexcept + { + return async_completed_handler_t{ nullptr }; + } + + std::uint32_t Id() const noexcept + { + return 1; + } + + AsyncStatus Status() const noexcept + { + return AsyncStatus::Completed; + } + + hresult ErrorCode() const noexcept + { + return 0; + } + + void Cancel() const noexcept + { + } + + void Close() const noexcept + { + } + + private: + + std::atomic m_completed_assigned{ false }; + }; + + template + struct ready_async_operation : + ready_async_base, Windows::Foundation::IAsyncOperation> + { + explicit ready_async_operation(TResult value) : m_result(std::move(value)) + { + } + + TResult GetResults() + { + return m_result; + } + + private: + + TResult m_result; + }; + + struct ready_async_action : + ready_async_base + { + void GetResults() const noexcept + { + } + }; +} + WINRT_EXPORT namespace winrt { template @@ -884,5 +964,16 @@ WINRT_EXPORT namespace winrt impl::check_status_canceled(shared->status); co_return shared->result.GetResults(); } + + template + Windows::Foundation::IAsyncOperation make_ready(TResult value) + { + return make>(std::move(value)); + } + + inline Windows::Foundation::IAsyncAction make_ready() noexcept + { + return make(); + } } #endif diff --git a/test/test/make_ready.cpp b/test/test/make_ready.cpp new file mode 100644 index 000000000..37c10080f --- /dev/null +++ b/test/test/make_ready.cpp @@ -0,0 +1,62 @@ +#include "pch.h" + +using namespace winrt; +using namespace Windows::Foundation; + +TEST_CASE("make_ready") +{ + // Completed synchronously with a value, with no coroutine frame. + { + IAsyncOperation op = make_ready(42); + REQUIRE(op.Status() == AsyncStatus::Completed); + REQUIRE(op.ErrorCode() == 0); + REQUIRE(op.GetResults() == 42); + REQUIRE(op.get() == 42); + } + + // co_await yields the value through the synchronous-completion path. + { + auto coro = []() -> IAsyncOperation + { + co_return co_await make_ready(7); + }; + + REQUIRE(coro().get() == 7); + } + + // A Completed handler on an already-completed operation fires immediately. + { + auto op = make_ready(5); + int32_t observed = 0; + AsyncStatus observed_status = AsyncStatus::Started; + + op.Completed([&](IAsyncOperation const& sender, AsyncStatus status) + { + observed = sender.GetResults(); + observed_status = status; + }); + + REQUIRE(observed == 5); + REQUIRE(observed_status == AsyncStatus::Completed); + } + + // Assigning Completed twice is illegal, matching the coroutine promise. + { + auto op = make_ready(1); + op.Completed([](auto&&, auto&&) {}); + REQUIRE_THROWS_AS(op.Completed([](auto&&, auto&&) {}), hresult_illegal_delegate_assignment); + } + + // Action variant carries no result. + { + IAsyncAction action = make_ready(); + REQUIRE(action.Status() == AsyncStatus::Completed); + action.get(); + } + + // A non-trivial result type round-trips. + { + auto op = make_ready(hstring{ L"ready" }); + REQUIRE(op.get() == L"ready"); + } +} diff --git a/test/test/test.vcxproj b/test/test/test.vcxproj index 43928dab2..f87a65a13 100644 --- a/test/test/test.vcxproj +++ b/test/test/test.vcxproj @@ -324,6 +324,7 @@ NotUsing + From 390e118ab5d14feddac33140bd86ba3bdc635f8e Mon Sep 17 00:00:00 2001 From: Jon Wiswall Date: Thu, 16 Jul 2026 15:48:53 -0700 Subject: [PATCH 03/12] Buffer range-for iteration over non-GetAt collections Range-for over a collection that lacks GetAt (a plain IIterable, or a map yielding IKeyValuePair) drove the projected IIterator one element per ABI crossing via Current/MoveNext. For a large sequence that is one vtable call per item. Route that path through a buffered_iterator that pulls a block of elements with a single IIterator::GetMany call into a small stack buffer and yields from it, refilling only when the buffer is drained. Existing `for (auto&& x : v)` code gets the speedup with no source change. The block is sized like windows-rs' BufferedIterator -- clamp(2048 / sizeof(T), 1, 128) -- to cap the buffer near 1-2 KB and bound over-fetch for large element types. Only the non-GetAt path changes. Collections with GetAt (IVector, IVectorView) keep the existing random-access fast_iterator, so no iterator-category guarantees are affected. The iterator is single-pass, matching IIterator's own semantics. Tested in test/buffered_iterator.cpp (multi-block, block boundary, empty, and a non-trivial element type). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 83914e59-f7cd-4284-ad4b-4cb7b79f28c1 --- strings/base_iterator.h | 82 +++++++++++++++++++++++++++++---- test/test/buffered_iterator.cpp | 71 ++++++++++++++++++++++++++++ test/test/test.vcxproj | 1 + 3 files changed, 146 insertions(+), 8 deletions(-) create mode 100644 test/test/buffered_iterator.cpp diff --git a/strings/base_iterator.h b/strings/base_iterator.h index 46c0c6b63..cbcf412d6 100644 --- a/strings/base_iterator.h +++ b/strings/base_iterator.h @@ -141,23 +141,89 @@ WINRT_EXPORT namespace winrt::impl static constexpr bool value = get_value(0); }; - template ::value, int> = 0> - auto get_begin_iterator(T const& collection) -> decltype(collection.First()) + // Forward iterator that batches an IIterator via GetMany into a small buffer and yields from + // it, so range-for over a collection that lacks GetAt crosses the ABI once per block instead of + // once per element (Current/MoveNext). Single-pass, matching IIterator's semantics. + template + struct buffered_iterator { - auto result = collection.First(); + using value_type = decltype(std::declval().Current()); + using iterator_category = std::input_iterator_tag; + using difference_type = std::ptrdiff_t; + using pointer = value_type const*; + using reference = value_type const&; + + static constexpr std::uint32_t buffer_capacity = static_cast( + (std::min)(128, (std::max)(1, std::size_t{ 2048 } / sizeof(value_type)))); + + buffered_iterator() noexcept = default; - if (!result.HasCurrent()) + explicit buffered_iterator(Iterator iterator) : m_iterator(std::move(iterator)) { - return {}; + fill(); } - return result; + reference operator*() const noexcept + { + return m_buffer[m_index]; + } + + pointer operator->() const noexcept + { + return std::addressof(m_buffer[m_index]); + } + + buffered_iterator& operator++() + { + if (++m_index == m_size) + { + fill(); + } + + return *this; + } + + buffered_iterator operator++(int) + { + auto previous = *this; + ++*this; + return previous; + } + + bool operator==(buffered_iterator const& other) const noexcept + { + return (m_size == 0) && (other.m_size == 0); + } + + bool operator!=(buffered_iterator const& other) const noexcept + { + return !(*this == other); + } + + private: + + void fill() + { + m_index = 0; + m_size = m_iterator ? m_iterator.GetMany(m_buffer) : 0; + } + + Iterator m_iterator{ nullptr }; + std::array m_buffer; + std::uint32_t m_size{ 0 }; + std::uint32_t m_index{ 0 }; + }; + + template ::value, int> = 0> + auto get_begin_iterator(T const& collection) + { + return buffered_iterator{ collection.First() }; } template ::value, int> = 0> - auto get_end_iterator([[maybe_unused]] T const& collection) noexcept -> decltype(collection.First()) + auto get_end_iterator([[maybe_unused]] T const& collection) noexcept { - return {}; + return buffered_iterator().First())>{}; } template ::value, int> = 0> diff --git a/test/test/buffered_iterator.cpp b/test/test/buffered_iterator.cpp new file mode 100644 index 000000000..49c6814ed --- /dev/null +++ b/test/test/buffered_iterator.cpp @@ -0,0 +1,71 @@ +#include "pch.h" + +using namespace winrt; +using namespace Windows::Foundation::Collections; + +TEST_CASE("buffered_iterator") +{ + // Iterating through IIterable (which has no GetAt) exercises the buffered + // GetMany path. Use enough elements to span multiple buffer blocks so a refill + // is required. + { + std::vector expected; + for (int32_t i = 0; i < 300; ++i) + { + expected.push_back(i); + } + + IIterable iterable = single_threaded_vector(std::vector(expected)); + + std::vector observed; + for (auto&& value : iterable) + { + observed.push_back(value); + } + + REQUIRE(observed == expected); + } + + // Exactly one element beyond a single block boundary (int32_t block is 128). + { + IIterable iterable = single_threaded_vector(std::vector(129, 7)); + + uint32_t count = 0; + for (auto&& value : iterable) + { + REQUIRE(value == 7); + ++count; + } + + REQUIRE(count == 129); + } + + // Empty collection yields nothing. + { + IIterable iterable = single_threaded_vector(); + + uint32_t count = 0; + for (auto&& value : iterable) + { + (void)value; + ++count; + } + + REQUIRE(count == 0); + } + + // A non-trivial element type round-trips through the buffer. + { + IIterable iterable = single_threaded_vector({ L"a", L"b", L"c" }); + + std::vector observed; + for (auto&& value : iterable) + { + observed.push_back(value); + } + + REQUIRE(observed.size() == 3); + REQUIRE(observed[0] == L"a"); + REQUIRE(observed[2] == L"c"); + } +} diff --git a/test/test/test.vcxproj b/test/test/test.vcxproj index f87a65a13..dc33fea31 100644 --- a/test/test/test.vcxproj +++ b/test/test/test.vcxproj @@ -234,6 +234,7 @@ + NotUsing From 1f16ab0fbca676cc8315c9c63576d32a24e4efcf Mon Sep 17 00:00:00 2001 From: Jon Wiswall Date: Thu, 16 Jul 2026 15:51:57 -0700 Subject: [PATCH 04/12] Perfect-forward make_ready value Use TResult&& + decay_t so make_ready forwards its argument into the operation instead of taking it by value, saving a move and matching the make_unique/make_shared idiom. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 83914e59-f7cd-4284-ad4b-4cb7b79f28c1 --- strings/base_coroutine_foundation.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/strings/base_coroutine_foundation.h b/strings/base_coroutine_foundation.h index 7f381d9e8..051e0aa87 100644 --- a/strings/base_coroutine_foundation.h +++ b/strings/base_coroutine_foundation.h @@ -966,9 +966,9 @@ WINRT_EXPORT namespace winrt } template - Windows::Foundation::IAsyncOperation make_ready(TResult value) + Windows::Foundation::IAsyncOperation> make_ready(TResult&& value) { - return make>(std::move(value)); + return make>>(std::forward(value)); } inline Windows::Foundation::IAsyncAction make_ready() noexcept From 0adfbebcf1c95060402032739ac0559a4541f14d Mon Sep 17 00:00:00 2001 From: Jon Wiswall Date: Thu, 16 Jul 2026 15:55:54 -0700 Subject: [PATCH 05/12] Make _hs a constant expression Move the fast-pass header from a function-local static into an inline constexpr variable template and mark hstring_reference and the operator constexpr, so `constexpr auto s = L"x"_hs;` is a genuine compile-time construction rather than per-call work. Add a constexpr construction to the test to prove it. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 83914e59-f7cd-4284-ad4b-4cb7b79f28c1 --- strings/base_string.h | 32 +++++++++++++++++------------ test/test_cpp20/hstring_literal.cpp | 9 ++++++++ 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/strings/base_string.h b/strings/base_string.h index 4daac531c..5c369f0fc 100644 --- a/strings/base_string.h +++ b/strings/base_string.h @@ -406,9 +406,9 @@ WINRT_EXPORT namespace winrt struct hstring_reference { - hstring_reference() noexcept = default; + constexpr hstring_reference() noexcept = default; - explicit hstring_reference(impl::hstring_header const* header) noexcept : + constexpr explicit hstring_reference(impl::hstring_header const* header) noexcept : m_handle(const_cast(header)) { } @@ -481,12 +481,27 @@ WINRT_EXPORT namespace winrt #if defined(__cpp_nontype_template_args) && __cpp_nontype_template_args >= 201911L +WINRT_EXPORT namespace winrt::impl +{ + // The fast-pass header for each distinct literal, materialized once as a compile-time + // constant so `_hs` is a constant expression rather than per-call work. + template + inline constexpr hstring_header hstring_literal_header + { + hstring_reference_flag, + static_cast(Literal.size - 1), + 0, + 0, + Literal.value + }; +} + WINRT_EXPORT namespace winrt { inline namespace literals { template - hstring_reference operator ""_hs() noexcept + constexpr hstring_reference operator ""_hs() noexcept { if constexpr (Literal.size <= 1) { @@ -494,16 +509,7 @@ WINRT_EXPORT namespace winrt } else { - static constexpr impl::hstring_header header - { - impl::hstring_reference_flag, - static_cast(Literal.size - 1), - 0, - 0, - Literal.value - }; - - return hstring_reference{ &header }; + return hstring_reference{ &impl::hstring_literal_header }; } } } diff --git a/test/test_cpp20/hstring_literal.cpp b/test/test_cpp20/hstring_literal.cpp index 12a501307..0e92854c8 100644 --- a/test/test_cpp20/hstring_literal.cpp +++ b/test/test_cpp20/hstring_literal.cpp @@ -19,6 +19,15 @@ namespace TEST_CASE("hstring_literal") { + // The literal is a genuine constant expression: the fast-pass header is built at + // compile time, so an hstring_reference can be constructed in a constexpr context. + { + constexpr winrt::hstring_reference lit = L"kittens"_hs; + winrt::hstring const& value = lit; + REQUIRE(value == L"kittens"sv); + REQUIRE(value.size() == 7); + } + // Content and length match the literal. { winrt::hstring_reference const lit = L"kittens"_hs; From 71cc2c68ead161cd3a095242d1e0b8eaab93f76f Mon Sep 17 00:00:00 2001 From: Jon Wiswall Date: Thu, 16 Jul 2026 16:11:22 -0700 Subject: [PATCH 06/12] Box scalars locally instead of via combase PropertyValue box_value on a scalar routed through the cached Windows.Foundation. PropertyValue activation factory into combase, which allocates a general IPropertyValue carrying the discriminated-union machinery for all property types. For the common scalar cases that round-trip is pure overhead. Point the scalar reference_traits (u8..u64, float, double, bool, char16, hstring, guid) at the in-process impl::reference that already backs non-scalar IReference, and make that type a correct IPropertyValue: report the right PropertyType per T (was always OtherType), fix IsNumericScalar (was true for bool), and return the value from the matching typed getter (GetString/GetGuid/GetBoolean/GetChar16/GetSingle/GetDouble previously threw). Mismatched numeric getters keep combase-style conversion (GetInt16 on a boxed int32 converts), so consumers see the same behavior minus the combase hop. This mirrors windows-rs' StockReference. Composite/array/inspectable cases (DateTime, TimeSpan, Point, Size, Rect, IReferenceArray, IInspectable) stay on combase PropertyValue. Two honest deltas vs combase, both matching windows-rs: GetRuntimeClassName is now the IReference`1 name rather than Windows.Foundation. PropertyValue, and cross-process the value marshals as an IReference proxy rather than by value. Tested in test/reference_boxing.cpp. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 83914e59-f7cd-4284-ad4b-4cb7b79f28c1 --- strings/base_reference_produce.h | 124 ++++++------------------------- test/test/reference_boxing.cpp | 61 +++++++++++++++ test/test/test.vcxproj | 1 + 3 files changed, 86 insertions(+), 100 deletions(-) create mode 100644 test/test/reference_boxing.cpp diff --git a/strings/base_reference_produce.h b/strings/base_reference_produce.h index abffb3384..5248a400d 100644 --- a/strings/base_reference_produce.h +++ b/strings/base_reference_produce.h @@ -15,12 +15,27 @@ WINRT_EXPORT namespace winrt::impl Windows::Foundation::PropertyType Type() const noexcept { - return Windows::Foundation::PropertyType::OtherType; + using pt = Windows::Foundation::PropertyType; + + if constexpr (std::is_same_v) { return pt::UInt8; } + else if constexpr (std::is_same_v) { return pt::Int16; } + else if constexpr (std::is_same_v) { return pt::UInt16; } + else if constexpr (std::is_same_v) { return pt::Int32; } + else if constexpr (std::is_same_v) { return pt::UInt32; } + else if constexpr (std::is_same_v) { return pt::Int64; } + else if constexpr (std::is_same_v) { return pt::UInt64; } + else if constexpr (std::is_same_v) { return pt::Single; } + else if constexpr (std::is_same_v) { return pt::Double; } + else if constexpr (std::is_same_v) { return pt::Char16; } + else if constexpr (std::is_same_v) { return pt::Boolean; } + else if constexpr (std::is_same_v) { return pt::String; } + else if constexpr (std::is_same_v) { return pt::Guid; } + else { return pt::OtherType; } } static constexpr bool IsNumericScalar() noexcept { - return std::is_arithmetic_v || std::is_enum_v; + return (std::is_arithmetic_v && !std::is_same_v && !std::is_same_v) || std::is_enum_v; } std::uint8_t GetUInt8() const @@ -58,12 +73,12 @@ WINRT_EXPORT namespace winrt::impl return to_scalar(); } - float GetSingle() { throw hresult_not_implemented(); } - double GetDouble() { throw hresult_not_implemented(); } - char16_t GetChar16() { throw hresult_not_implemented(); } - bool GetBoolean() { throw hresult_not_implemented(); } - hstring GetString() { throw hresult_not_implemented(); } - guid GetGuid() { throw hresult_not_implemented(); } + float GetSingle() { return to_scalar(); } + double GetDouble() { return to_scalar(); } + char16_t GetChar16() { if constexpr (std::is_same_v) { return m_value; } else { throw hresult_not_implemented(); } } + bool GetBoolean() { if constexpr (std::is_same_v) { return m_value; } else { throw hresult_not_implemented(); } } + hstring GetString() { if constexpr (std::is_same_v) { return m_value; } else { throw hresult_not_implemented(); } } + guid GetGuid() { if constexpr (std::is_same_v) { return m_value; } else { throw hresult_not_implemented(); } } Windows::Foundation::DateTime GetDateTime() { throw hresult_not_implemented(); } Windows::Foundation::TimeSpan GetTimeSpan() { throw hresult_not_implemented(); } Windows::Foundation::Point GetPoint() { throw hresult_not_implemented(); } @@ -114,90 +129,6 @@ WINRT_EXPORT namespace winrt::impl using itf = Windows::Foundation::IReference; }; - template <> - struct reference_traits - { - static auto make(std::uint8_t value) { return Windows::Foundation::PropertyValue::CreateUInt8(value); } - using itf = Windows::Foundation::IReference; - }; - - template <> - struct reference_traits - { - static auto make(std::uint16_t value) { return Windows::Foundation::PropertyValue::CreateUInt16(value); } - using itf = Windows::Foundation::IReference; - }; - - template <> - struct reference_traits - { - static auto make(std::int16_t value) { return Windows::Foundation::PropertyValue::CreateInt16(value); } - using itf = Windows::Foundation::IReference; - }; - - template <> - struct reference_traits - { - static auto make(std::uint32_t value) { return Windows::Foundation::PropertyValue::CreateUInt32(value); } - using itf = Windows::Foundation::IReference; - }; - - template <> - struct reference_traits - { - static auto make(std::int32_t value) { return Windows::Foundation::PropertyValue::CreateInt32(value); } - using itf = Windows::Foundation::IReference; - }; - - template <> - struct reference_traits - { - static auto make(std::uint64_t value) { return Windows::Foundation::PropertyValue::CreateUInt64(value); } - using itf = Windows::Foundation::IReference; - }; - - template <> - struct reference_traits - { - static auto make(std::int64_t value) { return Windows::Foundation::PropertyValue::CreateInt64(value); } - using itf = Windows::Foundation::IReference; - }; - - template <> - struct reference_traits - { - static auto make(float value) { return Windows::Foundation::PropertyValue::CreateSingle(value); } - using itf = Windows::Foundation::IReference; - }; - - template <> - struct reference_traits - { - static auto make(double value) { return Windows::Foundation::PropertyValue::CreateDouble(value); } - using itf = Windows::Foundation::IReference; - }; - - template <> - struct reference_traits - { - static auto make(char16_t value) { return Windows::Foundation::PropertyValue::CreateChar16(value); } - using itf = Windows::Foundation::IReference; - }; - - template <> - struct reference_traits - { - static auto make(bool value) { return Windows::Foundation::PropertyValue::CreateBoolean(value); } - using itf = Windows::Foundation::IReference; - }; - - template <> - struct reference_traits - { - static auto make(hstring const& value) { return Windows::Foundation::PropertyValue::CreateString(value); } - using itf = Windows::Foundation::IReference; - }; - template <> struct reference_traits { @@ -205,17 +136,10 @@ WINRT_EXPORT namespace winrt::impl using itf = Windows::Foundation::IInspectable; }; - template <> - struct reference_traits - { - static auto make(guid const& value) { return Windows::Foundation::PropertyValue::CreateGuid(value); } - using itf = Windows::Foundation::IReference; - }; - template <> struct reference_traits { - static auto make(GUID const& value) { return Windows::Foundation::PropertyValue::CreateGuid(reinterpret_cast(value)); } + static auto make(GUID const& value) { return reference_traits::make(reinterpret_cast(value)); } using itf = Windows::Foundation::IReference; }; diff --git a/test/test/reference_boxing.cpp b/test/test/reference_boxing.cpp new file mode 100644 index 000000000..24476c2cd --- /dev/null +++ b/test/test/reference_boxing.cpp @@ -0,0 +1,61 @@ +#include "pch.h" + +using namespace winrt; +using namespace Windows::Foundation; + +// Scalar box_value now produces a local IReference/IPropertyValue instead of hopping to +// combase PropertyValue. These confirm it reports the correct PropertyType, keeps combase-style +// numeric conversion on mismatched getters, and round-trips through unbox_value. +TEST_CASE("reference_boxing") +{ + { + auto boxed = box_value(42); + auto pv = boxed.as(); + REQUIRE(pv.Type() == PropertyType::Int32); + REQUIRE(pv.IsNumericScalar()); + REQUIRE(pv.GetInt32() == 42); + REQUIRE(pv.GetInt16() == 42); + REQUIRE(pv.GetDouble() == 42.0); + REQUIRE(unbox_value(boxed) == 42); + } + + { + auto pv = box_value(3.5).as(); + REQUIRE(pv.Type() == PropertyType::Double); + REQUIRE(pv.IsNumericScalar()); + REQUIRE(pv.GetDouble() == 3.5); + REQUIRE(pv.GetSingle() == 3.5f); + } + + { + auto pv = box_value(hstring{ L"hello" }).as(); + REQUIRE(pv.Type() == PropertyType::String); + REQUIRE(!pv.IsNumericScalar()); + REQUIRE(pv.GetString() == L"hello"); + REQUIRE_THROWS_AS(pv.GetInt32(), hresult_not_implemented); + } + + { + auto pv = box_value(true).as(); + REQUIRE(pv.Type() == PropertyType::Boolean); + REQUIRE(!pv.IsNumericScalar()); + REQUIRE(pv.GetBoolean()); + REQUIRE_THROWS_AS(pv.GetInt32(), hresult_not_implemented); + } + + { + guid const g{ 0x11223344, 0x5566, 0x7788, { 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00 } }; + auto pv = box_value(g).as(); + REQUIRE(pv.Type() == PropertyType::Guid); + REQUIRE(!pv.IsNumericScalar()); + REQUIRE(pv.GetGuid() == g); + } + + { + auto pv = box_value(static_cast(7)).as(); + REQUIRE(pv.Type() == PropertyType::UInt8); + REQUIRE(pv.IsNumericScalar()); + REQUIRE(pv.GetUInt8() == 7); + REQUIRE(unbox_value(box_value(static_cast(7))) == 7); + } +} diff --git a/test/test/test.vcxproj b/test/test/test.vcxproj index dc33fea31..41b3e4d40 100644 --- a/test/test/test.vcxproj +++ b/test/test/test.vcxproj @@ -326,6 +326,7 @@ NotUsing + From bee7550f06fa12f9c1e9ff40104e2641ff74d7dc Mon Sep 17 00:00:00 2001 From: Jon Wiswall Date: Thu, 16 Jul 2026 17:03:39 -0700 Subject: [PATCH 07/12] Marshal boxed scalars by value via combase PropertyValue The in-proc scalar reference introduced in the prior commit is agile via the free-threaded marshaler, so cross-process it marshals by reference (an IReference proxy) rather than by value the way combase PropertyValue does. Mirror windows-rs and combase: for the stock scalar types, mark reference non_agile and supply IMarshal from query_interface_tearoff by lazily building the equivalent combase PropertyValue and delegating marshaling to it, so the destination materializes a real PropertyValue copy. IAgileObject is still advertised (the reference is immutable and thread-safe) to keep the agile fast path. The combase hop is paid only on marshal, never on box_value/unbox_value. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- strings/base_reference_produce.h | 85 +++++++++++++++++++++++++++++++- test/test/reference_boxing.cpp | 29 +++++++++++ 2 files changed, 113 insertions(+), 1 deletion(-) diff --git a/strings/base_reference_produce.h b/strings/base_reference_produce.h index 5248a400d..2b81722a4 100644 --- a/strings/base_reference_produce.h +++ b/strings/base_reference_produce.h @@ -2,7 +2,32 @@ WINRT_EXPORT namespace winrt::impl { template - struct reference : implements, Windows::Foundation::IReference, Windows::Foundation::IPropertyValue> + struct reference; + + // The scalar types that combase PropertyValue can carry by value. box_value on one of these + // produces an in-process reference for the fast path, but must still marshal by value across + // apartments/processes so the destination sees a real PropertyValue copy rather than a proxy. + template + inline constexpr bool is_stock_reference_v = + std::is_same_v || std::is_same_v || + std::is_same_v || std::is_same_v || + std::is_same_v || std::is_same_v || + std::is_same_v || std::is_same_v || + std::is_same_v || std::is_same_v || + std::is_same_v || std::is_same_v || + std::is_same_v; + + // Stock scalar references are marked non_agile so that our query_interface_tearoff supplies + // IMarshal (delegating to combase PropertyValue for by-value marshaling) instead of the default + // free-threaded-marshaler that marshals by reference. All other T keep the default agile shape. + template + using reference_base_t = std::conditional_t< + is_stock_reference_v, + implements, Windows::Foundation::IReference, Windows::Foundation::IPropertyValue, non_agile>, + implements, Windows::Foundation::IReference, Windows::Foundation::IPropertyValue>>; + + template + struct reference : reference_base_t { reference(T const& value) : m_value(value) { @@ -106,6 +131,64 @@ WINRT_EXPORT namespace winrt::impl private: + // For stock scalar T, hand out an IMarshal that marshals by value: build the equivalent + // combase PropertyValue on demand and delegate marshaling to it. This is lazy - box_value and + // unbox_value never touch combase; the hop only happens if the reference is actually marshaled. + std::int32_t query_interface_tearoff(guid const& id, void** object) const noexcept override + { + if constexpr (is_stock_reference_v) + { + if (is_guid_of(id)) + { + try + { + auto marshal = create_property_value().as(); + *object = detach_abi(marshal); + return error_ok; + } + catch (...) + { + *object = nullptr; + return to_hresult(); + } + } + + // reference is immutable, so it is safe to call from any apartment. Advertise + // IAgileObject (as combase PropertyValue does) so callers keep the agile fast path; + // cross-apartment/process marshaling still routes through the by-value IMarshal above. + if (is_guid_of(id)) + { + auto unknown = reinterpret_cast(to_abi>(this)); + unknown->AddRef(); + *object = unknown; + return error_ok; + } + } + + *object = nullptr; + return error_no_interface; + } + + Windows::Foundation::IInspectable create_property_value() const + { + using pv = Windows::Foundation::PropertyValue; + + if constexpr (std::is_same_v) { return pv::CreateUInt8(m_value); } + else if constexpr (std::is_same_v) { return pv::CreateInt16(m_value); } + else if constexpr (std::is_same_v) { return pv::CreateUInt16(m_value); } + else if constexpr (std::is_same_v) { return pv::CreateInt32(m_value); } + else if constexpr (std::is_same_v) { return pv::CreateUInt32(m_value); } + else if constexpr (std::is_same_v) { return pv::CreateInt64(m_value); } + else if constexpr (std::is_same_v) { return pv::CreateUInt64(m_value); } + else if constexpr (std::is_same_v) { return pv::CreateSingle(m_value); } + else if constexpr (std::is_same_v) { return pv::CreateDouble(m_value); } + else if constexpr (std::is_same_v) { return pv::CreateChar16(m_value); } + else if constexpr (std::is_same_v) { return pv::CreateBoolean(m_value); } + else if constexpr (std::is_same_v) { return pv::CreateString(m_value); } + else if constexpr (std::is_same_v) { return pv::CreateGuid(m_value); } + else { return nullptr; } + } + template To to_scalar() const { diff --git a/test/test/reference_boxing.cpp b/test/test/reference_boxing.cpp index 24476c2cd..bb243e7b1 100644 --- a/test/test/reference_boxing.cpp +++ b/test/test/reference_boxing.cpp @@ -1,4 +1,6 @@ #include "pch.h" +#include +#include using namespace winrt; using namespace Windows::Foundation; @@ -59,3 +61,30 @@ TEST_CASE("reference_boxing") REQUIRE(unbox_value(box_value(static_cast(7))) == 7); } } + +// The in-proc reference stays agile but must marshal by value across processes, exactly like a real +// combase PropertyValue. Prove it by confirming our IMarshal reports the SAME unmarshal class as a +// genuine PropertyValue - i.e. we forward marshaling to combase - and specifically NOT the +// free-threaded (marshal-by-reference) class the default agile path would have used. +TEST_CASE("reference_boxing marshal by value") +{ + auto boxed = box_value(42); + REQUIRE(boxed.try_as()); + auto ours = boxed.as(); + + auto genuine = PropertyValue::CreateInt32(42); + auto reference = genuine.as(); + + guid our_clsid{}; + guid reference_clsid{}; + check_hresult(ours->GetUnmarshalClass(guid_of(), get_unknown(boxed), + MSHCTX_DIFFERENTMACHINE, nullptr, MSHLFLAGS_NORMAL, &our_clsid)); + check_hresult(reference->GetUnmarshalClass(guid_of(), get_unknown(genuine), + MSHCTX_DIFFERENTMACHINE, nullptr, MSHLFLAGS_NORMAL, &reference_clsid)); + + REQUIRE(our_clsid == reference_clsid); + + // CLSID_InProcFreeMarshaler - the by-reference class the agile FTM would have produced. + guid const free_threaded_marshaler{ 0x0000033A, 0x0000, 0x0000, { 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46 } }; + REQUIRE(our_clsid != free_threaded_marshaler); +} From 75bf146be8327ea8947e144e11b867dcf16e076a Mon Sep 17 00:00:00 2001 From: Jon Wiswall Date: Thu, 16 Jul 2026 17:07:46 -0700 Subject: [PATCH 08/12] Box DateTime, TimeSpan, and Point in-process too Extend the in-proc reference to cover DateTime, TimeSpan, and Point: report the correct PropertyType, return the value from the matching typed getter, and mark them stock so they still marshal by value through combase PropertyValue. Drop their combase reference_traits specializations so box_value routes to the local reference like the other scalars. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- strings/base_reference_produce.h | 36 ++++++++++---------------------- test/test/reference_boxing.cpp | 32 ++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 25 deletions(-) diff --git a/strings/base_reference_produce.h b/strings/base_reference_produce.h index 2b81722a4..30f099ec0 100644 --- a/strings/base_reference_produce.h +++ b/strings/base_reference_produce.h @@ -15,7 +15,8 @@ WINRT_EXPORT namespace winrt::impl std::is_same_v || std::is_same_v || std::is_same_v || std::is_same_v || std::is_same_v || std::is_same_v || - std::is_same_v; + std::is_same_v || std::is_same_v || + std::is_same_v || std::is_same_v; // Stock scalar references are marked non_agile so that our query_interface_tearoff supplies // IMarshal (delegating to combase PropertyValue for by-value marshaling) instead of the default @@ -55,6 +56,9 @@ WINRT_EXPORT namespace winrt::impl else if constexpr (std::is_same_v) { return pt::Boolean; } else if constexpr (std::is_same_v) { return pt::String; } else if constexpr (std::is_same_v) { return pt::Guid; } + else if constexpr (std::is_same_v) { return pt::DateTime; } + else if constexpr (std::is_same_v) { return pt::TimeSpan; } + else if constexpr (std::is_same_v) { return pt::Point; } else { return pt::OtherType; } } @@ -104,9 +108,9 @@ WINRT_EXPORT namespace winrt::impl bool GetBoolean() { if constexpr (std::is_same_v) { return m_value; } else { throw hresult_not_implemented(); } } hstring GetString() { if constexpr (std::is_same_v) { return m_value; } else { throw hresult_not_implemented(); } } guid GetGuid() { if constexpr (std::is_same_v) { return m_value; } else { throw hresult_not_implemented(); } } - Windows::Foundation::DateTime GetDateTime() { throw hresult_not_implemented(); } - Windows::Foundation::TimeSpan GetTimeSpan() { throw hresult_not_implemented(); } - Windows::Foundation::Point GetPoint() { throw hresult_not_implemented(); } + Windows::Foundation::DateTime GetDateTime() { if constexpr (std::is_same_v) { return m_value; } else { throw hresult_not_implemented(); } } + Windows::Foundation::TimeSpan GetTimeSpan() { if constexpr (std::is_same_v) { return m_value; } else { throw hresult_not_implemented(); } } + Windows::Foundation::Point GetPoint() { if constexpr (std::is_same_v) { return m_value; } else { throw hresult_not_implemented(); } } Windows::Foundation::Size GetSize() { throw hresult_not_implemented(); } Windows::Foundation::Rect GetRect() { throw hresult_not_implemented(); } void GetUInt8Array(com_array &) { throw hresult_not_implemented(); } @@ -186,6 +190,9 @@ WINRT_EXPORT namespace winrt::impl else if constexpr (std::is_same_v) { return pv::CreateBoolean(m_value); } else if constexpr (std::is_same_v) { return pv::CreateString(m_value); } else if constexpr (std::is_same_v) { return pv::CreateGuid(m_value); } + else if constexpr (std::is_same_v) { return pv::CreateDateTime(m_value); } + else if constexpr (std::is_same_v) { return pv::CreateTimeSpan(m_value); } + else if constexpr (std::is_same_v) { return pv::CreatePoint(m_value); } else { return nullptr; } } @@ -226,27 +233,6 @@ WINRT_EXPORT namespace winrt::impl using itf = Windows::Foundation::IReference; }; - template <> - struct reference_traits - { - static auto make(Windows::Foundation::DateTime value) { return Windows::Foundation::PropertyValue::CreateDateTime(value); } - using itf = Windows::Foundation::IReference; - }; - - template <> - struct reference_traits - { - static auto make(Windows::Foundation::TimeSpan value) { return Windows::Foundation::PropertyValue::CreateTimeSpan(value); } - using itf = Windows::Foundation::IReference; - }; - - template <> - struct reference_traits - { - static auto make(Windows::Foundation::Point const& value) { return Windows::Foundation::PropertyValue::CreatePoint(value); } - using itf = Windows::Foundation::IReference; - }; - template <> struct reference_traits { diff --git a/test/test/reference_boxing.cpp b/test/test/reference_boxing.cpp index bb243e7b1..fbd83c32f 100644 --- a/test/test/reference_boxing.cpp +++ b/test/test/reference_boxing.cpp @@ -60,6 +60,38 @@ TEST_CASE("reference_boxing") REQUIRE(pv.GetUInt8() == 7); REQUIRE(unbox_value(box_value(static_cast(7))) == 7); } + + // DateTime, TimeSpan, and Point are also boxed in-process now (they still marshal by value). + { + Point const point{ 3.0f, 4.0f }; + auto pv = box_value(point).as(); + REQUIRE(pv.Type() == PropertyType::Point); + REQUIRE(!pv.IsNumericScalar()); + REQUIRE(pv.GetPoint().X == point.X); + REQUIRE(pv.GetPoint().Y == point.Y); + auto const round_tripped = unbox_value(box_value(point)); + REQUIRE(round_tripped.X == point.X); + REQUIRE(round_tripped.Y == point.Y); + REQUIRE_THROWS_AS(pv.GetInt32(), hresult_not_implemented); + } + + { + TimeSpan const span{ std::chrono::seconds{ 90 } }; + auto pv = box_value(span).as(); + REQUIRE(pv.Type() == PropertyType::TimeSpan); + REQUIRE(!pv.IsNumericScalar()); + REQUIRE(pv.GetTimeSpan() == span); + REQUIRE(unbox_value(box_value(span)) == span); + } + + { + DateTime const when{ TimeSpan{ std::chrono::seconds{ 1000 } } }; + auto pv = box_value(when).as(); + REQUIRE(pv.Type() == PropertyType::DateTime); + REQUIRE(!pv.IsNumericScalar()); + REQUIRE(pv.GetDateTime() == when); + REQUIRE(unbox_value(box_value(when)) == when); + } } // The in-proc reference stays agile but must marshal by value across processes, exactly like a real From ab1f299635a44f853e10f3281de0a9967123eabb Mon Sep 17 00:00:00 2001 From: Jon Wiswall <18537118+jonwis@users.noreply.github.com> Date: Thu, 16 Jul 2026 23:42:57 -0700 Subject: [PATCH 09/12] Buffer range-for over GetAt collections (batch IVector via GetMany) fast_iterator (used for random-access, GetAt-capable collections like IVector/ IVectorView) now prefetches a block with a single GetMany call and serves in-window reads from it, so range-for crosses the ABI ~once per block instead of one GetAt per element. Random access is preserved: an out-of-window index re-anchors the block, and an at/after-end index defers to GetAt so E_BOUNDS behavior is unchanged. Elements are copied out (no move-out) so an index may be read repeatedly, as the random-access contract requires. This extends the non-GetAt buffering to vectors, closing the IterateVector gap to windows-rs. --- strings/base_iterator.h | 37 +++++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/strings/base_iterator.h b/strings/base_iterator.h index cbcf412d6..e2d8b7050 100644 --- a/strings/base_iterator.h +++ b/strings/base_iterator.h @@ -73,12 +73,12 @@ WINRT_EXPORT namespace winrt::impl reference operator*() const { - return m_collection->GetAt(m_index); + return fetch(m_index); } reference operator[](difference_type n) const { - return m_collection->GetAt(m_index + static_cast(n)); + return fetch(m_index + static_cast(n)); } bool operator==(fast_iterator const& other) const noexcept @@ -126,8 +126,41 @@ WINRT_EXPORT namespace winrt::impl private: + // Batched forward traversal for random-access (GetAt-capable) collections. Instead of one + // GetAt ABI call per element, `fetch` fills a small block with a single GetMany call and + // serves in-window reads from it, so range-for (sequential ++ then *) crosses the ABI + // ~once per `block` elements. Random access still works: an out-of-window index re-anchors + // the block there, and an at/after-end index defers to GetAt so the component's bounds + // behavior (E_BOUNDS) is preserved. Elements are copied out -- no move-out -- so a given + // index may be read repeatedly, as the random-access contract requires. + static constexpr std::uint32_t block = static_cast( + (std::min)(128, (std::max)(1, std::size_t{ 2048 } / sizeof(value_type)))); + + reference fetch(std::uint32_t const index) const + { + if (index < m_buffer_base || index >= m_buffer_base + m_buffer_size) + { + for (std::uint32_t i = 0; i < m_buffer_size; ++i) + { + m_buffer[i] = value_type{}; + } + m_buffer_base = index; + m_buffer_size = m_collection->GetMany(index, m_buffer); + } + + if (index < m_buffer_base + m_buffer_size) + { + return m_buffer[index - m_buffer_base]; + } + + return m_collection->GetAt(index); + } + T const* m_collection = nullptr; std::uint32_t m_index = 0; + mutable std::uint32_t m_buffer_base = 0; + mutable std::uint32_t m_buffer_size = 0; + mutable std::array m_buffer{}; }; template From 4112ae6d500eeac313f6afe111a8321d038dfea8 Mon Sep 17 00:00:00 2001 From: Jon Wiswall Date: Fri, 17 Jul 2026 11:59:11 -0700 Subject: [PATCH 10/12] Fix MSVC and clang-cl build breaks from the interop fast paths Three portability breaks surfaced by CI across MSVC and clang-cl: - Iterator batching assumed every collection/iterator has GetMany and every element type is default-constructible. IBindableVectorView has GetAt but no GetMany, IBindableIterator has no GetMany, and types like JsonValue have no default constructor. Gate the GetMany block buffer on both a GetMany detector and default-constructibility (can_batch); otherwise fall back to per-element GetAt / Current+MoveNext. The block buffer is elided entirely when unused. - reference::query_interface_tearoff called .as() on a dependent expression; clang requires .template as(). - hstring_reference::m_handle is only read via layout punning in get_abi, so clang -Werror,-Wunused-private-field rejected it. Mark it [[maybe_unused]]. Verified: msbuild cppwinrt + test/test_old (MSVC) and test/test_nocoro/test_old (clang-cl), x64 Debug, all build clean. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- strings/base_iterator.h | 102 ++++++++++++++++++++++++------- strings/base_reference_produce.h | 2 +- strings/base_string.h | 2 +- 3 files changed, 83 insertions(+), 23 deletions(-) diff --git a/strings/base_iterator.h b/strings/base_iterator.h index e2d8b7050..afb45aaff 100644 --- a/strings/base_iterator.h +++ b/strings/base_iterator.h @@ -1,6 +1,35 @@ WINRT_EXPORT namespace winrt::impl { + // Detects whether an indexed (GetAt-capable) collection also exposes the bulk + // GetMany(startIndex, array) accessor. IBindableVectorView and similar have GetAt but no GetMany. + template + class has_GetMany_indexed + { + template ().GetAt(0)), + typename = decltype(std::declval().GetMany(0u, std::declval&>()))> + static constexpr bool get_value(int) { return true; } + template static constexpr bool get_value(...) { return false; } + + public: + + static constexpr bool value = get_value(0); + }; + + // Detects whether an iterator exposes the bulk GetMany(array) accessor. IBindableIterator does not. + template + class has_GetMany_iter + { + template ().Current()), + typename = decltype(std::declval().GetMany(std::declval&>()))> + static constexpr bool get_value(int) { return true; } + template static constexpr bool get_value(...) { return false; } + + public: + + static constexpr bool value = get_value(0); + }; + template struct fast_iterator { @@ -126,31 +155,36 @@ WINRT_EXPORT namespace winrt::impl private: - // Batched forward traversal for random-access (GetAt-capable) collections. Instead of one - // GetAt ABI call per element, `fetch` fills a small block with a single GetMany call and - // serves in-window reads from it, so range-for (sequential ++ then *) crosses the ABI - // ~once per `block` elements. Random access still works: an out-of-window index re-anchors - // the block there, and an at/after-end index defers to GetAt so the component's bounds - // behavior (E_BOUNDS) is preserved. Elements are copied out -- no move-out -- so a given - // index may be read repeatedly, as the random-access contract requires. + // Batched forward traversal for random-access (GetAt-capable) collections that also expose + // GetMany. Instead of one GetAt ABI call per element, `fetch` fills a small block with a single + // GetMany call and serves in-window reads from it, so range-for (sequential ++ then *) crosses + // the ABI ~once per `block` elements. Random access still works: an out-of-window index + // re-anchors the block there, and an at/after-end index defers to GetAt so the component's + // bounds behavior (E_BOUNDS) is preserved. Elements are copied out -- no move-out -- so a given + // index may be read repeatedly, as the random-access contract requires. Collections without + // GetMany, or whose element type is not default-constructible, fall back to plain GetAt. + static constexpr bool can_batch = std::is_default_constructible_v && has_GetMany_indexed::value; + static constexpr std::uint32_t block = static_cast( (std::min)(128, (std::max)(1, std::size_t{ 2048 } / sizeof(value_type)))); + struct no_buffer {}; + using buffer_type = std::conditional_t, no_buffer>; + reference fetch(std::uint32_t const index) const { - if (index < m_buffer_base || index >= m_buffer_base + m_buffer_size) + if constexpr (can_batch) { - for (std::uint32_t i = 0; i < m_buffer_size; ++i) + if (index < m_buffer_base || index >= m_buffer_base + m_buffer_size) { - m_buffer[i] = value_type{}; + m_buffer_base = index; + m_buffer_size = m_collection->GetMany(index, m_buffer); } - m_buffer_base = index; - m_buffer_size = m_collection->GetMany(index, m_buffer); - } - if (index < m_buffer_base + m_buffer_size) - { - return m_buffer[index - m_buffer_base]; + if (index < m_buffer_base + m_buffer_size) + { + return m_buffer[index - m_buffer_base]; + } } return m_collection->GetAt(index); @@ -160,7 +194,7 @@ WINRT_EXPORT namespace winrt::impl std::uint32_t m_index = 0; mutable std::uint32_t m_buffer_base = 0; mutable std::uint32_t m_buffer_size = 0; - mutable std::array m_buffer{}; + mutable buffer_type m_buffer{}; }; template @@ -176,7 +210,8 @@ WINRT_EXPORT namespace winrt::impl // Forward iterator that batches an IIterator via GetMany into a small buffer and yields from // it, so range-for over a collection that lacks GetAt crosses the ABI once per block instead of - // once per element (Current/MoveNext). Single-pass, matching IIterator's semantics. + // once per element (Current/MoveNext). Single-pass, matching IIterator's semantics. Only used + // when the iterator exposes GetMany and its element type is default-constructible. template struct buffered_iterator { @@ -186,6 +221,8 @@ WINRT_EXPORT namespace winrt::impl using pointer = value_type const*; using reference = value_type const&; + static constexpr bool can_batch = std::is_default_constructible_v && has_GetMany_iter::value; + static constexpr std::uint32_t buffer_capacity = static_cast( (std::min)(128, (std::max)(1, std::size_t{ 2048 } / sizeof(value_type)))); @@ -250,13 +287,36 @@ WINRT_EXPORT namespace winrt::impl template ::value, int> = 0> auto get_begin_iterator(T const& collection) { - return buffered_iterator{ collection.First() }; + using iterator_type = decltype(collection.First()); + if constexpr (buffered_iterator::can_batch) + { + return buffered_iterator{ collection.First() }; + } + else + { + auto result = collection.First(); + + if (!result.HasCurrent()) + { + return iterator_type{}; + } + + return result; + } } template ::value, int> = 0> - auto get_end_iterator([[maybe_unused]] T const& collection) noexcept + auto get_end_iterator([[maybe_unused]] T const& collection) { - return buffered_iterator().First())>{}; + using iterator_type = decltype(collection.First()); + if constexpr (buffered_iterator::can_batch) + { + return buffered_iterator{}; + } + else + { + return iterator_type{}; + } } template ::value, int> = 0> diff --git a/strings/base_reference_produce.h b/strings/base_reference_produce.h index 30f099ec0..c925fa96d 100644 --- a/strings/base_reference_produce.h +++ b/strings/base_reference_produce.h @@ -146,7 +146,7 @@ WINRT_EXPORT namespace winrt::impl { try { - auto marshal = create_property_value().as(); + auto marshal = create_property_value().template as(); *object = detach_abi(marshal); return error_ok; } diff --git a/strings/base_string.h b/strings/base_string.h index 5c369f0fc..d0cfac111 100644 --- a/strings/base_string.h +++ b/strings/base_string.h @@ -420,7 +420,7 @@ WINRT_EXPORT namespace winrt private: - void* m_handle{}; + [[maybe_unused]] void* m_handle{}; }; inline void* get_abi(hstring_reference const& object) noexcept From 6e4a57876da3352e032675e8ac5cb2b291050228 Mon Sep 17 00:00:00 2001 From: Jon Wiswall Date: Fri, 17 Jul 2026 13:03:39 -0700 Subject: [PATCH 11/12] Add optional clang compiler arg to build_test_all.cmd build_test_all.cmd only built the MSVC toolset, so a local pass did not catch clang-cl -Werror breaks that CI's clang-cl leg rejects. Add an optional 5th positional arg (default msvc); pass clang/clang-cl to append Clang=1,PlatformToolset=ClangCl to the cppwinrt.sln compiler and test builds, matching CI. natvis and NugetTest.sln stay on MSVC, as the clang CI leg does not cover them. Also fix a stray trailing quote on the nuget restore line and document the arg in README. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- README.md | 2 +- build_test_all.cmd | 32 +++++++++++++++++++------------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 19e1493dc..112eeb2f8 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Don't build C++/WinRT yourself - just download the latest version here: https:// ## Working on the compiler -If you really want to build it yourself, the simplest way to do so is to run the `build_test_all.cmd` script in the root directory. Developers needing to work on the C++/WinRT compiler itself should go through the following steps to arrive at an efficient inner loop: +If you really want to build it yourself, the simplest way to do so is to run the `build_test_all.cmd` script in the root directory. It takes optional positional arguments: `build_test_all.cmd [platform] [configuration] [version] [clean] [compiler]`, e.g. `build_test_all.cmd x64 Debug 999.999.999.999 "" clang`. The `compiler` argument defaults to `msvc`; pass `clang` (or `clang-cl`) to build the compiler and test projects with the clang-cl toolset, matching the clang-cl leg of CI. Developers needing to work on the C++/WinRT compiler itself should go through the following steps to arrive at an efficient inner loop: * Open a dev command prompt pointing at the root of the repo. * Open the `cppwinrt.sln` solution. diff --git a/build_test_all.cmd b/build_test_all.cmd index c9beb852c..4ae1ee5ac 100644 --- a/build_test_all.cmd +++ b/build_test_all.cmd @@ -4,38 +4,44 @@ set target_platform=%1 set target_configuration=%2 set target_version=%3 set clean_intermediate_files=%4 +set target_compiler=%5 if "%target_platform%"=="" set target_platform=x64 if "%target_configuration%"=="" set target_configuration=Release if "%target_version%"=="" set target_version=999.999.999.999 +if "%target_compiler%"=="" set target_compiler=msvc + +set compiler_props= +if /i "%target_compiler%"=="clang" set compiler_props=,Clang=1,PlatformToolset=ClangCl +if /i "%target_compiler%"=="clang-cl" set compiler_props=,Clang=1,PlatformToolset=ClangCl if not exist ".\.nuget" mkdir ".\.nuget" if not exist ".\.nuget\nuget.exe" powershell -Command "$ProgressPreference = 'SilentlyContinue' ; Invoke-WebRequest https://dist.nuget.org/win-x86-commandline/latest/nuget.exe -OutFile .\.nuget\nuget.exe" -call .nuget\nuget.exe restore cppwinrt.sln" +call .nuget\nuget.exe restore cppwinrt.sln call .nuget\nuget.exe restore natvis\cppwinrtvisualizer.sln call .nuget\nuget.exe restore test\nuget\NugetTest.sln -call msbuild /m /p:Configuration=%target_configuration%,Platform=%target_platform%,CppWinRTBuildVersion=%target_version% cppwinrt.sln /t:fast_fwd +call msbuild /m /p:Configuration=%target_configuration%,Platform=%target_platform%,CppWinRTBuildVersion=%target_version%%compiler_props% cppwinrt.sln /t:fast_fwd call msbuild /p:Configuration=%target_configuration%,Platform=%target_platform%,Deployment=Component;CppWinRTBuildVersion=%target_version% natvis\cppwinrtvisualizer.sln call msbuild /p:Configuration=%target_configuration%,Platform=%target_platform%,Deployment=Standalone;CppWinRTBuildVersion=%target_version% natvis\cppwinrtvisualizer.sln if "%target_platform%"=="arm64" goto :eof -call msbuild /m /p:Configuration=%target_configuration%,Platform=%target_platform%,CppWinRTBuildVersion=%target_version% cppwinrt.sln /t:cppwinrt +call msbuild /m /p:Configuration=%target_configuration%,Platform=%target_platform%,CppWinRTBuildVersion=%target_version%%compiler_props% cppwinrt.sln /t:cppwinrt call msbuild /p:Configuration=%target_configuration%,Platform=%target_platform%,CppWinRTBuildVersion=%target_version% test\nuget\NugetTest.sln -call msbuild /m /p:Configuration=%target_configuration%,Platform=%target_platform%,CppWinRTBuildVersion=%target_version% cppwinrt.sln /t:test\test -call msbuild /m /p:Configuration=%target_configuration%,Platform=%target_platform%,CppWinRTBuildVersion=%target_version% cppwinrt.sln /t:test\test_nocoro -call msbuild /m /p:Configuration=%target_configuration%,Platform=%target_platform%,CppWinRTBuildVersion=%target_version% cppwinrt.sln /t:test\test_cpp20 -call msbuild /m /p:Configuration=%target_configuration%,Platform=%target_platform%,CppWinRTBuildVersion=%target_version% cppwinrt.sln /t:test\test_cpp20_no_sourcelocation -call msbuild /m /p:Configuration=%target_configuration%,Platform=%target_platform%,CppWinRTBuildVersion=%target_version% cppwinrt.sln /t:test\test_fast -call msbuild /m /p:Configuration=%target_configuration%,Platform=%target_platform%,CppWinRTBuildVersion=%target_version% cppwinrt.sln /t:test\test_slow -call msbuild /m /p:Configuration=%target_configuration%,Platform=%target_platform%,CppWinRTBuildVersion=%target_version% cppwinrt.sln /t:test\test_module_lock_custom -call msbuild /m /p:Configuration=%target_configuration%,Platform=%target_platform%,CppWinRTBuildVersion=%target_version% cppwinrt.sln /t:test\test_module_lock_none -call msbuild /m /p:Configuration=%target_configuration%,Platform=%target_platform%,CppWinRTBuildVersion=%target_version% cppwinrt.sln /t:test\test_module_lock_none -call msbuild /m /p:Configuration=%target_configuration%,Platform=%target_platform%,CppWinRTBuildVersion=%target_version% cppwinrt.sln /t:test\old_tests\test_old +call msbuild /m /p:Configuration=%target_configuration%,Platform=%target_platform%,CppWinRTBuildVersion=%target_version%%compiler_props% cppwinrt.sln /t:test\test +call msbuild /m /p:Configuration=%target_configuration%,Platform=%target_platform%,CppWinRTBuildVersion=%target_version%%compiler_props% cppwinrt.sln /t:test\test_nocoro +call msbuild /m /p:Configuration=%target_configuration%,Platform=%target_platform%,CppWinRTBuildVersion=%target_version%%compiler_props% cppwinrt.sln /t:test\test_cpp20 +call msbuild /m /p:Configuration=%target_configuration%,Platform=%target_platform%,CppWinRTBuildVersion=%target_version%%compiler_props% cppwinrt.sln /t:test\test_cpp20_no_sourcelocation +call msbuild /m /p:Configuration=%target_configuration%,Platform=%target_platform%,CppWinRTBuildVersion=%target_version%%compiler_props% cppwinrt.sln /t:test\test_fast +call msbuild /m /p:Configuration=%target_configuration%,Platform=%target_platform%,CppWinRTBuildVersion=%target_version%%compiler_props% cppwinrt.sln /t:test\test_slow +call msbuild /m /p:Configuration=%target_configuration%,Platform=%target_platform%,CppWinRTBuildVersion=%target_version%%compiler_props% cppwinrt.sln /t:test\test_module_lock_custom +call msbuild /m /p:Configuration=%target_configuration%,Platform=%target_platform%,CppWinRTBuildVersion=%target_version%%compiler_props% cppwinrt.sln /t:test\test_module_lock_none +call msbuild /m /p:Configuration=%target_configuration%,Platform=%target_platform%,CppWinRTBuildVersion=%target_version%%compiler_props% cppwinrt.sln /t:test\test_module_lock_none +call msbuild /m /p:Configuration=%target_configuration%,Platform=%target_platform%,CppWinRTBuildVersion=%target_version%%compiler_props% cppwinrt.sln /t:test\old_tests\test_old call run_tests.cmd %target_platform% %target_configuration% From 232174149b9c9da39fd3abfcd76bb1a1b6ef5fa9 Mon Sep 17 00:00:00 2001 From: Jon Wiswall Date: Fri, 17 Jul 2026 13:36:32 -0700 Subject: [PATCH 12/12] Guard _hs test's literals using-directive for non-conforming NTTP compilers hstring_literal.cpp had an unconditional using-directive for winrt::literals, but that namespace and its operator ""_hs only exist under __cpp_nontype_template_args >= 201911L. clang-cl reports 201411L (no class-type NTTP), so the namespace is absent and the using-directive failed to compile ("expected namespace name"), breaking the clang-cl test_cpp20 leg. Move the using-directive inside the same feature guard the rest of the test already uses. Verified: full CI test-project set builds clean on both MSVC v145 and clang-cl, x64 Debug. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- test/test_cpp20/hstring_literal.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/test_cpp20/hstring_literal.cpp b/test/test_cpp20/hstring_literal.cpp index 0e92854c8..2ba0f3ad7 100644 --- a/test/test_cpp20/hstring_literal.cpp +++ b/test/test_cpp20/hstring_literal.cpp @@ -1,11 +1,12 @@ #include "pch.h" using namespace winrt; -using namespace winrt::literals; using namespace std::literals; #if defined(__cpp_nontype_template_args) && __cpp_nontype_template_args >= 201911L +using namespace winrt::literals; + namespace { // Exercises the hstring_reference -> param::hstring conversion that projected