From a535d38f083e6a428b64ac47e3144653c9739bc5 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Jul 2026 12:38:40 -0700 Subject: [PATCH 1/7] Fix C4819 warning: replace non-ASCII em dash in base_macros.h comment (#1606) * Initial plan * Fix C4819 warning: replace non-ASCII em dash with ASCII hyphen in base_macros.h comment --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- strings/base_macros.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/strings/base_macros.h b/strings/base_macros.h index 91b0121d9..850d0c627 100644 --- a/strings/base_macros.h +++ b/strings/base_macros.h @@ -44,7 +44,7 @@ // Template specializations in namespace std (hash, coroutine_traits) need extern "C++" // linkage in module builds for proper merging with the std module, but must NOT be -// exported — exporting namespace std would make all of std transitively visible. +// exported - exporting namespace std would make all of std transitively visible. #ifndef WINRT_IMPL_STD_EXPORT #ifdef WINRT_IMPL_BUILD_MODULE #define WINRT_IMPL_STD_EXPORT extern "C++" From d3d92d70e3e089307ab6d6deb5ed39c7a534fec2 Mon Sep 17 00:00:00 2001 From: Raymond Chen Date: Mon, 27 Jul 2026 09:39:55 -0700 Subject: [PATCH 2/7] Fix race condition in cancellation setup/teardown (#1609) The code failed to handle three cases. 1. A cancellation is in progress when a cancellable awaitable begins. 2. A cancellation is in progress when a cancellable awaitable ends. 3. A cancellation is in progress when a cancel() request is made. The m_canceller member has one of these three values: * nullptr, meaning that there is nothing to cancel. It has this value when the coroutine is not awaiting, or if it is awaiting something that cannot be cancelled. * cancelling_ptr, meaning that another thread (not the coroutine thread) is in the middle of cancellation request. * function pointer, representing the function to call to cancel the await. In case 1, we should not overwrite the canceling_ptr with the function pointer, because only the code doing the cancel() can transition into/out of cancelling_ptr. In case 2, we intended to spin until the m_canceller is no longer cancelling_ptr, but we used m_canceller.exchange(nullptr) in a loop, which means that if m_canceller was cancelling_ptr, we overwrite it with nullptr. As a result, the "while" loop always exits after one iteration. We need to spin on the m_canceller without modifying it if it is cancelling_ptr. In case 3, cancel() function resets m_cancelling back to nullptr, even if the value was cancelling_ptr on entry, prematurely declaring that the existing cancel() has completed. If the original value was cancelling_ptr, we should leave it that way. There are still other cases not handled: * Coroutine already cancelled when a co_await starts. In this case, we never call the canceller, so the coroutine fails to propagate cancellation. This will require a broader fix, so I'm not going to fix it in this PR. This PR is primarily about fixing the crash caused by case 2. Cases 1 and 3 were fixed opportunistically. --- strings/base_coroutine_threadpool.h | 34 +++++++++++++++++++---------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/strings/base_coroutine_threadpool.h b/strings/base_coroutine_threadpool.h index 057d5b548..c901b94ba 100644 --- a/strings/base_coroutine_threadpool.h +++ b/strings/base_coroutine_threadpool.h @@ -146,32 +146,42 @@ WINRT_EXPORT namespace winrt void set_canceller(canceller_t canceller, void* context) { m_context = context; - m_canceller.store(canceller, std::memory_order_release); + canceller_t expected = nullptr; + m_canceller.compare_exchange_strong(expected, canceller, std::memory_order_release, std::memory_order_relaxed); } void revoke_canceller() { - while (m_canceller.exchange(nullptr, std::memory_order_acquire) == cancelling_ptr) + auto existing = m_canceller.load(std::memory_order_relaxed); + do { - std::this_thread::yield(); + while (existing == cancelling_ptr) + { + std::this_thread::yield(); + existing = m_canceller.load(std::memory_order_relaxed); + } } + while (!m_canceller.compare_exchange_weak(existing, nullptr, std::memory_order_acquire, std::memory_order_relaxed)); } void cancel() { auto canceller = m_canceller.exchange(cancelling_ptr, std::memory_order_acquire); - struct unique_cancellation_lock + if (canceller != cancelling_ptr) { - cancellable_promise* promise; - ~unique_cancellation_lock() + struct unique_cancellation_lock + { + cancellable_promise* promise; + ~unique_cancellation_lock() + { + promise->m_canceller.store(nullptr, std::memory_order_release); + } + } lock{ this }; + + if (canceller != nullptr) { - promise->m_canceller.store(nullptr, std::memory_order_release); + canceller(m_context); } - } lock{ this }; - - if ((canceller != nullptr) && (canceller != cancelling_ptr)) - { - canceller(m_context); } } From d6cff316a991152071038668fb586a8b5ab1d51f Mon Sep 17 00:00:00 2001 From: Derek Morris Date: Mon, 27 Jul 2026 18:15:40 -0700 Subject: [PATCH 3/7] Add header to base_includes.h (#1612) base_types.h uses std::ratio_multiply, which is defined in . Under strict include-what-you-use rules not referencing this header can lead to weird build breaks. --- strings/base_includes.h | 1 + 1 file changed, 1 insertion(+) diff --git a/strings/base_includes.h b/strings/base_includes.h index 287a09709..bac7358bd 100644 --- a/strings/base_includes.h +++ b/strings/base_includes.h @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include From a7f0233d64620801fd281ae970fe39a2943eca34 Mon Sep 17 00:00:00 2001 From: Yexuan Xiao Date: Thu, 20 Aug 2026 01:49:04 +0800 Subject: [PATCH 4/7] Remove WINRT_EXPORT in Component.g.cpp (#1597) Co-authored-by: Ryan Shepherd --- cppwinrt/code_writers.h | 11 +++++++++++ cppwinrt/component_writers.h | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/cppwinrt/code_writers.h b/cppwinrt/code_writers.h index ab119ae89..b3855c097 100644 --- a/cppwinrt/code_writers.h +++ b/cppwinrt/code_writers.h @@ -226,6 +226,17 @@ namespace cppwinrt return { w, write_close_namespace }; } + [[nodiscard]] static finish_with wrap_type_namespace_without_export(writer& w, std::string_view const& ns) + { + auto format = R"(namespace winrt::@ +{ +)"; + + w.write(format, ns); + + return { w, write_close_namespace }; + } + static void write_enum_field(writer& w, Field const& field) { auto format = R"( % = %, diff --git a/cppwinrt/component_writers.h b/cppwinrt/component_writers.h index af5626d14..94e20e53e 100644 --- a/cppwinrt/component_writers.h +++ b/cppwinrt/component_writers.h @@ -400,7 +400,7 @@ catch (...) { return winrt::to_hresult(); } return; } - auto wrap_type = wrap_type_namespace(w, type_namespace); + auto wrap_type = wrap_type_namespace_without_export(w, type_namespace); for (auto&&[factory_name, factory] : get_factories(w, type)) { From 74a13c5617c04ddc8ae42e087cfb1dd1617bcfaa Mon Sep 17 00:00:00 2001 From: Jon Wiswall Date: Mon, 24 Aug 2026 14:51:11 -0700 Subject: [PATCH 5/7] C++/WinRT ABI interop improvements (#1608) * 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 * 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 * 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 * 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 * 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 * 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 * 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> * 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> * 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. * 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> * 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> * 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> * Remove buffered-iterator and ready-async helpers Drop the batched range-for iterator (base_iterator.h) and the already-completed make_ready/ready_async async helpers (base_coroutine_foundation.h), reverting both headers to their base state. These will be reintroduced in wil/cppwinrt.h. Removes the buffered_iterator.cpp and make_ready.cpp tests accordingly. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Simplify reference_base_t and test in-proc agility Collapse reference_base_t to a single implements<> whose trailing marker is conditional (non_agile for stock scalars, an inert marker placeholder otherwise), instead of duplicating the interface list across a conditional_t. implements<> ignores non-interface, non-marker type parameters, so the placeholder is a no-op. Add a test that boxes a scalar in one STA and fetches it from another via the Global Interface Table, asserting the object identity is preserved - proving the IAgileObject in-proc fast path. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Box arrays locally and address review feedback Add an in-process reference_array (IReferenceArray + IPropertyValue), the array counterpart to reference, so box_value/unbox_value of the stock array element types no longer builds a combase PropertyValue. hstring, TimeSpan, DateTime (projected type != ABI) and IInspectable/Size/Rect stay on combase. Collapse every IPropertyValue getter on both types into one internal get_as() that holds the constexpr type check and throw, and share a scalar_property_type()/array_property_type() helper. Also fold in the earlier review fixes: single implements<> with a conditional trailing marker for reference_base_t, GetSize/GetRect fast paths, an _hs null-termination static_assert, and drop the duplicate test_module_lock_none build in build_test_all.cmd. Extend reference_boxing.cpp with array boxing, array marshal-by-value, and cross-STA agile identity coverage. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Share a producer base, drop comments and build-script changes Fold the duplicated IPropertyValue getters and IMarshal tearoff shared by reference and reference_array into a reference_producer CRTP base; the two leaves just supply storage, get_as, and create_property_value. Same codegen and per-instance footprint as the two-template version, ~80 fewer source lines. Per review feedback, strip the newly-added implementation comments from base_reference_produce.h and base_string.h, and revert the build_test_all.cmd compiler-selection helper and its README note so this PR carries only the projection/boxing functionality. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: Jon Wiswall <18537118+jonwis@users.noreply.github.com> Copilot-Session: 83914e59-f7cd-4284-ad4b-4cb7b79f28c1 --- strings/base_reference_produce.h | 423 ++++++++++++++++------------ strings/base_string.h | 80 ++++++ strings/base_string_input.h | 4 + test/test/reference_boxing.cpp | 268 ++++++++++++++++++ test/test/test.vcxproj | 1 + test/test_cpp20/hstring_literal.cpp | 68 +++++ test/test_cpp20/test_cpp20.vcxproj | 1 + 7 files changed, 660 insertions(+), 185 deletions(-) create mode 100644 test/test/reference_boxing.cpp create mode 100644 test/test_cpp20/hstring_literal.cpp diff --git a/strings/base_reference_produce.h b/strings/base_reference_produce.h index abffb3384..db9639cc3 100644 --- a/strings/base_reference_produce.h +++ b/strings/base_reference_produce.h @@ -2,99 +2,197 @@ WINRT_EXPORT namespace winrt::impl { template - struct reference : implements, Windows::Foundation::IReference, Windows::Foundation::IPropertyValue> + struct reference; + + template + struct reference_array; + + 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 || std::is_same_v || + std::is_same_v || std::is_same_v; + + template + constexpr Windows::Foundation::PropertyType scalar_property_type() noexcept + { + 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::Inspectable; } + 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 if constexpr (std::is_same_v) { return pt::Size; } + else if constexpr (std::is_same_v) { return pt::Rect; } + else { return pt::OtherType; } + } + + template + constexpr Windows::Foundation::PropertyType array_property_type() noexcept { - reference(T const& value) : m_value(value) - { - } + return static_cast( + static_cast(scalar_property_type()) + 1024); + } - T Value() const - { - return m_value; - } + template + inline constexpr bool is_numeric_scalar_v = + (std::is_arithmetic_v && !std::is_same_v && !std::is_same_v) || std::is_enum_v; + template + struct reference_producer : implements, non_agile, marker>> + { Windows::Foundation::PropertyType Type() const noexcept { - return Windows::Foundation::PropertyType::OtherType; + if constexpr (IsArray) { return array_property_type(); } + else { return scalar_property_type(); } } static constexpr bool IsNumericScalar() noexcept { - return std::is_arithmetic_v || std::is_enum_v; + return !IsArray && is_numeric_scalar_v; } - std::uint8_t GetUInt8() const - { - return to_scalar(); - } + std::uint8_t GetUInt8() const { return derived()->template get_as(); } + std::int16_t GetInt16() const { return derived()->template get_as(); } + std::uint16_t GetUInt16() const { return derived()->template get_as(); } + std::int32_t GetInt32() const { return derived()->template get_as(); } + std::uint32_t GetUInt32() const { return derived()->template get_as(); } + std::int64_t GetInt64() const { return derived()->template get_as(); } + std::uint64_t GetUInt64() const { return derived()->template get_as(); } + float GetSingle() const { return derived()->template get_as(); } + double GetDouble() const { return derived()->template get_as(); } + char16_t GetChar16() const { return derived()->template get_as(); } + bool GetBoolean() const { return derived()->template get_as(); } + hstring GetString() const { return derived()->template get_as(); } + guid GetGuid() const { return derived()->template get_as(); } + Windows::Foundation::DateTime GetDateTime() const { return derived()->template get_as(); } + Windows::Foundation::TimeSpan GetTimeSpan() const { return derived()->template get_as(); } + Windows::Foundation::Point GetPoint() const { return derived()->template get_as(); } + Windows::Foundation::Size GetSize() const { return derived()->template get_as(); } + Windows::Foundation::Rect GetRect() const { return derived()->template get_as(); } + void GetUInt8Array(com_array& value) const { derived()->get_as(value); } + void GetInt16Array(com_array& value) const { derived()->get_as(value); } + void GetUInt16Array(com_array& value) const { derived()->get_as(value); } + void GetInt32Array(com_array& value) const { derived()->get_as(value); } + void GetUInt32Array(com_array& value) const { derived()->get_as(value); } + void GetInt64Array(com_array& value) const { derived()->get_as(value); } + void GetUInt64Array(com_array& value) const { derived()->get_as(value); } + void GetSingleArray(com_array& value) const { derived()->get_as(value); } + void GetDoubleArray(com_array& value) const { derived()->get_as(value); } + void GetChar16Array(com_array& value) const { derived()->get_as(value); } + void GetBooleanArray(com_array& value) const { derived()->get_as(value); } + void GetStringArray(com_array& value) const { derived()->get_as(value); } + void GetInspectableArray(com_array& value) const { derived()->get_as(value); } + void GetGuidArray(com_array& value) const { derived()->get_as(value); } + void GetDateTimeArray(com_array& value) const { derived()->get_as(value); } + void GetTimeSpanArray(com_array& value) const { derived()->get_as(value); } + void GetPointArray(com_array& value) const { derived()->get_as(value); } + void GetSizeArray(com_array& value) const { derived()->get_as(value); } + void GetRectArray(com_array& value) const { derived()->get_as(value); } - std::int16_t GetInt16() const - { - return to_scalar(); - } + private: - std::uint16_t GetUInt16() const - { - return to_scalar(); - } + Derived const* derived() const noexcept { return static_cast(this); } - std::int32_t GetInt32() const + std::int32_t query_interface_tearoff(guid const& id, void** object) const noexcept override { - return to_scalar(); - } + if constexpr (is_stock_reference_v) + { + if (is_guid_of(id)) + { + try + { + auto marshal = derived()->create_property_value().template as(); + *object = detach_abi(marshal); + return error_ok; + } + catch (...) + { + *object = nullptr; + return to_hresult(); + } + } - std::uint32_t GetUInt32() const - { - return to_scalar(); + if (is_guid_of(id)) + { + auto unknown = reinterpret_cast(to_abi(derived())); + unknown->AddRef(); + *object = unknown; + return error_ok; + } + } + + *object = nullptr; + return error_no_interface; } + }; - std::int64_t GetInt64() const + template + struct reference : reference_producer, T, Windows::Foundation::IReference, false> + { + reference(T const& value) : m_value(value) { - return to_scalar(); } - std::uint64_t GetUInt64() const + T Value() const { - return to_scalar(); + return m_value; } - 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(); } - 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::Size GetSize() { throw hresult_not_implemented(); } - Windows::Foundation::Rect GetRect() { throw hresult_not_implemented(); } - void GetUInt8Array(com_array &) { throw hresult_not_implemented(); } - void GetInt16Array(com_array &) { throw hresult_not_implemented(); } - void GetUInt16Array(com_array &) { throw hresult_not_implemented(); } - void GetInt32Array(com_array &) { throw hresult_not_implemented(); } - void GetUInt32Array(com_array &) { throw hresult_not_implemented(); } - void GetInt64Array(com_array &) { throw hresult_not_implemented(); } - void GetUInt64Array(com_array &) { throw hresult_not_implemented(); } - void GetSingleArray(com_array &) { throw hresult_not_implemented(); } - void GetDoubleArray(com_array &) { throw hresult_not_implemented(); } - void GetChar16Array(com_array &) { throw hresult_not_implemented(); } - void GetBooleanArray(com_array &) { throw hresult_not_implemented(); } - void GetStringArray(com_array &) { throw hresult_not_implemented(); } - void GetInspectableArray(com_array &) { throw hresult_not_implemented(); } - void GetGuidArray(com_array &) { throw hresult_not_implemented(); } - void GetDateTimeArray(com_array &) { throw hresult_not_implemented(); } - void GetTimeSpanArray(com_array &) { throw hresult_not_implemented(); } - void GetPointArray(com_array &) { throw hresult_not_implemented(); } - void GetSizeArray(com_array &) { throw hresult_not_implemented(); } - void GetRectArray(com_array &) { throw hresult_not_implemented(); } - private: + template friend struct reference_producer; + + 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 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; } + } + template - To to_scalar() const + To get_as() const { - if constexpr (IsNumericScalar()) + if constexpr (std::is_same_v) + { + return m_value; + } + else if constexpr (is_numeric_scalar_v && is_numeric_scalar_v) { return static_cast(m_value); } @@ -104,98 +202,81 @@ WINRT_EXPORT namespace winrt::impl } } + template + void get_as(com_array const&) const + { + throw hresult_not_implemented(); + } + T m_value; }; template - struct reference_traits - { - static auto make(T const& value) { return winrt::make>(value); } - 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 + struct reference_array : reference_producer, T, Windows::Foundation::IReferenceArray, true> { - static auto make(std::int16_t value) { return Windows::Foundation::PropertyValue::CreateInt16(value); } - using itf = Windows::Foundation::IReference; - }; + reference_array(array_view const& value) : m_value(value.begin(), value.end()) + { + } - template <> - struct reference_traits - { - static auto make(std::uint32_t value) { return Windows::Foundation::PropertyValue::CreateUInt32(value); } - using itf = Windows::Foundation::IReference; - }; + com_array Value() const + { + return com_array(m_value.begin(), m_value.end()); + } - template <> - struct reference_traits - { - static auto make(std::int32_t value) { return Windows::Foundation::PropertyValue::CreateInt32(value); } - using itf = Windows::Foundation::IReference; - }; + private: - template <> - struct reference_traits - { - static auto make(std::uint64_t value) { return Windows::Foundation::PropertyValue::CreateUInt64(value); } - using itf = Windows::Foundation::IReference; - }; + template friend struct reference_producer; - 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; - }; + Windows::Foundation::IInspectable create_property_value() const + { + using pv = Windows::Foundation::PropertyValue; + + if constexpr (std::is_same_v) { return pv::CreateUInt8Array(m_value); } + else if constexpr (std::is_same_v) { return pv::CreateInt16Array(m_value); } + else if constexpr (std::is_same_v) { return pv::CreateUInt16Array(m_value); } + else if constexpr (std::is_same_v) { return pv::CreateInt32Array(m_value); } + else if constexpr (std::is_same_v) { return pv::CreateUInt32Array(m_value); } + else if constexpr (std::is_same_v) { return pv::CreateInt64Array(m_value); } + else if constexpr (std::is_same_v) { return pv::CreateUInt64Array(m_value); } + else if constexpr (std::is_same_v) { return pv::CreateSingleArray(m_value); } + else if constexpr (std::is_same_v) { return pv::CreateDoubleArray(m_value); } + else if constexpr (std::is_same_v) { return pv::CreateChar16Array(m_value); } + else if constexpr (std::is_same_v) { return pv::CreateBooleanArray(m_value); } + else if constexpr (std::is_same_v) { return pv::CreateStringArray(m_value); } + else if constexpr (std::is_same_v) { return pv::CreateGuidArray(m_value); } + else if constexpr (std::is_same_v) { return pv::CreateDateTimeArray(m_value); } + else if constexpr (std::is_same_v) { return pv::CreateTimeSpanArray(m_value); } + else if constexpr (std::is_same_v) { return pv::CreatePointArray(m_value); } + else { return nullptr; } + } - template <> - struct reference_traits - { - static auto make(double value) { return Windows::Foundation::PropertyValue::CreateDouble(value); } - using itf = Windows::Foundation::IReference; - }; + template + To get_as() const + { + throw hresult_not_implemented(); + } - template <> - struct reference_traits - { - static auto make(char16_t value) { return Windows::Foundation::PropertyValue::CreateChar16(value); } - using itf = Windows::Foundation::IReference; - }; + template + void get_as(com_array& value) const + { + if constexpr (std::is_same_v) + { + value = com_array(m_value.begin(), m_value.end()); + } + else + { + throw hresult_not_implemented(); + } + } - template <> - struct reference_traits - { - static auto make(bool value) { return Windows::Foundation::PropertyValue::CreateBoolean(value); } - using itf = Windows::Foundation::IReference; + com_array m_value; }; - template <> - struct reference_traits + template + struct reference_traits { - static auto make(hstring const& value) { return Windows::Foundation::PropertyValue::CreateString(value); } - using itf = Windows::Foundation::IReference; + static auto make(T const& value) { return winrt::make>(value); } + using itf = Windows::Foundation::IReference; }; template <> @@ -205,41 +286,13 @@ 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; }; - 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 { @@ -257,77 +310,77 @@ WINRT_EXPORT namespace winrt::impl template <> struct reference_traits> { - static auto make(array_view const& value) { return Windows::Foundation::PropertyValue::CreateUInt8Array(value); } + static auto make(array_view const& value) { return winrt::make>(value); } using itf = Windows::Foundation::IReferenceArray; }; template <> struct reference_traits> { - static auto make(array_view const& value) { return Windows::Foundation::PropertyValue::CreateInt16Array(value); } + static auto make(array_view const& value) { return winrt::make>(value); } using itf = Windows::Foundation::IReferenceArray; }; template <> struct reference_traits> { - static auto make(array_view const& value) { return Windows::Foundation::PropertyValue::CreateUInt16Array(value); } + static auto make(array_view const& value) { return winrt::make>(value); } using itf = Windows::Foundation::IReferenceArray; }; template <> struct reference_traits> { - static auto make(array_view const& value) { return Windows::Foundation::PropertyValue::CreateInt32Array(value); } + static auto make(array_view const& value) { return winrt::make>(value); } using itf = Windows::Foundation::IReferenceArray; }; template <> struct reference_traits> { - static auto make(com_array const& value) { return Windows::Foundation::PropertyValue::CreateUInt32Array(value); } + static auto make(com_array const& value) { return winrt::make>(value); } using itf = Windows::Foundation::IReferenceArray; }; template <> struct reference_traits> { - static auto make(array_view const& value) { return Windows::Foundation::PropertyValue::CreateInt64Array(value); } + static auto make(array_view const& value) { return winrt::make>(value); } using itf = Windows::Foundation::IReferenceArray; }; template <> struct reference_traits> { - static auto make(array_view const& value) { return Windows::Foundation::PropertyValue::CreateUInt64Array(value); } + static auto make(array_view const& value) { return winrt::make>(value); } using itf = Windows::Foundation::IReferenceArray; }; template <> struct reference_traits> { - static auto make(array_view const& value) { return Windows::Foundation::PropertyValue::CreateSingleArray(value); } + static auto make(array_view const& value) { return winrt::make>(value); } using itf = Windows::Foundation::IReferenceArray; }; template <> struct reference_traits> { - static auto make(array_view const& value) { return Windows::Foundation::PropertyValue::CreateDoubleArray(value); } + static auto make(array_view const& value) { return winrt::make>(value); } using itf = Windows::Foundation::IReferenceArray; }; template <> struct reference_traits> { - static auto make(array_view const& value) { return Windows::Foundation::PropertyValue::CreateChar16Array(value); } + static auto make(array_view const& value) { return winrt::make>(value); } using itf = Windows::Foundation::IReferenceArray; }; template <> struct reference_traits> { - static auto make(array_view const& value) { return Windows::Foundation::PropertyValue::CreateBooleanArray(value); } + static auto make(array_view const& value) { return winrt::make>(value); } using itf = Windows::Foundation::IReferenceArray; }; @@ -348,14 +401,14 @@ WINRT_EXPORT namespace winrt::impl template <> struct reference_traits> { - static auto make(array_view const& value) { return Windows::Foundation::PropertyValue::CreateGuidArray(value); } + static auto make(array_view const& value) { return winrt::make>(value); } using itf = Windows::Foundation::IReferenceArray; }; template <> struct reference_traits> { - static auto make(array_view const& value) { return Windows::Foundation::PropertyValue::CreateGuidArray(reinterpret_cast const&>(value)); } + static auto make(array_view const& value) { return winrt::make>(reinterpret_cast const&>(value)); } using itf = Windows::Foundation::IReferenceArray; }; @@ -376,7 +429,7 @@ WINRT_EXPORT namespace winrt::impl template <> struct reference_traits> { - static auto make(array_view const& value) { return Windows::Foundation::PropertyValue::CreatePointArray(value); } + static auto make(array_view const& value) { return winrt::make>(value); } using itf = Windows::Foundation::IReferenceArray; }; diff --git a/strings/base_string.h b/strings/base_string.h index 6b1fb37b5..50a248c02 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 + { + constexpr hstring_reference() noexcept = default; + + constexpr explicit hstring_reference(impl::hstring_header const* header) noexcept : + m_handle(const_cast(header)) + { + } + + operator hstring const&() const noexcept + { + return *reinterpret_cast(this); + } + + private: + + [[maybe_unused]] 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,44 @@ WINRT_EXPORT namespace winrt } } +#if defined(__cpp_nontype_template_args) && __cpp_nontype_template_args >= 201911L + +WINRT_EXPORT namespace winrt::impl +{ + 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 + constexpr hstring_reference operator ""_hs() noexcept + { + static_assert(Literal.value[Literal.size - 1] == L'\0', "_hs requires a null-terminated wide string literal"); + + if constexpr (Literal.size <= 1) + { + return hstring_reference{}; + } + else + { + return hstring_reference{ &impl::hstring_literal_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/reference_boxing.cpp b/test/test/reference_boxing.cpp new file mode 100644 index 000000000..c6e300886 --- /dev/null +++ b/test/test/reference_boxing.cpp @@ -0,0 +1,268 @@ +#include "pch.h" +#include +#include +#include + +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); + // A scalar reference holds no array, so every array getter routes through get_as and throws. + { + com_array ints; + REQUIRE_THROWS_AS(pv.GetInt32Array(ints), hresult_not_implemented); + com_array strings; + REQUIRE_THROWS_AS(pv.GetStringArray(strings), hresult_not_implemented); + } + 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); + } + + // 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); + } +} + +// Array boxing produces a local IReferenceArray / IPropertyValue (no combase PropertyValue) for the +// stock element types. Confirm the array PropertyType, round-trips, and the get_as throw behavior. +TEST_CASE("reference_boxing arrays") +{ + { + int32_t values[]{ 0, 42, 1729, -1 }; + auto boxed = box_value(com_array{ std::begin(values), std::end(values) }); + auto pv = boxed.as(); + REQUIRE(pv.Type() == PropertyType::Int32Array); + REQUIRE(!pv.IsNumericScalar()); + + com_array out; + pv.GetInt32Array(out); + REQUIRE(out == array_view{ values }); + + // A scalar getter on an array PV throws, and so does a mismatched-element array getter. + REQUIRE_THROWS_AS(pv.GetInt32(), hresult_not_implemented); + com_array wrong; + REQUIRE_THROWS_AS(pv.GetDoubleArray(wrong), hresult_not_implemented); + + REQUIRE(unbox_value>(boxed) == array_view{ values }); + REQUIRE(boxed.as>().Value() == array_view{ values }); + } + + // guid arrays are local too. + { + guid values[]{ + { 0x11223344, 0x5566, 0x7788, { 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0x00 } }, + { 0x00112233, 0x4455, 0x6677, { 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF } } }; + auto boxed = box_value(com_array{ std::begin(values), std::end(values) }); + REQUIRE(boxed.as().Type() == PropertyType::GuidArray); + REQUIRE(unbox_value>(boxed) == array_view{ values }); + } +} + +// The local array reference must marshal by value across processes just like the scalar one: its +// IMarshal reports the same unmarshal class as a genuine combase array PropertyValue, and not the +// free-threaded (by-reference) class. +TEST_CASE("reference_boxing array marshal by value") +{ + int32_t values[]{ 1, 2, 3 }; + auto boxed = box_value(com_array{ std::begin(values), std::end(values) }); + REQUIRE(boxed.try_as()); + auto ours = boxed.as(); + + auto genuine = PropertyValue::CreateInt32Array(values); + 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); + + guid const free_threaded_marshaler{ 0x0000033A, 0x0000, 0x0000, { 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46 } }; + REQUIRE(our_clsid != free_threaded_marshaler); +} + +// 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); +} + +// The in-proc reference advertises IAgileObject, so handing it between two single-threaded +// apartments in the same process must resolve to the *same* object pointer - no proxy. The Global +// Interface Table returns an agile object's original pointer directly, but hands back a proxy (a +// different identity) for a non-agile object, so pointer equality here confirms the agile fast path. +TEST_CASE("reference_boxing agile in-proc identity across apartments") +{ + auto identity_of = [](::IUnknown* raw) -> void* + { + com_ptr<::IUnknown> identity; + check_hresult(raw->QueryInterface(IID_PPV_ARGS(identity.put()))); + return identity.get(); + }; + + com_ptr git; + check_hresult(CoCreateInstance(CLSID_StdGlobalInterfaceTable, nullptr, + CLSCTX_INPROC_SERVER, IID_PPV_ARGS(git.put()))); + + Windows::Foundation::IInspectable boxed{ nullptr }; + DWORD cookie{}; + void* original_identity{}; + void* marshaled_identity{}; + HRESULT sta1_hr = S_OK; + HRESULT sta2_hr = S_OK; + + handle registered{ check_pointer(CreateEventW(nullptr, true, false, nullptr)) }; + handle fetched{ check_pointer(CreateEventW(nullptr, true, false, nullptr)) }; + + std::thread sta1([&] + { + sta1_hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + if (SUCCEEDED(sta1_hr)) + { + boxed = box_value(42); + auto unknown = reinterpret_cast<::IUnknown*>(get_abi(boxed)); + original_identity = identity_of(unknown); + sta1_hr = git->RegisterInterfaceInGlobal(unknown, IID_IUnknown, &cookie); + } + SetEvent(registered.get()); + + WaitForSingleObject(fetched.get(), INFINITE); + if (SUCCEEDED(sta1_hr)) + { + CoUninitialize(); + } + }); + + std::thread sta2([&] + { + WaitForSingleObject(registered.get(), INFINITE); + if (SUCCEEDED(sta1_hr)) + { + sta2_hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + if (SUCCEEDED(sta2_hr)) + { + ::IUnknown* raw{}; + sta2_hr = git->GetInterfaceFromGlobal(cookie, IID_IUnknown, reinterpret_cast(&raw)); + if (SUCCEEDED(sta2_hr)) + { + marshaled_identity = identity_of(raw); + raw->Release(); + } + git->RevokeInterfaceFromGlobal(cookie); + CoUninitialize(); + } + } + SetEvent(fetched.get()); + }); + + sta1.join(); + sta2.join(); + + REQUIRE(SUCCEEDED(sta1_hr)); + REQUIRE(SUCCEEDED(sta2_hr)); + REQUIRE(original_identity != nullptr); + REQUIRE(original_identity == marshaled_identity); +} diff --git a/test/test/test.vcxproj b/test/test/test.vcxproj index 43928dab2..f95584d99 100644 --- a/test/test/test.vcxproj +++ b/test/test/test.vcxproj @@ -324,6 +324,7 @@ NotUsing + diff --git a/test/test_cpp20/hstring_literal.cpp b/test/test_cpp20/hstring_literal.cpp new file mode 100644 index 000000000..2ba0f3ad7 --- /dev/null +++ b/test/test_cpp20/hstring_literal.cpp @@ -0,0 +1,68 @@ +#include "pch.h" + +using namespace winrt; +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 + // 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") +{ + // 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; + 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 9cf9564cf31638e41149016f2ca7a07b3f97adb6 Mon Sep 17 00:00:00 2001 From: Chris Guzak Date: Wed, 26 Aug 2026 16:14:48 -0700 Subject: [PATCH 6/7] Add get_unchecked() to async operations (#1565) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add get_only_safe_from_non_presenting_sta() to async operations Add a peer to .get() on IAsyncAction, IAsyncOperation, IAsyncActionWithProgress, and IAsyncOperationWithProgress that skips the _DEBUG-only STA blocking assert. The existing .get() asserts !is_sta_thread() to guard against blocking UI threads. However, not all STAs are UI threads — some never present UI, haven't presented yet, or never will. The assert is also _DEBUG-only, making it invisible to codebases that don't build with _DEBUG (e.g. the Windows OS). The new method get_only_safe_from_non_presenting_sta() is functionally identical to .get() but omits the STA check. The intentionally long name communicates the risk to callers. Changes: - strings/base_coroutine_foundation.h: Add wait_get_bypass_sta_check() impl helper and get_only_safe_from_non_presenting_sta() for all 4 async consume templates - cppwinrt/code_writers.h: Add declaration to generated code for all 4 async types - test/test_nocoro: Add test calling the new method from an STA thread using a real WinRT async operation (PathIO::ReadTextAsync on C:\Windows\win.ini) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Rename STA bypassing async get method Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix STA test cleanup and assertions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Chris Guzak (WINDOWS) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- cppwinrt/code_writers.h | 12 +++++++++ strings/base_coroutine_foundation.h | 33 ++++++++++++++++++++++++ test/test_nocoro/get.cpp | 40 +++++++++++++++++++++++++++++ test/test_nocoro/pch.h | 1 + 4 files changed, 86 insertions(+) diff --git a/cppwinrt/code_writers.h b/cppwinrt/code_writers.h index b3855c097..a9aa5bc78 100644 --- a/cppwinrt/code_writers.h +++ b/cppwinrt/code_writers.h @@ -1475,24 +1475,36 @@ namespace cppwinrt else if (type_name == "Windows.Foundation.IAsyncAction") { w.write(R"( auto get() const; + // Synchronously waits without asserting that the calling thread is not an STA. + // Use only when the STA is not presenting UI and blocking is known to be safe. + auto get_unchecked() const; auto wait_for(Windows::Foundation::TimeSpan const& timeout) const; )"); } else if (type_name == "Windows.Foundation.IAsyncOperation`1") { w.write(R"( auto get() const; + // Synchronously waits without asserting that the calling thread is not an STA. + // Use only when the STA is not presenting UI and blocking is known to be safe. + auto get_unchecked() const; auto wait_for(Windows::Foundation::TimeSpan const& timeout) const; )"); } else if (type_name == "Windows.Foundation.IAsyncActionWithProgress`1") { w.write(R"( auto get() const; + // Synchronously waits without asserting that the calling thread is not an STA. + // Use only when the STA is not presenting UI and blocking is known to be safe. + auto get_unchecked() const; auto wait_for(Windows::Foundation::TimeSpan const& timeout) const; )"); } else if (type_name == "Windows.Foundation.IAsyncOperationWithProgress`2") { w.write(R"( auto get() const; + // Synchronously waits without asserting that the calling thread is not an STA. + // Use only when the STA is not presenting UI and blocking is known to be safe. + auto get_unchecked() const; auto wait_for(Windows::Foundation::TimeSpan const& timeout) const; )"); } diff --git a/strings/base_coroutine_foundation.h b/strings/base_coroutine_foundation.h index 5cefad836..82582ee64 100644 --- a/strings/base_coroutine_foundation.h +++ b/strings/base_coroutine_foundation.h @@ -99,6 +99,19 @@ WINRT_EXPORT namespace winrt::impl return async.GetResults(); } + template + auto wait_get_bypass_sta_check(Async const& async) + { + auto status = async.Status(); + if (status == Windows::Foundation::AsyncStatus::Started) + { + status = wait_for_completed(async, 0xFFFFFFFF); // INFINITE + } + check_status_canceled(status); + + return async.GetResults(); + } + #ifdef WINRT_IMPL_COROUTINES struct ignore_apartment_context {}; @@ -220,6 +233,11 @@ WINRT_EXPORT namespace winrt::impl impl::wait_get(static_cast(static_cast(*this))); } template + auto consume_Windows_Foundation_IAsyncAction::get_unchecked() const + { + impl::wait_get_bypass_sta_check(static_cast(static_cast(*this))); + } + template auto consume_Windows_Foundation_IAsyncAction::wait_for(Windows::Foundation::TimeSpan const& timeout) const { return impl::wait_for(static_cast(static_cast(*this)), timeout); @@ -231,6 +249,11 @@ WINRT_EXPORT namespace winrt::impl return impl::wait_get(static_cast const&>(static_cast(*this))); } template + auto consume_Windows_Foundation_IAsyncOperation::get_unchecked() const + { + return impl::wait_get_bypass_sta_check(static_cast const&>(static_cast(*this))); + } + template auto consume_Windows_Foundation_IAsyncOperation::wait_for(Windows::Foundation::TimeSpan const& timeout) const { return impl::wait_for(static_cast const&>(static_cast(*this)), timeout); @@ -242,6 +265,11 @@ WINRT_EXPORT namespace winrt::impl impl::wait_get(static_cast const&>(static_cast(*this))); } template + auto consume_Windows_Foundation_IAsyncActionWithProgress::get_unchecked() const + { + impl::wait_get_bypass_sta_check(static_cast const&>(static_cast(*this))); + } + template auto consume_Windows_Foundation_IAsyncActionWithProgress::wait_for(Windows::Foundation::TimeSpan const& timeout) const { return impl::wait_for(static_cast const&>(static_cast(*this)), timeout); @@ -253,6 +281,11 @@ WINRT_EXPORT namespace winrt::impl return impl::wait_get(static_cast const&>(static_cast(*this))); } template + auto consume_Windows_Foundation_IAsyncOperationWithProgress::get_unchecked() const + { + return impl::wait_get_bypass_sta_check(static_cast const&>(static_cast(*this))); + } + template auto consume_Windows_Foundation_IAsyncOperationWithProgress::wait_for(Windows::Foundation::TimeSpan const& timeout) const { return impl::wait_for(static_cast const&>(static_cast(*this)), timeout); diff --git a/test/test_nocoro/get.cpp b/test/test_nocoro/get.cpp index 11339e10b..4fd85fe3e 100644 --- a/test/test_nocoro/get.cpp +++ b/test/test_nocoro/get.cpp @@ -2,6 +2,7 @@ using namespace winrt; using namespace Windows::Foundation; +using namespace Windows::Storage; template struct async_completion_source : implements, IAsyncOperation, IAsyncInfo> @@ -72,3 +73,42 @@ TEST_CASE("get") REQUIRE(acs.as>().get() == 0xDEADBEEF); } + +TEST_CASE("get_unchecked") +{ + // Call a real WinRT async operation from an STA thread. + // This is the scenario the new API is designed for: an STA that is not + // presenting UI, where a synchronous blocking wait is safe. + std::exception_ptr failure{}; + bool content_available = false; + std::thread sta_thread([&failure, &content_available] + { + try + { + winrt::init_apartment(winrt::apartment_type::single_threaded); + struct apartment_guard + { + ~apartment_guard() + { + winrt::uninit_apartment(); + } + } guard; + + auto content = PathIO::ReadTextAsync(L"C:\\Windows\\win.ini").get_unchecked(); + content_available = content.size() > 0; + } + catch (...) + { + failure = std::current_exception(); + } + }); + + sta_thread.join(); + + if (failure) + { + std::rethrow_exception(failure); + } + + REQUIRE(content_available); +} diff --git a/test/test_nocoro/pch.h b/test/test_nocoro/pch.h index 7ff48a37c..30a6d3079 100644 --- a/test/test_nocoro/pch.h +++ b/test/test_nocoro/pch.h @@ -2,5 +2,6 @@ #include "catch.hpp" #include "winrt/Windows.Foundation.h" +#include "winrt/Windows.Storage.h" using namespace std::literals; From 856eaff70a48d6b053c2ad3dffec15b81ca34b10 Mon Sep 17 00:00:00 2001 From: Dan Fiedler <151573964+danfiedler-msft@users.noreply.github.com> Date: Wed, 26 Aug 2026 19:33:15 -0400 Subject: [PATCH 7/7] Pin GitHub Actions to full-length commit SHAs (#1618) --- .github/actions/setup-llvm-mingw/action.yml | 4 ++-- .github/actions/setup-llvm-msvc/action.yml | 2 +- .github/dependabot.yml | 2 ++ .github/workflows/check-line-endings.yml | 2 +- .github/workflows/ci.yml | 26 ++++++++++----------- 5 files changed, 19 insertions(+), 17 deletions(-) diff --git a/.github/actions/setup-llvm-mingw/action.yml b/.github/actions/setup-llvm-mingw/action.yml index 50fe696b7..d36b48a75 100644 --- a/.github/actions/setup-llvm-mingw/action.yml +++ b/.github/actions/setup-llvm-mingw/action.yml @@ -19,7 +19,7 @@ runs: - name: Cache llvm-mingw (Windows) id: cache-llvm if: runner.os == 'Windows' - uses: actions/cache@v4 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: .llvm-mingw key: llvm-mingw-${{ runner.os }}-${{ inputs.llvm-mingw-version }}-${{ inputs.host-arch }} @@ -50,7 +50,7 @@ runs: - name: Cache llvm-mingw (Linux) id: cache-llvm-linux if: runner.os == 'Linux' - uses: actions/cache@v4 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: /opt/llvm-mingw key: llvm-mingw-${{ runner.os }}-${{ inputs.llvm-mingw-version }}-${{ inputs.host-arch }} diff --git a/.github/actions/setup-llvm-msvc/action.yml b/.github/actions/setup-llvm-msvc/action.yml index 036d0fa28..e5a696a9f 100644 --- a/.github/actions/setup-llvm-msvc/action.yml +++ b/.github/actions/setup-llvm-msvc/action.yml @@ -14,7 +14,7 @@ runs: steps: - name: Cache LLVM and tools id: cache-llvm - uses: actions/cache@v4 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: | .LLVM diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 47f88349d..9557baa7a 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -8,3 +8,5 @@ updates: directory: "/" schedule: interval: "daily" + cooldown: + default-days: 7 diff --git a/.github/workflows/check-line-endings.yml b/.github/workflows/check-line-endings.yml index dea958274..3891aec09 100644 --- a/.github/workflows/check-line-endings.yml +++ b/.github/workflows/check-line-endings.yml @@ -11,7 +11,7 @@ jobs: name: Enforce .gitattributes line endings runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Check for line ending violations run: | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index afdeed462..5e2677fed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,7 @@ jobs: config: Release runs-on: ${{ matrix.toolchain.image }} steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Download nuget run: | @@ -71,7 +71,7 @@ jobs: cmd /c "$env:VSDevCmd" "&" msbuild /m /clp:ForceConsoleColor "$env:msbuild_config_props" cppwinrt.sln /t:cppwinrt - name: Upload built executables - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: msvc-build-${{ matrix.compiler}}-${{ matrix.arch }}-${{ matrix.config }}-${{ matrix.toolchain.platform_toolset }}-bin path: | @@ -113,18 +113,18 @@ jobs: test_exe: test_cpp20_module runs-on: ${{ matrix.toolchain.image }} steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Fetch cppwinrt executables if: matrix.arch != 'arm64' - uses: actions/download-artifact@v8 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: msvc-build-${{ matrix.compiler}}-${{ matrix.arch }}-${{ matrix.config }}-${{ matrix.toolchain.platform_toolset }}-bin path: _build/${{ matrix.arch }}/${{ matrix.config }}/ - name: Fetch x86 cppwinrt executables (arm64 only) if: matrix.arch == 'arm64' - uses: actions/download-artifact@v8 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: msvc-build-${{ matrix.compiler}}-x86-Release-${{ matrix.toolchain.platform_toolset }}-bin path: _build/x86/Release/ @@ -243,7 +243,7 @@ jobs: - name: Upload arm64 test executables if: matrix.arch == 'arm64' - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: msvc-tests-${{ matrix.test_exe }}-${{ matrix.compiler }}-${{ matrix.arch }}-${{ matrix.config }}-${{ matrix.toolchain.platform_toolset }}-bin path: | @@ -263,7 +263,7 @@ jobs: CMAKE_COLOR_DIAGNOSTICS: 1 CLICOLOR_FORCE: 1 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Install cross compiler run: | @@ -282,7 +282,7 @@ jobs: cmake --build build/cross_x64/ --target install -j2 - name: Upload cppwinrt.exe - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: cross-build-${{ matrix.arch }}-bin path: install/bin/cppwinrt.exe @@ -296,7 +296,7 @@ jobs: Deployment: [Component, Standalone] runs-on: windows-2025-vs2026 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Download nuget run: | @@ -339,10 +339,10 @@ jobs: platform_toolset: v145 runs-on: ${{ matrix.toolchain.image }} steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Fetch cppwinrt executables - uses: actions/download-artifact@v8 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: msvc-build-${{ matrix.compiler}}-${{ matrix.arch }}-${{ matrix.config }}-${{ matrix.toolchain.platform_toolset }}-bin path: _build/${{ matrix.arch }}/${{ matrix.config }}/ @@ -384,7 +384,7 @@ jobs: name: Build nuget package with MSVC runs-on: windows-2025-vs2026 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Package run: | @@ -399,7 +399,7 @@ jobs: } - name: Upload nuget package artifact - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: package path: "*.nupkg"