Skip to content

Project IReference of TypeName/HResult as Type/Exception - #2461

Open
Sergio0694 wants to merge 5 commits into
staging/3.0from
sergio0694-ireference-typename-projection
Open

Project IReference of TypeName/HResult as Type/Exception#2461
Sergio0694 wants to merge 5 commits into
staging/3.0from
sergio0694-ireference-typename-projection

Conversation

@Sergio0694

Copy link
Copy Markdown
Member

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.Type and Windows.Foundation.HResult -> System.Exception. These now project to Type / Exception instead of the invalid System.Nullable<Type> / System.Nullable<Exception>.

Motivation

IReference<T> normally projects to System.Nullable<T>, which is correct for value-type inners. But TypeName and HResult are Windows Runtime value types that project to the .NET reference types System.Type and System.Exception. Nullable<Type> / Nullable<Exception> are not valid C# (the type argument of Nullable<T> must be a non-nullable value type), so any metadata containing IReference<TypeName> or IReference<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 / ExceptionMarshaller BoxToUnmanaged / UnboxToManaged via IID_IReferenceOfType / IID_IReferenceOfException); only the projected type name was wrong.

Changes

  • src/WinRT.Projection.Writer/Helpers/TypedefNameWriter.cs: when projecting Windows.Foundation.IReference<T>, detect inners whose projection is a .NET reference type (TypeName -> Type, HResult -> Exception, plus a literal System.Type) and emit the inner type directly instead of wrapping it in System.Nullable<>.
  • src/Tests/TestComponentCSharp/ (TestComponentCSharp.idl, Class.h, Class.cpp): add scalar IReference<TypeName> and IReference<HResult> members - a native-boxed property (BoxedTypeName / BoxedHResult) and a round-trip method (RoundtripTypeName / RoundtripHResult).
  • src/Tests/UnitTest/TestComponentCSharp_Tests.cs: add ReferenceTypeNameProjectsAsType and ReferenceHResultProjectsAsException, 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: since Type / Exception are reference types there is no distinct Nullable<> managed shape, so such a collection is indistinguishable from IVector<TypeName> / IVector<HResult> at the interop layer.

@Sergio0694 Sergio0694 added bug Something isn't working CsWinRT 3.0 labels Jun 20, 2026
@Sergio0694
Sergio0694 requested a review from manodasanW June 20, 2026 00:49
@Sergio0694
Sergio0694 force-pushed the sergio0694-ireference-typename-projection branch from 0e19ee7 to 2b39a90 Compare June 20, 2026 01:27
@Sergio0694
Sergio0694 marked this pull request as draft June 20, 2026 01:46
@Sergio0694
Sergio0694 force-pushed the sergio0694-ireference-typename-projection branch 2 times, most recently from fed7430 to e8f6227 Compare June 20, 2026 20:11
@Sergio0694
Sergio0694 marked this pull request as ready for review July 28, 2026 18:02
Sergio0694 and others added 5 commits July 28, 2026 11:02
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

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

@Sergio0694
Sergio0694 force-pushed the sergio0694-ireference-typename-projection branch from e8f6227 to 9d42deb Compare July 28, 2026 18:02
[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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working CsWinRT 3.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants