Skip to content
Draft
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
44 changes: 44 additions & 0 deletions src/Tests/TestComponentCSharp/Class.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1891,6 +1891,50 @@ namespace winrt::TestComponentCSharp::implementation
return type.Name;
}

IReference<Windows::UI::Xaml::Interop::TypeName> Class::BoxedTypeName()
{
return winrt::box_value(winrt::xaml_typename<winrt::TestComponentCSharp::Class>()).as<IReference<Windows::UI::Xaml::Interop::TypeName>>();
}

IReference<Windows::UI::Xaml::Interop::TypeName> Class::RoundtripTypeName(IReference<Windows::UI::Xaml::Interop::TypeName> const& value)
{
return value;
}

IReference<winrt::hresult> Class::BoxedHResult()
{
// Box a known failure 'HRESULT' ('E_INVALIDARG') as 'IReference<HResult>'
return winrt::box_value(winrt::hresult{ static_cast<int32_t>(0x80070057) }).as<IReference<winrt::hresult>>();
}

IReference<winrt::hresult> Class::RoundtripHResult(IReference<winrt::hresult> const& value)
{
return value;
}

IVector<IReference<Windows::UI::Xaml::Interop::TypeName>> Class::GetReferenceTypeNameList()
{
return single_threaded_vector<IReference<Windows::UI::Xaml::Interop::TypeName>>({
winrt::box_value(winrt::xaml_typename<winrt::TestComponentCSharp::Class>()).as<IReference<Windows::UI::Xaml::Interop::TypeName>>(),
winrt::box_value(winrt::xaml_typename<int32_t>()).as<IReference<Windows::UI::Xaml::Interop::TypeName>>() });
}

int32_t Class::CountReferenceTypeNameList(IVector<IReference<Windows::UI::Xaml::Interop::TypeName>> const& value)
{
return value ? static_cast<int32_t>(value.Size()) : 0;
}

IVector<IReference<winrt::hresult>> Class::GetReferenceHResultList()
{
return single_threaded_vector<IReference<winrt::hresult>>({
winrt::box_value(winrt::hresult{ static_cast<int32_t>(0x80070057) }).as<IReference<winrt::hresult>>() });
}

int32_t Class::CountReferenceHResultList(IVector<IReference<winrt::hresult>> const& value)
{
return value ? static_cast<int32_t>(value.Size()) : 0;
}

