Skip to content
343 changes: 343 additions & 0 deletions include/wil/cppwinrt_helpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,140 @@ namespace details
return {};
}
}

// Number of elements to prefetch per GetMany call. Aim for ~2KB blocks, clamped to [1, 128].

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Curious about the choice of 128 since it' not documented here. Is it just "collections are rarely this big"?

The comment says this "aims" for 2 KiB blocks, however 128 pointers - which is probably the typical case - is 1 KiB

constexpr uint32_t batched_block_size(size_t element_size) noexcept
{
return static_cast<uint32_t>(std::clamp<size_t>(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 <typename Iterator>
struct batched_iterator_source
{
using value_type = decltype(std::declval<Iterator>().Current());

uint32_t fill(winrt::array_view<value_type> 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 <typename Collection>
struct batched_indexed_source
{
using value_type = decltype(std::declval<Collection>().GetAt(0));

uint32_t fill(winrt::array_view<value_type> 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 <typename Source>
struct batched_iterator
{
using value_type = typename Source::value_type;
using iterator_category = std::input_iterator_tag;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Input iterators require *it++ to be well formed and be equivalent to:

auto __val = *it;
++it;

This type only provides pre-increment and therefore can potentially fail to compile, e.g. when calling STL algorithms. Adding pre-increment is probably not the right call since a copy of this iterator type is such a heavy operation (another property that's very unusual for iterators).

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];

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Some asserts would be nice. E.g. WI_ASSERT(m_index < m_size)

}

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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This would break self-equality (i.e. a == a) for non-end iterators. Probably never an issue in practice (realistically someone would be comparing copies) especially given that this is an input iterator, but is enough to cause a feeling of uneasiness.

}

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<value_type, buffer_capacity> m_buffer{};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

So, the end iterator also holds a potentially large array? 😬. One possible option would be to use a separate sentinel type, however these types would no longer be usable with the old STL algorithms (but would work with the ranges library).

Similarly, this would also be problematic when calling STL or STL-like algorithms, which take iterator arguments by value as iterators are typically assumed to be cheap to copy.

uint32_t m_size{0};
uint32_t m_index{0};
};

template <typename Source>
struct batched_view
{
explicit batched_view(Source source) : m_source(std::move(source))
{
}

batched_iterator<Source> begin()
{
return batched_iterator<Source>{std::move(m_source)};
}

batched_iterator<Source> end() const noexcept
{
return {};
}

private:
Source m_source;
};
} // namespace details
/// @endcond

Expand Down Expand Up @@ -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<winrt::hstring> collection = GetCollection();
for (winrt::hstring const& item : wil::batched_range(collection))
{
// use item
}
@endcode
Works for IVector<T>, IVectorView<T>, IIterable<T>, IIterator<T>, and any type or interface that
C++/WinRT projects those interfaces for (PropertySet, IMap<K,V>, 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 <typename TSrc>
auto batched_range(TSrc src)
{
if constexpr (details::is_winrt_vector_like<TSrc>::value)
{
return details::batched_view<details::batched_indexed_source<TSrc>>{{std::move(src), 0}};
}
else if constexpr (details::is_winrt_iterator_like<TSrc>::value)
{
return details::batched_view<details::batched_iterator_source<TSrc>>{{std::move(src)}};
}
else
{
using Iterator = decltype(src.First());
return details::batched_view<details::batched_iterator_source<Iterator>>{{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 <typename Derived, typename AsyncInterface, typename CompletedHandler>
struct ready_async_base : winrt::implements<Derived, AsyncInterface, winrt::Windows::Foundation::IAsyncInfo>
{
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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: this is a WinRT contract, not a coroutine contract

if (std::exchange(m_completed_assigned, true))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

std::exchange is not an atomic operation. In practice that's probably fine, especially since this type doesn't store the object, but another place that gives the feeling of uneasiness.

{
throw winrt::hresult_illegal_delegate_assignment();
}

if (handler)
{
handler(static_cast<Derived*>(this)->get_strong().template as<AsyncInterface>(), Status());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It's been a while since I used C++/WinRT extensively, so this might be wrong, but this appears to be:

  1. Performing an AddRef on the derived object, and then
  2. Performing a QueryInterface on the derived object

I can see an argument for increasing the reference count once (the caller may free its only reference in the callback), but two sems unnecessary.

}
}

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<T>, redefined here because that lives under the
// Collections guard while this block only requires Windows.Foundation.
Comment on lines +598 to +599

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Perhaps invert the two? Define empty<T> here and have the collections code use this definition.

template <typename T>
T ready_empty_result() noexcept
{
if constexpr (std::is_base_of_v<winrt::Windows::Foundation::IUnknown, T>)
{
return nullptr;
}
else
{
return T{};
}
}

template <typename TResult>
struct ready_async_operation
: ready_async_base<ready_async_operation<TResult>, winrt::Windows::Foundation::IAsyncOperation<TResult>, winrt::Windows::Foundation::AsyncOperationCompletedHandler<TResult>>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Maybe I'm just being dumb, but is there a reason these derive from the completed handler delegate types?

{
using base =
ready_async_base<ready_async_operation<TResult>, winrt::Windows::Foundation::IAsyncOperation<TResult>, winrt::Windows::Foundation::AsyncOperationCompletedHandler<TResult>>;

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<TResult>()};
};

struct ready_async_action
: ready_async_base<ready_async_action, winrt::Windows::Foundation::IAsyncAction, winrt::Windows::Foundation::AsyncActionCompletedHandler>
{
using base =
ready_async_base<ready_async_action, winrt::Windows::Foundation::IAsyncAction, winrt::Windows::Foundation::AsyncActionCompletedHandler>;

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<T> 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<int32_t> GetCachedValue()
{
if (m_haveValue)
{
return wil::already_complete(m_value); // no coroutine frame for the already-known answer
}
return ComputeValueAsync();
}
@endcode
*/
template <typename TResult>
winrt::Windows::Foundation::IAsyncOperation<std::decay_t<TResult>> already_complete(TResult&& value)
{
return winrt::make<details::ready_async_operation<std::decay_t<TResult>>>(std::forward<TResult>(value));
}

//! Returns an IAsyncAction already in the Completed state, with no coroutine frame.
inline winrt::Windows::Foundation::IAsyncAction already_complete()
{
return winrt::make<details::ready_async_action>();
}

//! 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<details::ready_async_action>(error);
}

//! Returns an IAsyncOperation<T> already in the Error state carrying @p error; GetResults() throws it.
template <typename TResult>
winrt::Windows::Foundation::IAsyncOperation<TResult> already_failed(winrt::hresult error)
{
return winrt::make<details::ready_async_operation<TResult>>(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
Expand Down
Loading
Loading