Project IReference of TypeName/HResult as Type/Exception - #2461
Project IReference of TypeName/HResult as Type/Exception#2461Sergio0694 wants to merge 5 commits into
Conversation
0e19ee7 to
2b39a90
Compare
fed7430 to
e8f6227
Compare
Add members to TestComponentCSharp exercising the scalar IReference<T> projection for the two WinRT value types that map to .NET reference types: IReference<Windows.UI.Xaml.Interop.TypeName> -> System.Type and IReference<Windows.Foundation.HResult> -> 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<Type> / Nullable<Exception> for these instantiations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…able<> 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<T> of those must project to the inner type directly (Type / Exception), not the invalid System.Nullable<Type/Exception> (the type argument of Nullable<T> 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>
Add IVector<IReference<TypeName>> and IVector<IReference<HResult>> 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<Type> / IList<Exception> via the scalar fix, while the ABI marshaller keeps the Nullable<T> element marker and so targets the IVector<IReference<T>> IID with per-element box/unbox. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…eference<HResult> The scalar 'IReference<TypeName>' and 'IReference<HResult>' 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<IReference<TypeName>>'), the projection writer emitted an UnsafeAccessor referencing a 'WinRT.Interop' marshaller whose name embeds 'Nullable<Type>' / 'Nullable<Exception>'. 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<Type>' / 'IList<Exception>' and compiles), only its body becomes the throw. The scalar case is unaffected. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The collection cases over 'IReference<TypeName>' / 'IReference<HResult>' 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>
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
e8f6227 to
9d42deb
Compare
| [TestMethod] | ||
| public void ReferenceTypeNameListReturnThrowsNotSupported() | ||
| { | ||
| // 'IVector<IReference<TypeName>>' projects its public surface as 'IList<Type>', but it cannot be |
There was a problem hiding this comment.
What was the 2.x behavior here?
There was a problem hiding this comment.
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`1 → System.Nullable, Windows.UI.Xaml.Interop.TypeName → System.Type and Windows.Foundation.HResult → System.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.
Summary
Fix the C# projection of
IReference<T>for the two Windows Runtime value types that map to .NET reference types:Windows.UI.Xaml.Interop.TypeName->System.TypeandWindows.Foundation.HResult->System.Exception. These now project toType/Exceptioninstead of the invalidSystem.Nullable<Type>/System.Nullable<Exception>.Motivation
IReference<T>normally projects toSystem.Nullable<T>, which is correct for value-type inners. ButTypeNameandHResultare Windows Runtime value types that project to the .NET reference typesSystem.TypeandSystem.Exception.Nullable<Type>/Nullable<Exception>are not valid C# (the type argument ofNullable<T>must be a non-nullable value type), so any metadata containingIReference<TypeName>orIReference<HResult>produced a projection that failed to compile. A reference type is already nullable, so the projection should simply be the inner type. This mirrors the equivalent CsWinRT 2.x fix (#2103).The marshalling itself was already fully supported by the runtime (
TypeMarshaller/ExceptionMarshallerBoxToUnmanaged/UnboxToManagedviaIID_IReferenceOfType/IID_IReferenceOfException); only the projected type name was wrong.Changes
src/WinRT.Projection.Writer/Helpers/TypedefNameWriter.cs: when projectingWindows.Foundation.IReference<T>, detect inners whose projection is a .NET reference type (TypeName->Type,HResult->Exception, plus a literalSystem.Type) and emit the inner type directly instead of wrapping it inSystem.Nullable<>.src/Tests/TestComponentCSharp/(TestComponentCSharp.idl,Class.h,Class.cpp): add scalarIReference<TypeName>andIReference<HResult>members - a native-boxed property (BoxedTypeName/BoxedHResult) and a round-trip method (RoundtripTypeName/RoundtripHResult).src/Tests/UnitTest/TestComponentCSharp_Tests.cs: addReferenceTypeNameProjectsAsTypeandReferenceHResultProjectsAsException, covering both marshalling directions and the null case.Notes
The test members and the projection-writer fix are committed separately so CI can demonstrate the tests failing to build (invalid
Nullable<Type>/Nullable<Exception>) before the fix lands, then passing afterward. Collections of these types (IVector<IReference<TypeName>>etc.) are intentionally out of scope: sinceType/Exceptionare reference types there is no distinctNullable<>managed shape, so such a collection is indistinguishable fromIVector<TypeName>/IVector<HResult>at the interop layer.