From a8db1362fe3461c2228e0782bcbd0713d30bace2 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 19 Jun 2026 17:44:50 -0700 Subject: [PATCH 1/5] Add scalar IReference/IReference projection tests Add members to TestComponentCSharp exercising the scalar IReference projection for the two WinRT value types that map to .NET reference types: IReference -> System.Type and IReference -> System.Exception. Each adds a native-boxed property (BoxedTypeName / BoxedHResult) and a round-trip method (RoundtripTypeName / RoundtripHResult), with matching UnitTest coverage including the null case. These tests fail to build until the projection writer stops emitting the invalid Nullable / Nullable for these instantiations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Tests/TestComponentCSharp/Class.cpp | 21 ++++++++++++++ src/Tests/TestComponentCSharp/Class.h | 6 ++++ .../TestComponentCSharp.idl | 10 +++++++ .../UnitTest/TestComponentCSharp_Tests.cs | 28 +++++++++++++++++++ 4 files changed, 65 insertions(+) diff --git a/src/Tests/TestComponentCSharp/Class.cpp b/src/Tests/TestComponentCSharp/Class.cpp index 602fcddbde..69b30e1a7f 100644 --- a/src/Tests/TestComponentCSharp/Class.cpp +++ b/src/Tests/TestComponentCSharp/Class.cpp @@ -1891,6 +1891,27 @@ namespace winrt::TestComponentCSharp::implementation return type.Name; } + IReference Class::BoxedTypeName() + { + return winrt::box_value(winrt::xaml_typename()).as>(); + } + + IReference Class::RoundtripTypeName(IReference const& value) + { + return value; + } + + IReference Class::BoxedHResult() + { + // Box a known failure 'HRESULT' ('E_INVALIDARG') as 'IReference' + return winrt::box_value(winrt::hresult{ static_cast(0x80070057) }).as>(); + } + + IReference Class::RoundtripHResult(IReference const& value) + { + return value; + } + WF::IInspectable Class::EmptyString() { return winrt::box_value(hstring{}); diff --git a/src/Tests/TestComponentCSharp/Class.h b/src/Tests/TestComponentCSharp/Class.h index ed75bbedfb..429324f62f 100644 --- a/src/Tests/TestComponentCSharp/Class.h +++ b/src/Tests/TestComponentCSharp/Class.h @@ -441,6 +441,12 @@ 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 BoxedTypeName(); + static Windows::Foundation::IReference RoundtripTypeName(Windows::Foundation::IReference const& value); + + static Windows::Foundation::IReference BoxedHResult(); + static Windows::Foundation::IReference RoundtripHResult(Windows::Foundation::IReference const& value); + static Windows::Foundation::IInspectable EmptyString(); static Windows::Foundation::IInspectable BoxedDelegate(); static Windows::Foundation::IInspectable BoxedEnum(); diff --git a/src/Tests/TestComponentCSharp/TestComponentCSharp.idl b/src/Tests/TestComponentCSharp/TestComponentCSharp.idl index 3c93d84344..ff362c7f47 100644 --- a/src/Tests/TestComponentCSharp/TestComponentCSharp.idl +++ b/src/Tests/TestComponentCSharp/TestComponentCSharp.idl @@ -520,6 +520,16 @@ namespace TestComponentCSharp static String GetTypeNameForType(Windows.UI.Xaml.Interop.TypeName type); + // 'IReference' 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'). + static Windows.Foundation.IReference BoxedTypeName{ get; }; + static Windows.Foundation.IReference RoundtripTypeName(Windows.Foundation.IReference value); + + // 'IReference' 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'). + static Windows.Foundation.IReference BoxedHResult{ get; }; + static Windows.Foundation.IReference RoundtripHResult(Windows.Foundation.IReference value); + // Other static Object EmptyString { get; }; diff --git a/src/Tests/UnitTest/TestComponentCSharp_Tests.cs b/src/Tests/UnitTest/TestComponentCSharp_Tests.cs index 1a6b1dda1f..cb7df1abda 100644 --- a/src/Tests/UnitTest/TestComponentCSharp_Tests.cs +++ b/src/Tests/UnitTest/TestComponentCSharp_Tests.cs @@ -3758,6 +3758,34 @@ public void ProjectedTypeInfo() Assert.IsTrue(Class.VerifyTypeIsReferenceInt32Type(typeof(int?))); } + [TestMethod] + public void ReferenceTypeNameProjectsAsType() + { + // 'IReference' projects to 'System.Type', not the invalid 'Nullable' + // ('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' projects to 'System.Exception', not the invalid 'Nullable' + // ('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(Class.RoundtripHResult(new ArgumentOutOfRangeException())); + Assert.IsNull(Class.RoundtripHResult(null)); + } + [TestMethod] public void TypeInfoGenerics() { From 6fdcb35bd62c4b09053a9cb02bf9fd2b8783fcf4 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 19 Jun 2026 19:34:42 -0700 Subject: [PATCH 2/5] Project IReference of reference-projected value types without Nullable<> Windows.UI.Xaml.Interop.TypeName and Windows.Foundation.HResult are Windows Runtime value types that project to the .NET reference types System.Type and System.Exception. IReference of those must project to the inner type directly (Type / Exception), not the invalid System.Nullable (the type argument of Nullable must be a non-nullable value type). Generalize the TypedefNameWriter IReference special-case to drop the Nullable<> wrapper whenever the inner argument projects to a .NET reference type (TypeName -> Type, HResult -> Exception, or a literal System.Type). The runtime marshalling was already in place (TypeMarshaller / ExceptionMarshaller BoxToUnmanaged / UnboxToManaged); only the projected type name was wrong. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Helpers/TypedefNameWriter.cs | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/WinRT.Projection.Writer/Helpers/TypedefNameWriter.cs b/src/WinRT.Projection.Writer/Helpers/TypedefNameWriter.cs index d23bfdaf0a..32ea285dc6 100644 --- a/src/WinRT.Projection.Writer/Helpers/TypedefNameWriter.cs +++ b/src/WinRT.Projection.Writer/Helpers/TypedefNameWriter.cs @@ -9,6 +9,7 @@ using WindowsRuntime.ProjectionWriter.Writers; using static WindowsRuntime.ProjectionWriter.References.ProjectionNames; using static WindowsRuntime.ProjectionWriter.References.WellKnownNamespaces; +using static WindowsRuntime.ProjectionWriter.References.WellKnownTypeNames; namespace WindowsRuntime.ProjectionWriter.Helpers; @@ -227,6 +228,21 @@ public static void WriteTypeName(IndentedTextWriter writer, ProjectionEmitContex case TypeSemantics.GenericInstanceRef gir: { (string ns, string name) = gir.GenericType.Names(); + + // 'IReference' normally projects to 'System.Nullable', but a couple of Windows Runtime + // value types project to .NET reference types ('Windows.UI.Xaml.Interop.TypeName' -> 'System.Type' + // and 'Windows.Foundation.HResult' -> 'System.Exception'). For those, 'Nullable' / + // 'Nullable' would be invalid C#, so drop the nullability and project the inner type + // directly (a reference type is already nullable). + if (ns == WindowsFoundation && + name == IReferenceGeneric && + gir.GenericArgs is [{ } innerArg] && + ProjectsToReferenceType(innerArg)) + { + WriteTypeName(writer, context, innerArg, nameType, forceWriteNamespace); + break; + } + _ = MappedTypes.ApplyMapping(ref ns, ref name); if (nameType == TypedefNameType.EventSource && ns == "System") @@ -313,6 +329,37 @@ public static void WriteProjectionType(IndentedTextWriter writer, ProjectionEmit WriteTypeName(writer, context, semantics, TypedefNameType.Projected, false); } + /// + /// Returns whether the projected form of the supplied is a .NET reference type. + /// Most Windows Runtime value types project to .NET value types (so IReference<T> projects to + /// ), but two Windows Runtime value types project to .NET reference types: + /// Windows.UI.Xaml.Interop.TypeName -> and Windows.Foundation.HResult + /// -> . For those, the wrapper is invalid C#, + /// so IReference<T> must project the inner type directly (a reference type is already nullable). + /// + /// The semantic representation of the type argument. + /// if the projected type is a .NET reference type; otherwise . + private static bool ProjectsToReferenceType(TypeSemantics semantics) + { + // A literal 'System.Type' reference (e.g. from attribute-blob-encoded 'Type' arguments) + if (semantics is TypeSemantics.SystemType) + { + return true; + } + + if (semantics is TypeSemantics.Reference reference) + { + (string ns, string name) = reference.Type.Names(); + + _ = MappedTypes.ApplyMapping(ref ns, ref name); + + // The only Windows Runtime value types that map to a .NET reference type: 'TypeName' -> 'Type', 'HResult' -> 'Exception' + return ns == "System" && name is "Type" or "Exception"; + } + + return false; + } + /// /// A callback that writes the type name to the writer it's appended to. public static IndentedTextWriterCallback WriteTypeName(ProjectionEmitContext context, TypeSemantics semantics, TypedefNameType nameType, bool forceWriteNamespace) From 8369b77f3ab13c981725beeac912f1af1e7653b1 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 19 Jun 2026 20:06:55 -0700 Subject: [PATCH 3/5] Add generic IReference collection tests for Type and Exception Add IVector> and IVector> members to TestComponentCSharp (return and parameter, exercised in isolation) plus matching UnitTest coverage, to verify the generic projection compiles and to capture the runtime round-trip behavior. The public type collapses to IList / IList via the scalar fix, while the ABI marshaller keeps the Nullable element marker and so targets the IVector> IID with per-element box/unbox. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Tests/TestComponentCSharp/Class.cpp | 23 ++++++++++ src/Tests/TestComponentCSharp/Class.h | 5 +++ .../TestComponentCSharp.idl | 9 ++++ .../UnitTest/TestComponentCSharp_Tests.cs | 44 +++++++++++++++++++ 4 files changed, 81 insertions(+) diff --git a/src/Tests/TestComponentCSharp/Class.cpp b/src/Tests/TestComponentCSharp/Class.cpp index 69b30e1a7f..58a4452cd1 100644 --- a/src/Tests/TestComponentCSharp/Class.cpp +++ b/src/Tests/TestComponentCSharp/Class.cpp @@ -1912,6 +1912,29 @@ namespace winrt::TestComponentCSharp::implementation return value; } + IVector> Class::GetReferenceTypeNameList() + { + return single_threaded_vector>({ + winrt::box_value(winrt::xaml_typename()).as>(), + winrt::box_value(winrt::xaml_typename()).as>() }); + } + + int32_t Class::CountReferenceTypeNameList(IVector> const& value) + { + return value ? static_cast(value.Size()) : 0; + } + + IVector> Class::GetReferenceHResultList() + { + return single_threaded_vector>({ + winrt::box_value(winrt::hresult{ static_cast(0x80070057) }).as>() }); + } + + int32_t Class::CountReferenceHResultList(IVector> const& value) + { + return value ? static_cast(value.Size()) : 0; + } + WF::IInspectable Class::EmptyString() { return winrt::box_value(hstring{}); diff --git a/src/Tests/TestComponentCSharp/Class.h b/src/Tests/TestComponentCSharp/Class.h index 429324f62f..11fd9af6ba 100644 --- a/src/Tests/TestComponentCSharp/Class.h +++ b/src/Tests/TestComponentCSharp/Class.h @@ -447,6 +447,11 @@ namespace winrt::TestComponentCSharp::implementation static Windows::Foundation::IReference BoxedHResult(); static Windows::Foundation::IReference RoundtripHResult(Windows::Foundation::IReference const& value); + static Windows::Foundation::Collections::IVector> GetReferenceTypeNameList(); + static int32_t CountReferenceTypeNameList(Windows::Foundation::Collections::IVector> const& value); + static Windows::Foundation::Collections::IVector> GetReferenceHResultList(); + static int32_t CountReferenceHResultList(Windows::Foundation::Collections::IVector> const& value); + static Windows::Foundation::IInspectable EmptyString(); static Windows::Foundation::IInspectable BoxedDelegate(); static Windows::Foundation::IInspectable BoxedEnum(); diff --git a/src/Tests/TestComponentCSharp/TestComponentCSharp.idl b/src/Tests/TestComponentCSharp/TestComponentCSharp.idl index ff362c7f47..7ec779df8c 100644 --- a/src/Tests/TestComponentCSharp/TestComponentCSharp.idl +++ b/src/Tests/TestComponentCSharp/TestComponentCSharp.idl @@ -530,6 +530,15 @@ namespace TestComponentCSharp static Windows.Foundation.IReference BoxedHResult{ get; }; static Windows.Foundation.IReference RoundtripHResult(Windows.Foundation.IReference value); + // Generic collections of these reference-projected types: 'IVector>' and + // 'IVector>' both project to 'IList' / 'IList', which collide + // with the canonical 'IVector' / 'IVector' IIDs. These members exist to verify the + // projection still compiles; the matching unit tests document the (broken) runtime behavior. + static Windows.Foundation.Collections.IVector > GetReferenceTypeNameList(); + static Int32 CountReferenceTypeNameList(Windows.Foundation.Collections.IVector > value); + static Windows.Foundation.Collections.IVector > GetReferenceHResultList(); + static Int32 CountReferenceHResultList(Windows.Foundation.Collections.IVector > value); + // Other static Object EmptyString { get; }; diff --git a/src/Tests/UnitTest/TestComponentCSharp_Tests.cs b/src/Tests/UnitTest/TestComponentCSharp_Tests.cs index cb7df1abda..1fb177ac69 100644 --- a/src/Tests/UnitTest/TestComponentCSharp_Tests.cs +++ b/src/Tests/UnitTest/TestComponentCSharp_Tests.cs @@ -3786,6 +3786,50 @@ public void ReferenceHResultProjectsAsException() Assert.IsNull(Class.RoundtripHResult(null)); } + [TestMethod] + public void ReferenceTypeNameListProjectsAsTypeList() + { + // 'IVector>' projects to the public 'IList', but the ABI marshaller keeps + // the 'Nullable' element marker, so it targets the correct 'IVector>' IID + // and boxes/unboxes each element via 'TypeMarshaller'. The list returned from native round-trips with + // usable 'Type' elements (this is the return-only direction, created entirely in C++). + IList list = Class.GetReferenceTypeNameList(); + + Assert.AreEqual(2, list.Count); + Assert.AreEqual(typeof(Class), list[0]); + Assert.AreEqual(typeof(int), list[1]); + } + + [TestMethod] + public void ReferenceTypeNameListAsParameter() + { + // Passing a managed 'IList' to a native 'IVector>' parameter (the native + // side only reads the count here, which is element-agnostic) + Assert.AreEqual(2, Class.CountReferenceTypeNameList(new List { typeof(Class), typeof(int) })); + } + + [TestMethod] + public void ReferenceHResultListProjectsAsExceptionList() + { + // 'IVector>' projects to the public 'IList', but the ABI marshaller keeps + // the 'Nullable' element marker, so it targets the correct 'IVector>' IID + // and boxes/unboxes each element via 'ExceptionMarshaller'. The list returned from native round-trips + // with usable 'Exception' elements (this is the return-only direction, created entirely in C++). + IList list = Class.GetReferenceHResultList(); + + Assert.AreEqual(1, list.Count); + Assert.IsNotNull(list[0]); + Assert.AreEqual(unchecked((int)0x80070057), list[0].HResult); // 'E_INVALIDARG' + } + + [TestMethod] + public void ReferenceHResultListAsParameter() + { + // Passing a managed 'IList' to a native 'IVector>' parameter (the native + // side only reads the count here, which is element-agnostic) + Assert.AreEqual(2, Class.CountReferenceHResultList(new List { new ArgumentException(), new InvalidOperationException() })); + } + [TestMethod] public void TypeInfoGenerics() { From be64b9fdf063d711b24c079055a24d9ff7b04ac4 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 20 Jun 2026 12:55:40 -0700 Subject: [PATCH 4/5] Throw NotSupportedException for generics over IReference/IReference The scalar 'IReference' and 'IReference' cases project as bare 'System.Type' and 'System.Exception' and marshal correctly. However, when one of these is nested inside a generic instantiation (e.g. 'IVector>'), the projection writer emitted an UnsafeAccessor referencing a 'WinRT.Interop' marshaller whose name embeds 'Nullable' / 'Nullable'. Those are not constructible types, so the interop generator never produces such a marshaller and the call failed at runtime with 'Could not resolve type ...Marshaller'. Detect this unsupported shape and emit 'throw new NotSupportedException()' as the method body instead, on both the RCW caller and CCW (Do_Abi) emission paths. The public member signature is unchanged (it still projects as 'IList' / 'IList' and compiles), only its body becomes the throw. The scalar case is unaffected. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Extensions/TypeSignatureExtensions.cs | 57 +++++++++++++++++++ .../Factories/AbiMethodBodyFactory.DoAbi.cs | 15 +++++ .../AbiMethodBodyFactory.RcwCaller.cs | 17 ++++++ .../Factories/AbiMethodBodyFactory.cs | 32 ++++++++++- 4 files changed, 120 insertions(+), 1 deletion(-) diff --git a/src/WinRT.Projection.Writer/Extensions/TypeSignatureExtensions.cs b/src/WinRT.Projection.Writer/Extensions/TypeSignatureExtensions.cs index b98a20625d..f6da4b9ca7 100644 --- a/src/WinRT.Projection.Writer/Extensions/TypeSignatureExtensions.cs +++ b/src/WinRT.Projection.Writer/Extensions/TypeSignatureExtensions.cs @@ -119,6 +119,63 @@ public bool IsGenericInstance() return sig is GenericInstanceTypeSignature; } + /// + /// Returns whether the signature is a Windows.Foundation.IReference`1 instantiation whose + /// argument is a Windows Runtime value type that projects to a .NET reference type, namely + /// Windows.UI.Xaml.Interop.TypeName (projected as ) or + /// Windows.Foundation.HResult (projected as ). + /// + /// + /// Unlike other IReference<T> shapes, these two project to a bare reference type + /// rather than a , because and + /// are reference types. They marshal correctly as a scalar + /// (via ABI.System.TypeMarshaller / ABI.System.ExceptionMarshaller), but cannot + /// appear inside a generic instantiation (see ). + /// + /// if the signature is IReference<TypeName> or IReference<HResult>; otherwise . + public bool IsNullableTOfReferenceType() + { + if (!sig.IsNullableT()) + { + return false; + } + + TypeSignature? inner = sig.GetNullableInnerType(); + + return inner is not null && (inner.IsSystemType() || inner.IsHResultException()); + } + + /// + /// Returns whether the signature is a generic instantiation whose type-argument tree + /// (recursively) contains an shape. + /// + /// + /// Such instantiations (e.g. IVector<IReference<TypeName>>) cannot be + /// marshalled: the WinRT.Interop marshaller name would embed a Nullable<Type> + /// or Nullable<Exception>, which is not a constructible type, so the interop generator + /// never produces a matching marshaller. The scalar 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. + /// + /// if a reference-projected IReference<T> is nested as a type argument; otherwise . + 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; + } + /// /// Returns whether the signature has the "ABI reference-pointer" shape: it crosses the /// ABI as a void* / IInspectable*. That covers (HSTRING), diff --git a/src/WinRT.Projection.Writer/Factories/AbiMethodBodyFactory.DoAbi.cs b/src/WinRT.Projection.Writer/Factories/AbiMethodBodyFactory.DoAbi.cs index 394c19f5f7..21a3b18071 100644 --- a/src/WinRT.Projection.Writer/Factories/AbiMethodBodyFactory.DoAbi.cs +++ b/src/WinRT.Projection.Writer/Factories/AbiMethodBodyFactory.DoAbi.cs @@ -49,6 +49,21 @@ internal static void EmitDoAbiBodyIfSimple(IndentedTextWriter writer, Projection $"on '{ifaceFullName}'. Events should dispatch through EmitDoAbiAddEvent / EmitDoAbiRemoveEvent."); } + // Generic instantiations over 'IReference' / 'IReference' 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()) { diff --git a/src/WinRT.Projection.Writer/Factories/AbiMethodBodyFactory.RcwCaller.cs b/src/WinRT.Projection.Writer/Factories/AbiMethodBodyFactory.RcwCaller.cs index d6b88dbec9..26f23df563 100644 --- a/src/WinRT.Projection.Writer/Factories/AbiMethodBodyFactory.RcwCaller.cs +++ b/src/WinRT.Projection.Writer/Factories/AbiMethodBodyFactory.RcwCaller.cs @@ -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' / 'IReference' cannot be marshalled: + // 'System.Type' and 'System.Exception' are reference types with no 'Nullable' 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); diff --git a/src/WinRT.Projection.Writer/Factories/AbiMethodBodyFactory.cs b/src/WinRT.Projection.Writer/Factories/AbiMethodBodyFactory.cs index 1eceaff0d2..ae56f81a94 100644 --- a/src/WinRT.Projection.Writer/Factories/AbiMethodBodyFactory.cs +++ b/src/WinRT.Projection.Writer/Factories/AbiMethodBodyFactory.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using WindowsRuntime.ProjectionWriter.Models; + namespace WindowsRuntime.ProjectionWriter.Factories; /// @@ -16,4 +18,32 @@ namespace WindowsRuntime.ProjectionWriter.Factories; /// AbiMethodBodyFactory.MarshallerDispatch.cs - Per-marshaller ConvertToManaged/Unmanaged dispatch helpers. /// /// -internal static partial class AbiMethodBodyFactory; \ No newline at end of file +internal static partial class AbiMethodBodyFactory +{ + /// + /// Returns whether the method's return type or any parameter type involves a generic + /// instantiation over IReference<TypeName> / IReference<HResult>, which + /// cannot be marshalled (see ). + /// When this is the case, the projected member's body is replaced with a throw instead of + /// emitting a reference to a WinRT.Interop marshaller that the interop generator cannot produce. + /// + /// The interface method signature being emitted. + /// if the method cannot be marshalled; otherwise . + 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; + } +} From e8f622781f9a5133a2bcae57371e3a22127d452f Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 20 Jun 2026 13:00:34 -0700 Subject: [PATCH 5/5] Update generic IReference tests to assert NotSupportedException The collection cases over 'IReference' / 'IReference' cannot be marshalled, so the projection now emits a throwing body for them. Update the four generic tests to validate that calling these projected members throws 'NotSupportedException', covering both the return-only and parameter directions for each type, and rename them to reflect the asserted behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../UnitTest/TestComponentCSharp_Tests.cs | 48 ++++++++----------- 1 file changed, 20 insertions(+), 28 deletions(-) diff --git a/src/Tests/UnitTest/TestComponentCSharp_Tests.cs b/src/Tests/UnitTest/TestComponentCSharp_Tests.cs index 1fb177ac69..bb08d13298 100644 --- a/src/Tests/UnitTest/TestComponentCSharp_Tests.cs +++ b/src/Tests/UnitTest/TestComponentCSharp_Tests.cs @@ -3787,47 +3787,39 @@ public void ReferenceHResultProjectsAsException() } [TestMethod] - public void ReferenceTypeNameListProjectsAsTypeList() + public void ReferenceTypeNameListReturnThrowsNotSupported() { - // 'IVector>' projects to the public 'IList', but the ABI marshaller keeps - // the 'Nullable' element marker, so it targets the correct 'IVector>' IID - // and boxes/unboxes each element via 'TypeMarshaller'. The list returned from native round-trips with - // usable 'Type' elements (this is the return-only direction, created entirely in C++). - IList list = Class.GetReferenceTypeNameList(); - - Assert.AreEqual(2, list.Count); - Assert.AreEqual(typeof(Class), list[0]); - Assert.AreEqual(typeof(int), list[1]); + // 'IVector>' projects its public surface as 'IList', but it cannot be + // marshalled: 'System.Type' is a reference type, so there is no valid 'Nullable' 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(() => Class.GetReferenceTypeNameList()); } [TestMethod] - public void ReferenceTypeNameListAsParameter() + public void ReferenceTypeNameListParameterThrowsNotSupported() { - // Passing a managed 'IList' to a native 'IVector>' parameter (the native - // side only reads the count here, which is element-agnostic) - Assert.AreEqual(2, Class.CountReferenceTypeNameList(new List { typeof(Class), typeof(int) })); + // Same limitation as the return direction: passing a managed 'IList' to a native + // 'IVector>' parameter throws 'NotSupportedException' + Assert.ThrowsExactly(() => Class.CountReferenceTypeNameList(new List { typeof(Class), typeof(int) })); } [TestMethod] - public void ReferenceHResultListProjectsAsExceptionList() + public void ReferenceHResultListReturnThrowsNotSupported() { - // 'IVector>' projects to the public 'IList', but the ABI marshaller keeps - // the 'Nullable' element marker, so it targets the correct 'IVector>' IID - // and boxes/unboxes each element via 'ExceptionMarshaller'. The list returned from native round-trips - // with usable 'Exception' elements (this is the return-only direction, created entirely in C++). - IList list = Class.GetReferenceHResultList(); - - Assert.AreEqual(1, list.Count); - Assert.IsNotNull(list[0]); - Assert.AreEqual(unchecked((int)0x80070057), list[0].HResult); // 'E_INVALIDARG' + // 'IVector>' projects its public surface as 'IList', but it cannot be + // marshalled: 'System.Exception' is a reference type, so there is no valid 'Nullable' 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(() => Class.GetReferenceHResultList()); } [TestMethod] - public void ReferenceHResultListAsParameter() + public void ReferenceHResultListParameterThrowsNotSupported() { - // Passing a managed 'IList' to a native 'IVector>' parameter (the native - // side only reads the count here, which is element-agnostic) - Assert.AreEqual(2, Class.CountReferenceHResultList(new List { new ArgumentException(), new InvalidOperationException() })); + // Same limitation as the return direction: passing a managed 'IList' to a native + // 'IVector>' parameter throws 'NotSupportedException' + Assert.ThrowsExactly(() => Class.CountReferenceHResultList(new List { new ArgumentException(), new InvalidOperationException() })); } [TestMethod]