diff --git a/src/Tests/TestComponentCSharp/Class.cpp b/src/Tests/TestComponentCSharp/Class.cpp index 602fcddbd..58a4452cd 100644 --- a/src/Tests/TestComponentCSharp/Class.cpp +++ b/src/Tests/TestComponentCSharp/Class.cpp @@ -1891,6 +1891,50 @@ 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; + } + + 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 ed75bbedf..11fd9af6b 100644 --- a/src/Tests/TestComponentCSharp/Class.h +++ b/src/Tests/TestComponentCSharp/Class.h @@ -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 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::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 3c93d8434..7ec779df8 100644 --- a/src/Tests/TestComponentCSharp/TestComponentCSharp.idl +++ b/src/Tests/TestComponentCSharp/TestComponentCSharp.idl @@ -520,6 +520,25 @@ 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); + + // 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 1a6b1dda1..bb08d1329 100644 --- a/src/Tests/UnitTest/TestComponentCSharp_Tests.cs +++ b/src/Tests/UnitTest/TestComponentCSharp_Tests.cs @@ -3758,6 +3758,70 @@ 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 ReferenceTypeNameListReturnThrowsNotSupported() + { + // '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 ReferenceTypeNameListParameterThrowsNotSupported() + { + // 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 ReferenceHResultListReturnThrowsNotSupported() + { + // '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 ReferenceHResultListParameterThrowsNotSupported() + { + // 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] public void TypeInfoGenerics() { diff --git a/src/WinRT.Projection.Writer/Extensions/TypeSignatureExtensions.cs b/src/WinRT.Projection.Writer/Extensions/TypeSignatureExtensions.cs index b98a20625..f6da4b9ca 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 394c19f5f..21a3b1807 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 d6b88dbec..26f23df56 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 1eceaff0d..ae56f81a9 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; + } +} diff --git a/src/WinRT.Projection.Writer/Helpers/TypedefNameWriter.cs b/src/WinRT.Projection.Writer/Helpers/TypedefNameWriter.cs index d23bfdaf0..32ea285dc 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)