WF::IInspectable Class::EmptyString()
{
return winrt::box_value(hstring{});
Expand Down
11 changes: 11 additions & 0 deletions src/Tests/TestComponentCSharp/Class.h
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,17 @@ namespace winrt::TestComponentCSharp::implementation
static bool VerifyTypeIsThisClassType(Windows::UI::Xaml::Interop::TypeName const& type_name);
static hstring GetTypeNameForType(Windows::UI::Xaml::Interop::TypeName const& type);

static Windows::Foundation::IReference<Windows::UI::Xaml::Interop::TypeName> BoxedTypeName();
static Windows::Foundation::IReference<Windows::UI::Xaml::Interop::TypeName> RoundtripTypeName(Windows::Foundation::IReference<Windows::UI::Xaml::Interop::TypeName> const& value);

static Windows::Foundation::IReference<winrt::hresult> BoxedHResult();
static Windows::Foundation::IReference<winrt::hresult> RoundtripHResult(Windows::Foundation::IReference<winrt::hresult> const& value);

static Windows::Foundation::Collections::IVector<Windows::Foundation::IReference<Windows::UI::Xaml::Interop::TypeName>> GetReferenceTypeNameList();
static int32_t CountReferenceTypeNameList(Windows::Foundation::Collections::IVector<Windows::Foundation::IReference<Windows::UI::Xaml::Interop::TypeName>> const& value);
static Windows::Foundation::Collections::IVector<Windows::Foundation::IReference<winrt::hresult>> GetReferenceHResultList();
static int32_t CountReferenceHResultList(Windows::Foundation::Collections::IVector<Windows::Foundation::IReference<winrt::hresult>> const& value);

static Windows::Foundation::IInspectable EmptyString();
static Windows::Foundation::IInspectable BoxedDelegate();
static Windows::Foundation::IInspectable BoxedEnum();
Expand Down
19 changes: 19 additions & 0 deletions src/Tests/TestComponentCSharp/TestComponentCSharp.idl
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,25 @@ namespace TestComponentCSharp

static String GetTypeNameForType(Windows.UI.Xaml.Interop.TypeName type);

// 'IReference<WUX.Interop.TypeName>' projects to 'System.Type' ('TypeName' is a Windows Runtime value type
// but projects to the reference type 'System.Type', so it must project as 'Type', not 'Nullable<Type>').
static Windows.Foundation.IReference<Windows.UI.Xaml.Interop.TypeName> BoxedTypeName{ get; };
static Windows.Foundation.IReference<Windows.UI.Xaml.Interop.TypeName> RoundtripTypeName(Windows.Foundation.IReference<Windows.UI.Xaml.Interop.TypeName> value);

// 'IReference<HResult>' projects to 'System.Exception' ('HResult' is a Windows Runtime value type but
// projects to the reference type 'System.Exception', so it must project as 'Exception', not 'Nullable<Exception>').
static Windows.Foundation.IReference<Windows.Foundation.HResult> BoxedHResult{ get; };
static Windows.Foundation.IReference<Windows.Foundation.HResult> RoundtripHResult(Windows.Foundation.IReference<Windows.Foundation.HResult> value);

// Generic collections of these reference-projected types: 'IVector<IReference<TypeName>>' and
// 'IVector<IReference<HResult>>' both project to 'IList<Type>' / 'IList<Exception>', which collide
// with the canonical 'IVector<TypeName>' / 'IVector<HResult>' IIDs. These members exist to verify the
// projection still compiles; the matching unit tests document the (broken) runtime behavior.
static Windows.Foundation.Collections.IVector<Windows.Foundation.IReference<Windows.UI.Xaml.Interop.TypeName> > GetReferenceTypeNameList();
static Int32 CountReferenceTypeNameList(Windows.Foundation.Collections.IVector<Windows.Foundation.IReference<Windows.UI.Xaml.Interop.TypeName> > value);
static Windows.Foundation.Collections.IVector<Windows.Foundation.IReference<Windows.Foundation.HResult> > GetReferenceHResultList();
static Int32 CountReferenceHResultList(Windows.Foundation.Collections.IVector<Windows.Foundation.IReference<Windows.Foundation.HResult> > value);

// Other
static Object EmptyString { get; };

Expand Down
64 changes: 64 additions & 0 deletions src/Tests/UnitTest/TestComponentCSharp_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3758,6 +3758,70 @@ public void ProjectedTypeInfo()
Assert.IsTrue(Class.VerifyTypeIsReferenceInt32Type(typeof(int?)));
}

[TestMethod]
public void ReferenceTypeNameProjectsAsType()
{
// 'IReference<WUX.Interop.TypeName>' projects to 'System.Type', not the invalid 'Nullable<Type>'
// ('TypeName' is a Windows Runtime value type, but it projects to the reference type 'System.Type'). The boxed
// value round-trips through the native boundary as a 'Type', including the null case.
Assert.AreEqual(typeof(Class), Class.BoxedTypeName);

Assert.AreEqual(typeof(int), Class.RoundtripTypeName(typeof(int)));
Assert.AreEqual(typeof(Class), Class.RoundtripTypeName(typeof(Class)));
Assert.IsNull(Class.RoundtripTypeName(null));
}

[TestMethod]
public void ReferenceHResultProjectsAsException()
{
// 'IReference<HResult>' projects to 'System.Exception', not the invalid 'Nullable<Exception>'
// ('HResult' is a Windows Runtime value type, but it projects to the reference type 'System.Exception'). The
// boxed value round-trips through the native boundary as an 'Exception', including the null case.
Exception boxed = Class.BoxedHResult;

Assert.IsNotNull(boxed);
Assert.AreEqual(unchecked((int)0x80070057), boxed.HResult); // 'E_INVALIDARG'

Assert.IsInstanceOfType<ArgumentOutOfRangeException>(Class.RoundtripHResult(new ArgumentOutOfRangeException()));
Assert.IsNull(Class.RoundtripHResult(null));
}

[TestMethod]
public void ReferenceTypeNameListReturnThrowsNotSupported()
{
// 'IVector<IReference<TypeName>>' projects its public surface as 'IList<Type>', but it cannot be
// marshalled: 'System.Type' is a reference type, so there is no valid 'Nullable<Type>' collection
// marshaller for the interop generator to produce. The projected member throws 'NotSupportedException'
// instead of referencing a marshaller that does not exist (return-only direction, created in C++)
Assert.ThrowsExactly<NotSupportedException>(() => Class.GetReferenceTypeNameList());
}

