Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
423 changes: 238 additions & 185 deletions strings/base_reference_produce.h

Large diffs are not rendered by default.

80 changes: 80 additions & 0 deletions strings/base_string.h
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,24 @@ WINRT_EXPORT namespace winrt::impl
return nullptr;
}
};

template <std::size_t N>
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];
}
}
Comment thread
jonwis marked this conversation as resolved.
};

template <std::size_t N>
hstring_literal_storage(wchar_t const (&)[N]) -> hstring_literal_storage<N>;
}

WINRT_EXPORT namespace winrt
Expand Down Expand Up @@ -386,6 +404,30 @@ WINRT_EXPORT namespace winrt
handle_type<impl::hstring_traits> m_handle;
};

struct hstring_reference
{
constexpr hstring_reference() noexcept = default;

constexpr explicit hstring_reference(impl::hstring_header const* header) noexcept :
m_handle(const_cast<impl::hstring_header*>(header))
{
}

operator hstring const&() const noexcept
{
return *reinterpret_cast<hstring const*>(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);
Expand Down Expand Up @@ -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 <hstring_literal_storage Literal>
inline constexpr hstring_header hstring_literal_header
Comment thread
jonwis marked this conversation as resolved.
{
hstring_reference_flag,
static_cast<std::uint32_t>(Literal.size - 1),
0,
0,
Literal.value
};
}

WINRT_EXPORT namespace winrt
{
inline namespace literals
{
template <impl::hstring_literal_storage Literal>
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<Literal> };
}
}
}
}

#endif

#ifdef __cpp_lib_format
template<>
struct std::formatter<winrt::hstring, wchar_t> : std::formatter<std::wstring_view, wchar_t> {};
Expand Down
4 changes: 4 additions & 0 deletions strings/base_string_input.h
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
268 changes: 268 additions & 0 deletions test/test/reference_boxing.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
#include "pch.h"
#include <objbase.h>
#include <objidl.h>
#include <thread>

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<IPropertyValue>();
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<int32_t> ints;
REQUIRE_THROWS_AS(pv.GetInt32Array(ints), hresult_not_implemented);
com_array<hstring> strings;
REQUIRE_THROWS_AS(pv.GetStringArray(strings), hresult_not_implemented);
}
REQUIRE(unbox_value<int32_t>(boxed) == 42);
}

{
auto pv = box_value(3.5).as<IPropertyValue>();
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<IPropertyValue>();
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<IPropertyValue>();
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<IPropertyValue>();
REQUIRE(pv.Type() == PropertyType::Guid);
REQUIRE(!pv.IsNumericScalar());
REQUIRE(pv.GetGuid() == g);
}

{
auto pv = box_value(static_cast<uint8_t>(7)).as<IPropertyValue>();
REQUIRE(pv.Type() == PropertyType::UInt8);
REQUIRE(pv.IsNumericScalar());
REQUIRE(pv.GetUInt8() == 7);
REQUIRE(unbox_value<uint8_t>(box_value(static_cast<uint8_t>(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<IPropertyValue>();
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<Point>(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<IPropertyValue>();
REQUIRE(pv.Type() == PropertyType::TimeSpan);
REQUIRE(!pv.IsNumericScalar());
REQUIRE(pv.GetTimeSpan() == span);
REQUIRE(unbox_value<TimeSpan>(box_value(span)) == span);
}

{
DateTime const when{ TimeSpan{ std::chrono::seconds{ 1000 } } };
auto pv = box_value(when).as<IPropertyValue>();
REQUIRE(pv.Type() == PropertyType::DateTime);
REQUIRE(!pv.IsNumericScalar());
REQUIRE(pv.GetDateTime() == when);
REQUIRE(unbox_value<DateTime>(box_value(when)) == when);
}
}

// Array boxing produces a local IReferenceArray<T> / 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<int32_t>{ std::begin(values), std::end(values) });
auto pv = boxed.as<IPropertyValue>();
REQUIRE(pv.Type() == PropertyType::Int32Array);
REQUIRE(!pv.IsNumericScalar());

com_array<int32_t> out;
pv.GetInt32Array(out);
REQUIRE(out == array_view<int32_t>{ 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<double> wrong;
REQUIRE_THROWS_AS(pv.GetDoubleArray(wrong), hresult_not_implemented);

REQUIRE(unbox_value<com_array<int32_t>>(boxed) == array_view<int32_t>{ values });
REQUIRE(boxed.as<IReferenceArray<int32_t>>().Value() == array_view<int32_t>{ 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<guid>{ std::begin(values), std::end(values) });
REQUIRE(boxed.as<IPropertyValue>().Type() == PropertyType::GuidArray);
REQUIRE(unbox_value<com_array<guid>>(boxed) == array_view<guid>{ 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<int32_t>{ std::begin(values), std::end(values) });
REQUIRE(boxed.try_as<IAgileObject>());
auto ours = boxed.as<impl::IMarshal>();

auto genuine = PropertyValue::CreateInt32Array(values);
auto reference = genuine.as<impl::IMarshal>();

guid our_clsid{};
guid reference_clsid{};
check_hresult(ours->GetUnmarshalClass(guid_of<IPropertyValue>(), get_unknown(boxed),
MSHCTX_DIFFERENTMACHINE, nullptr, MSHLFLAGS_NORMAL, &our_clsid));
check_hresult(reference->GetUnmarshalClass(guid_of<IPropertyValue>(), 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<IAgileObject>());
auto ours = boxed.as<impl::IMarshal>();

auto genuine = PropertyValue::CreateInt32(42);
auto reference = genuine.as<impl::IMarshal>();

guid our_clsid{};
guid reference_clsid{};
check_hresult(ours->GetUnmarshalClass(guid_of<IPropertyValue>(), get_unknown(boxed),
MSHCTX_DIFFERENTMACHINE, nullptr, MSHLFLAGS_NORMAL, &our_clsid));
check_hresult(reference->GetUnmarshalClass(guid_of<IPropertyValue>(), 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<IGlobalInterfaceTable> 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<void**>(&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);
}
1 change: 1 addition & 0 deletions test/test/test.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@
<ClCompile Include="main.cpp">
<PrecompiledHeader>NotUsing</PrecompiledHeader>
</ClCompile>
<ClCompile Include="reference_boxing.cpp" />
<ClCompile Include="memory_buffer.cpp" />
<ClCompile Include="missing_required_interfaces.cpp" />
<ClCompile Include="module_lock_dll.cpp">
Expand Down
Loading