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
44 changes: 44 additions & 0 deletions src/Tests/TestComponentCSharp/Class.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1914,6 +1914,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 @@ -443,6 +443,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 @@ -524,6 +524,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 @@ -3883,6 +3883,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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What was the 2.x behavior here?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

TLDR: this just fails to build entirely in 2.x.

Copilot analysis:

There isn't a 2.x behavior to compare against here — in 2.x this is a hard build break, not a runtime behavior.

CsWinRT 2.x has no special-casing for this at all. helpers.h maps Windows.Foundation.IReference`1System.Nullable, Windows.UI.Xaml.Interop.TypeNameSystem.Type and Windows.Foundation.HResultSystem.Exception, and write_projection_type_for_name_type just recurses through generic instantiations applying that table. So the mapped names get substituted verbatim, producing types that aren't legal C#.

I verified this empirically rather than just reading the code: I built a .winmd with the exact member shapes this PR adds and ran the real cswinrt.exe from Microsoft.Windows.CsWinRT 2.2.0 over it. Generated output:

public static global::System.Nullable<global::System.Type> BoxedTypeName => ...;
public static global::System.Nullable<global::System.Type> RoundtripTypeName(global::System.Nullable<global::System.Type> value) => ...;
public static global::System.Nullable<global::System.Exception> BoxedHResult => ...;

// the case this test covers
public static global::System.Collections.Generic.IList<global::System.Nullable<global::System.Type>> GetReferenceTypeNameList() => ...;
public static int CountReferenceTypeNameList(global::System.Collections.Generic.IList<global::System.Nullable<global::System.Type>> value) => ...;
public static global::System.Collections.Generic.IList<global::System.Nullable<global::System.Exception>> GetReferenceHResultList() => ...;

It also emits a WinRT.GenericTypeInstantiations.Windows_Foundation_Collections_IVector_1_Windows_Foundation_IReference_1_Windows_UI_Xaml_Interop_TypeName_ class whose every member is typed Nullable<Type>.

Building that in a real consumer project fails with 58 errors, all CS0453 ("The type 'Type'/'Exception' must be a non-nullable value type in order to use it as parameter 'T' in the generic type or method 'Nullable<T>'"), spread across both the projection file and WinRTGenericTypeInstantiations.cs. Because cswinrt emits one file per namespace, a single such member takes down the whole projection — the entire consuming project fails to compile, so you never get far enough to observe any runtime behavior. There was no workaround short of removing the API from the metadata.

One nuance worth calling out, since it's the only part 2.x did handle: the boxed-as-object path works. TypeNameSupport hardcodes IReference`1<Windows.UI.Xaml.Interop.TypeName>ABI.System.Nullable_Type and IReference`1<Windows.Foundation.HResult>ABI.System.Nullable_Exception (alongside IReference`1<String>ABI.System.Nullable_string, with the comment "PropertySet and ValueSet can return IReference<String> but Nullable<String> is illegal"), and ComWrappersSupport routes typeof(Type)/typeof(Exception) through NullableType.GetValueFactory. That's what the existing Class.BoxedType test covers — but note it's declared as plain Object in the 2.x IDL. So the runtime could always unwrap these; it was only codegen that broke the moment the member was statically typed as IReference<TypeName> in metadata.

For context on why nobody hit this earlier: I ran 2.2.0's cswinrt over the entire Windows SDK (10.0.26100.0, 320 generated files incl. 23 Windows.UI.Xaml*) and there are zero occurrences of Nullable<Type>, Nullable<Exception> or Nullable<string> — no shipping SDK metadata declares these shapes in a typed signature (Nullable<int> shows up in 12 files, so IReference<T> itself is common). It only surfaced via a third-party .winmd.

Tracking: #2097 reports exactly this against 2.2.0 and is still open; community PR #2103 proposes emitting T? instead of Nullable<T> in write_projection_type_for_name_type, but it's unmerged (and targeted at staging/3.0). So every released 2.x is broken here.

Bottom line: there's no 2.x baseline to preserve. 2.x fails to build for all four shapes; this PR makes the two scalar cases project and marshal correctly, and degrades the two collection cases to a NotSupportedException at the call site — which is strictly better, and keeps the failure local to the unsupported member instead of breaking the whole projection.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is there any reason we shouldn't be just automatically unboxing this given we will correctly project it now as a reference type rather than throwing not supported?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Do you mean for the collection case?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

yea

// 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