[TestMethod]
public void ReferenceTypeNameListParameterThrowsNotSupported()
{
// Same limitation as the return direction: passing a managed 'IList<Type>' to a native
// 'IVector<IReference<TypeName>>' parameter throws 'NotSupportedException'
Assert.ThrowsExactly<NotSupportedException>(() => Class.CountReferenceTypeNameList(new List<Type> { typeof(Class), typeof(int) }));
}

[TestMethod]
public void ReferenceHResultListReturnThrowsNotSupported()
{
// 'IVector<IReference<HResult>>' projects its public surface as 'IList<Exception>', but it cannot be
// marshalled: 'System.Exception' is a reference type, so there is no valid 'Nullable<Exception>' collection
// marshaller for the interop generator to produce. The projected member throws 'NotSupportedException'
// instead of referencing a marshaller that does not exist (return-only direction, created in C++)
Assert.ThrowsExactly<NotSupportedException>(() => Class.GetReferenceHResultList());
}

[TestMethod]
public void ReferenceHResultListParameterThrowsNotSupported()
{
// Same limitation as the return direction: passing a managed 'IList<Exception>' to a native
// 'IVector<IReference<HResult>>' parameter throws 'NotSupportedException'
Assert.ThrowsExactly<NotSupportedException>(() => Class.CountReferenceHResultList(new List<Exception> { new ArgumentException(), new InvalidOperationException() }));
}

[TestMethod]
public void TypeInfoGenerics()
{
Expand Down
57 changes: 57 additions & 0 deletions src/WinRT.Projection.Writer/Extensions/TypeSignatureExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,63 @@ public bool IsGenericInstance()
return sig is GenericInstanceTypeSignature;
}

/// <summary>
/// Returns whether the signature is a <c>Windows.Foundation.IReference`1</c> instantiation whose
/// argument is a Windows Runtime value type that projects to a .NET reference type, namely
/// <c>Windows.UI.Xaml.Interop.TypeName</c> (projected as <see cref="System.Type"/>) or
/// <c>Windows.Foundation.HResult</c> (projected as <see cref="System.Exception"/>).
/// </summary>
/// <remarks>
/// Unlike other <c>IReference&lt;T&gt;</c> shapes, these two project to a bare reference type
/// rather than a <see cref="System.Nullable{T}"/>, because <see cref="System.Type"/> and
/// <see cref="System.Exception"/> are reference types. They marshal correctly as a scalar
/// (via <c>ABI.System.TypeMarshaller</c> / <c>ABI.System.ExceptionMarshaller</c>), but cannot
/// appear inside a generic instantiation (see <see cref="ContainsNestedNullableTOfReferenceType"/>).
/// </remarks>
/// <returns><see langword="true"/> if the signature is <c>IReference&lt;TypeName&gt;</c> or <c>IReference&lt;HResult&gt;</c>; otherwise <see langword="false"/>.</returns>
public bool IsNullableTOfReferenceType()
{
if (!sig.IsNullableT())
{
return false;
}

TypeSignature? inner = sig.GetNullableInnerType();

return inner is not null && (inner.IsSystemType() || inner.IsHResultException());
}

/// <summary>
/// Returns whether the signature is a generic instantiation whose type-argument tree
/// (recursively) contains an <see cref="IsNullableTOfReferenceType"/> shape.
/// </summary>
/// <remarks>
/// Such instantiations (e.g. <c>IVector&lt;IReference&lt;TypeName&gt;&gt;</c>) cannot be
/// marshalled: the <c>WinRT.Interop</c> marshaller name would embed a <c>Nullable&lt;Type&gt;</c>
/// or <c>Nullable&lt;Exception&gt;</c>, which is not a constructible type, so the interop generator
/// never produces a matching marshaller. The scalar <see cref="IsNullableTOfReferenceType"/> case
/// is intentionally excluded: it appears only as a (nested) type argument here, never as the
/// top-level signature, which still projects and marshals correctly on its own.
/// </remarks>
/// <returns><see langword="true"/> if a reference-projected <c>IReference&lt;T&gt;</c> is nested as a type argument; otherwise <see langword="false"/>.</returns>
public bool ContainsNestedNullableTOfReferenceType()
{
if (sig is not GenericInstanceTypeSignature gi)
{
return false;
}

foreach (TypeSignature arg in gi.TypeArguments)
{
if (arg.IsNullableTOfReferenceType() || arg.ContainsNestedNullableTOfReferenceType())
{
return true;
}
}

return false;
}

