diff --git a/include/wil/cppwinrt_helpers.h b/include/wil/cppwinrt_helpers.h index d3c09341..0140fd96 100644 --- a/include/wil/cppwinrt_helpers.h +++ b/include/wil/cppwinrt_helpers.h @@ -288,6 +288,140 @@ namespace details return {}; } } + + // Number of elements to prefetch per GetMany call. Aim for ~2KB blocks, clamped to [1, 128]. + constexpr uint32_t batched_block_size(size_t element_size) noexcept + { + return static_cast(std::clamp(size_t{2048} / (element_size ? element_size : 1), 1, 128)); + } + + // Refills a block from an IIterator via GetMany. The iterator carries its own cursor, so each + // call just pulls the next run of elements; a short (or empty) block means exhaustion. + template + struct batched_iterator_source + { + using value_type = decltype(std::declval().Current()); + + uint32_t fill(winrt::array_view block) + { + return m_iterator.GetMany(block); + } + + Iterator m_iterator{}; + }; + + // Refills a block from an indexed (GetAt-capable) collection via GetMany, tracking the running + // start index. A short (or empty) block means exhaustion, so no end-index or Size() is needed. + template + struct batched_indexed_source + { + using value_type = decltype(std::declval().GetAt(0)); + + uint32_t fill(winrt::array_view block) + { + uint32_t const fetched = m_collection.GetMany(m_start, block); + m_start += fetched; + return fetched; + } + + Collection m_collection{}; + uint32_t m_start{0}; + }; + + // Single-pass input iterator that batches a source via GetMany into a small buffer and yields + // from it, so range-for crosses the ABI once per block instead of once per element. The two + // shapes (IIterator vs indexed collection) differ only in how a block is refilled, which the + // Source policy supplies. A block shorter than the buffer signals the last block; a full block + // is followed by one more refill (which returns empty at the boundary), matching to_vector. + template + struct batched_iterator + { + using value_type = typename Source::value_type; + 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 uint32_t buffer_capacity = batched_block_size(sizeof(value_type)); + + batched_iterator() = default; // default-constructed is the end sentinel (m_size == 0) + + explicit batched_iterator(Source source) : m_source(std::move(source)) + { + fill(); + } + + reference operator*() const noexcept + { + return m_buffer[m_index]; + } + + pointer operator->() const noexcept + { + return std::addressof(m_buffer[m_index]); + } + + batched_iterator& operator++() + { + if (++m_index == m_size) + { + // A full block might have more behind it; a short block was the last one. + if (m_size == buffer_capacity) + { + fill(); + } + else + { + m_size = 0; + } + } + + return *this; + } + + bool operator==(batched_iterator const& other) const noexcept + { + return (m_size == 0) && (other.m_size == 0); + } + + bool operator!=(batched_iterator const& other) const noexcept + { + return !(*this == other); + } + + private: + void fill() + { + m_index = 0; + m_size = m_source.fill(m_buffer); + } + + Source m_source{}; + std::array m_buffer{}; + uint32_t m_size{0}; + uint32_t m_index{0}; + }; + + template + struct batched_view + { + explicit batched_view(Source source) : m_source(std::move(source)) + { + } + + batched_iterator begin() + { + return batched_iterator{std::move(m_source)}; + } + + batched_iterator end() const noexcept + { + return {}; + } + + private: + Source m_source; + }; } // namespace details /// @endcond @@ -349,9 +483,218 @@ auto to_vector(TSrc const& src) return to_vector(src.First()); } } + +/** Adapts a C++/WinRT collection for range-for so that elements are prefetched in blocks via +GetMany instead of one ABI round-trip per element. On a cross-process or heavily-marshaled +collection the per-element crossings are the dominant cost, so batching them cuts that cost to +roughly one crossing per block. +@code +winrt::IVector collection = GetCollection(); +for (winrt::hstring const& item : wil::batched_range(collection)) +{ + // use item +} +@endcode +Works for IVector, IVectorView, IIterable, IIterator, and any type or interface that +C++/WinRT projects those interfaces for (PropertySet, IMap, etc.). Indexed collections (those +exposing GetAt) prefetch blocks with GetMany(index, ...); iterable-only collections buffer through +IIterator::GetMany. Either way the block prefetch stops once GetMany returns a short block. + +The traversal is single-pass and buffering: a yielded element outlives the step that produced it, +matching the observable behavior of wil::to_vector(collection). The returned range and its +iterators keep the collection alive for the duration of the loop. +*/ +template +auto batched_range(TSrc src) +{ + if constexpr (details::is_winrt_vector_like::value) + { + return details::batched_view>{{std::move(src), 0}}; + } + else if constexpr (details::is_winrt_iterator_like::value) + { + return details::batched_view>{{std::move(src)}}; + } + else + { + using Iterator = decltype(src.First()); + return details::batched_view>{{src.First()}}; + } +} } // namespace wil #endif +#if (defined(WINRT_Windows_Foundation_H) && !defined(__WIL_CPPWINRT_WINDOWS_FOUNDATION_HELPERS)) || defined(WIL_DOXYGEN) +#define __WIL_CPPWINRT_WINDOWS_FOUNDATION_HELPERS +namespace wil +{ +/// @cond +namespace details +{ + // Shared base for a ready-made, already-settled async object. It fires the Completed handler + // inline (no coroutine frame, no mutex) and exposes the IAsyncInfo surface. A settled object is + // either Completed (m_error == S_OK) or Error (m_error is a failure HRESULT). + template + struct ready_async_base : winrt::implements + { + ready_async_base() = default; + + explicit ready_async_base(winrt::hresult error) noexcept : m_error(error) + { + } + + void Completed(CompletedHandler const& handler) + { + // Match the coroutine promise contract: Completed may be assigned at most once. + if (std::exchange(m_completed_assigned, true)) + { + throw winrt::hresult_illegal_delegate_assignment(); + } + + if (handler) + { + handler(static_cast(this)->get_strong().template as(), Status()); + } + } + + CompletedHandler Completed() const noexcept + { + return {nullptr}; + } + + uint32_t Id() const noexcept + { + return 1; + } + + winrt::Windows::Foundation::AsyncStatus Status() const noexcept + { + return (m_error < 0) ? winrt::Windows::Foundation::AsyncStatus::Error : winrt::Windows::Foundation::AsyncStatus::Completed; + } + + winrt::hresult ErrorCode() const noexcept + { + return m_error; + } + + void Cancel() const noexcept + { + } + + void Close() const noexcept + { + } + + protected: + winrt::hresult m_error{}; + + private: + bool m_completed_assigned{false}; + }; + + // Result storage initializer for the error path, which never reads the value (GetResults throws + // first). For projected WinRT object types (IInspectable/IUnknown-derived) a null handle avoids + // default-activating the runtimeclass; other types (scalars, WinRT structs, hstring) get a cheap + // value-init. Mirrors wil::details::empty, redefined here because that lives under the + // Collections guard while this block only requires Windows.Foundation. + template + T ready_empty_result() noexcept + { + if constexpr (std::is_base_of_v) + { + return nullptr; + } + else + { + return T{}; + } + } + + template + struct ready_async_operation + : ready_async_base, winrt::Windows::Foundation::IAsyncOperation, winrt::Windows::Foundation::AsyncOperationCompletedHandler> + { + using base = + ready_async_base, winrt::Windows::Foundation::IAsyncOperation, winrt::Windows::Foundation::AsyncOperationCompletedHandler>; + + explicit ready_async_operation(TResult value) : m_result(std::move(value)) + { + } + + explicit ready_async_operation(winrt::hresult error) : base(error) + { + } + + TResult GetResults() + { + winrt::check_hresult(this->m_error); + return m_result; + } + + private: + TResult m_result{ready_empty_result()}; + }; + + struct ready_async_action + : ready_async_base + { + using base = + ready_async_base; + + ready_async_action() = default; + + explicit ready_async_action(winrt::hresult error) : base(error) + { + } + + void GetResults() + { + winrt::check_hresult(this->m_error); + } + }; +} // namespace details +/// @endcond + +/** Returns an IAsyncOperation already in the Completed state carrying @p value, with no +coroutine frame. `co_await`, `.get()`, and a `Completed` handler all complete synchronously. +@code +winrt::Windows::Foundation::IAsyncOperation GetCachedValue() +{ + if (m_haveValue) + { + return wil::already_complete(m_value); // no coroutine frame for the already-known answer + } + return ComputeValueAsync(); +} +@endcode +*/ +template +winrt::Windows::Foundation::IAsyncOperation> already_complete(TResult&& value) +{ + return winrt::make>>(std::forward(value)); +} + +//! Returns an IAsyncAction already in the Completed state, with no coroutine frame. +inline winrt::Windows::Foundation::IAsyncAction already_complete() +{ + return winrt::make(); +} + +//! Returns an IAsyncAction already in the Error state carrying @p error; GetResults() throws it. +inline winrt::Windows::Foundation::IAsyncAction already_failed(winrt::hresult error) +{ + return winrt::make(error); +} + +//! Returns an IAsyncOperation already in the Error state carrying @p error; GetResults() throws it. +template +winrt::Windows::Foundation::IAsyncOperation already_failed(winrt::hresult error) +{ + return winrt::make>(error); +} +} // namespace wil +#endif // __WIL_CPPWINRT_WINDOWS_FOUNDATION_HELPERS + #if (defined(WINRT_Windows_UI_H) && defined(_WINDOWS_UI_INTEROP_H_) && !defined(__WIL_CPPWINRT_WINDOWS_UI_INTEROP_HELPERS)) || \ defined(WIL_DOXYGEN) /// @cond diff --git a/tests/CppWinRTTests.cpp b/tests/CppWinRTTests.cpp index 0db4e491..7ce50ecd 100644 --- a/tests/CppWinRTTests.cpp +++ b/tests/CppWinRTTests.cpp @@ -775,4 +775,232 @@ TEST_CASE("CppWinRTTests::ZStringViewFromHString", "[cppwinrt]") { winrt::hstring hstr = L"Hello"; REQUIRE(wil::zwstring_view(hstr) == hstr); -} \ No newline at end of file +} +TEST_CASE("CppWinRTTests::BatchedRangeAdapter", "[cppwinrt]") +{ + using namespace winrt::Windows::Foundation::Collections; + + // Indexed collection spanning multiple GetMany blocks (int32 block is 128). + { + std::vector expected; + for (int32_t i = 0; i < 300; ++i) + { + expected.push_back(i); + } + + auto vec = winrt::single_threaded_vector(std::vector(expected)); + + std::vector observed; + for (auto&& value : wil::batched_range(vec)) + { + observed.push_back(value); + } + REQUIRE(observed == expected); + + // The read-only view goes through the same indexed path. + observed.clear(); + for (auto&& value : wil::batched_range(vec.GetView())) + { + observed.push_back(value); + } + REQUIRE(observed == expected); + } + + // Exactly one element beyond a single block boundary. + { + auto vec = winrt::single_threaded_vector(std::vector(129, 7)); + uint32_t count = 0; + for (auto&& value : wil::batched_range(vec)) + { + REQUIRE(value == 7); + ++count; + } + REQUIRE(count == 129); + } + + // Empty collection yields nothing. + { + auto vec = winrt::single_threaded_vector(); + uint32_t count = 0; + for (auto&& value : wil::batched_range(vec)) + { + (void)value; + ++count; + } + REQUIRE(count == 0); + } + + // Iterable-only path (IIterable has no GetAt) buffers through IIterator::GetMany. + { + std::vector expected = {L"a", L"b", L"c"}; + IIterable iterable = winrt::single_threaded_vector(std::vector(expected)); + + std::vector observed; + for (auto&& value : wil::batched_range(iterable)) + { + observed.push_back(value); + } + REQUIRE(observed == expected); + } + + // Directly batching an iterator yields its current position onward. + { + auto vec = winrt::single_threaded_vector({1, 2, 3, 4, 5}); + std::vector observed; + for (auto&& value : wil::batched_range(vec.First())) + { + observed.push_back(value); + } + REQUIRE(observed == std::vector({1, 2, 3, 4, 5})); + } + + // Map batches over IKeyValuePair through the iterable path. + { + std::map src{{L"kittens", L"fluffy"}, {L"puppies", L"cute"}}; + auto map = winrt::single_threaded_map(std::map(src)); + uint32_t count = 0; + for (auto&& pair : wil::batched_range(map)) + { + REQUIRE(pair.Value() == src.at(pair.Key())); + ++count; + } + REQUIRE(count == src.size()); + } + + // Non-WinRT indexed shape works too, matching to_vector's duck typing. + { + uint32_t count = 0; + for (auto&& value : wil::batched_range(vector_like{})) + { + REQUIRE(value == vector_like{}.GetAt(0)); + ++count; + } + REQUIRE(count == vector_like{}.Size()); + } + + // Block-boundary edge cases. For int32 the prefetch block is 128, so exercise exactly one and + // exactly two full blocks: the "full block implies maybe-more" rule must fetch the trailing + // empty block and terminate cleanly -- no infinite loop, no dropped or duplicated element. + for (int32_t total : {1, 127, 128, 129, 256, 257}) + { + std::vector expected; + for (int32_t i = 0; i < total; ++i) + { + expected.push_back(i); + } + + // Indexed path. + auto vec = winrt::single_threaded_vector(std::vector(expected)); + std::vector observed; + for (auto&& value : wil::batched_range(vec)) + { + observed.push_back(value); + } + REQUIRE(observed == expected); // exact count and in-order, so no skip/dup across seams + + // Iterable-only path exercises the same boundary through IIterator::GetMany. + IIterable iterable = winrt::single_threaded_vector(std::vector(expected)); + observed.clear(); + for (auto&& value : wil::batched_range(iterable)) + { + observed.push_back(value); + } + REQUIRE(observed == expected); + } + + // Batching an iterator already advanced past its start yields only the remainder, matching + // to_vector's "current position and everything after it" contract (no re-anchor to index 0). + { + auto vec = winrt::single_threaded_vector({10, 20, 30, 40}); + auto it = vec.First(); + REQUIRE(it.Current() == 10); + it.MoveNext(); // now positioned at 20 + + std::vector observed; + for (auto&& value : wil::batched_range(it)) + { + observed.push_back(value); + } + REQUIRE(observed == std::vector({20, 30, 40})); + } +} + +TEST_CASE("CppWinRTTests::MakeReady", "[cppwinrt]") +{ + using namespace winrt; + using namespace winrt::Windows::Foundation; + + // Completed synchronously with a value, with no coroutine frame. + { + IAsyncOperation op = wil::already_complete(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 wil::already_complete(7); + }; + REQUIRE(coro().get() == 7); + } + + // A Completed handler on an already-completed operation fires immediately. + { + auto op = wil::already_complete(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 = wil::already_complete(1); + op.Completed([](auto&&, auto&&) {}); + REQUIRE_THROWS_AS(op.Completed([](auto&&, auto&&) {}), hresult_illegal_delegate_assignment); + } + + // Action variant carries no result. + { + IAsyncAction action = wil::already_complete(); + REQUIRE(action.Status() == AsyncStatus::Completed); + action.get(); + } + + // A non-trivial result type round-trips. + { + auto op = wil::already_complete(hstring{L"ready"}); + REQUIRE(op.get() == L"ready"); + } + + // Failed action: Error status, GetResults/get throw the carried HRESULT. + { + IAsyncAction action = wil::already_failed(E_ACCESSDENIED); + REQUIRE(action.Status() == AsyncStatus::Error); + REQUIRE(action.ErrorCode() == E_ACCESSDENIED); + REQUIRE_THROWS_AS(action.get(), hresult_access_denied); + } + + // Failed operation: GetResults throws the carried HRESULT. + { + auto op = wil::already_failed(E_INVALIDARG); + REQUIRE(op.Status() == AsyncStatus::Error); + REQUIRE_THROWS_AS(op.GetResults(), hresult_invalid_argument); + } + + // Failed operation whose result is a projected runtimeclass with no default constructor: the + // result storage must be null-initialized (Uri{nullptr}), never activated. A plain value-init + // here would fail to compile, and for default-activatable classes would needlessly activate. + { + auto op = wil::already_failed(E_FAIL); + REQUIRE(op.Status() == AsyncStatus::Error); + REQUIRE_THROWS(op.GetResults()); + } +}