/// <summary>
/// Returns whether the signature has the "ABI reference-pointer" shape: it crosses the
/// ABI as a <c>void*</c> / <c>IInspectable*</c>. That covers <see cref="string"/> (HSTRING),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,21 @@ internal static void EmitDoAbiBodyIfSimple(IndentedTextWriter writer, Projection
$"on '{ifaceFullName}'. Events should dispatch through EmitDoAbiAddEvent / EmitDoAbiRemoveEvent.");
}

// Generic instantiations over 'IReference<TypeName>' / 'IReference<HResult>' have no valid
// 'WinRT.Interop' marshaller (see 'RequiresUnsupportedNullableTOfReferenceTypeMarshalling'), so
// the CCW body throws instead of referencing a marshaller the interop generator cannot produce
if (RequiresUnsupportedNullableTOfReferenceTypeMarshalling(sig))
{
writer.WriteLine();
writer.WriteLine(isMultiline: true, """
{
throw new global::System.NotSupportedException();
}
""");

return;
}

writer.WriteLine();
using (writer.WriteBlock())
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,23 @@ public static IndentedTextWriterCallback EmitAbiMethodBodyIfSimple(ProjectionEmi
Justification = "if/else if chains over type-class predicates are more readable than nested ternaries.")]
public static void EmitAbiMethodBodyIfSimple(IndentedTextWriter writer, ProjectionEmitContext context, MethodSignatureInfo sig, int slot, string? paramNameOverride = null, bool isNoExcept = false)
{
// Generic instantiations over 'IReference<TypeName>' / 'IReference<HResult>' cannot be marshalled:
// 'System.Type' and 'System.Exception' are reference types with no 'Nullable<T>' shape, so the
// 'WinRT.Interop' marshaller they would reference does not exist. Emit a throwing body instead
if (RequiresUnsupportedNullableTOfReferenceTypeMarshalling(sig))
{
writer.WriteLine();
writer.IncreaseIndent();
writer.WriteLine(isMultiline: true, """
{
throw new global::System.NotSupportedException();
}
""");
writer.DecreaseIndent();

return;
}

TypeSignature? rt = sig.ReturnType;

MethodSignatureMarshallingFacts facts = MethodSignatureMarshallingFacts.From(sig, context.AbiTypeKindResolver);
Expand Down
32 changes: 31 additions & 1 deletion src/WinRT.Projection.Writer/Factories/AbiMethodBodyFactory.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using WindowsRuntime.ProjectionWriter.Models;

namespace WindowsRuntime.ProjectionWriter.Factories;

/// <summary>
Expand All @@ -16,4 +18,32 @@ namespace WindowsRuntime.ProjectionWriter.Factories;
/// <item><description><c>AbiMethodBodyFactory.MarshallerDispatch.cs</c> - Per-marshaller ConvertToManaged/Unmanaged dispatch helpers.</description></item>
/// </list>
/// </remarks>
internal static partial class AbiMethodBodyFactory;
internal static partial class AbiMethodBodyFactory
{
/// <summary>
/// Returns whether the method's return type or any parameter type involves a generic
/// instantiation over <c>IReference&lt;TypeName&gt;</c> / <c>IReference&lt;HResult&gt;</c>, which
/// cannot be marshalled (see <see cref="TypeSignatureExtensions.ContainsNestedNullableTOfReferenceType"/>).
/// When this is the case, the projected member's body is replaced with a <c>throw</c> instead of
/// emitting a reference to a <c>WinRT.Interop</c> marshaller that the interop generator cannot produce.
/// </summary>
/// <param name="sig">The interface method signature being emitted.</param>
/// <returns><see langword="true"/> if the method cannot be marshalled; otherwise <see langword="false"/>.</returns>
private static bool RequiresUnsupportedNullableTOfReferenceTypeMarshalling(MethodSignatureInfo sig)
{
if (sig.ReturnType is { } rt && rt.ContainsNestedNullableTOfReferenceType())
{
return true;
}

foreach (ParameterInfo p in sig.Parameters)
{
if (p.Type.StripByRefAndCustomModifiers().ContainsNestedNullableTOfReferenceType())
{
return true;
}
}

return false;
}
}
Loading