From 367643d4db50133967b26328bd76f89137820030 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 2 Jul 2026 15:06:59 -0700 Subject: [PATCH 1/8] Add WindowsRuntimeNativeExposedTypeAttribute Add a public assembly-level attribute in the WindowsRuntime.InteropServices namespace that lets developers explicitly opt a projected Windows Runtime class type into CCW marshalling code generation, which the interop generator normally skips for projected types. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- ...indowsRuntimeNativeExposedTypeAttribute.cs | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 src/WinRT.Runtime2/InteropServices/Attributes/WindowsRuntimeNativeExposedTypeAttribute.cs diff --git a/src/WinRT.Runtime2/InteropServices/Attributes/WindowsRuntimeNativeExposedTypeAttribute.cs b/src/WinRT.Runtime2/InteropServices/Attributes/WindowsRuntimeNativeExposedTypeAttribute.cs new file mode 100644 index 0000000000..8f5cf79230 --- /dev/null +++ b/src/WinRT.Runtime2/InteropServices/Attributes/WindowsRuntimeNativeExposedTypeAttribute.cs @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace WindowsRuntime.InteropServices; + +/// +/// Indicates a projected Windows Runtime class type for which CCW (COM Callable Wrapper) marshalling code +/// should be generated, so that its instances can be marshalled to native code through a CCW. +/// +/// +/// +/// CCW marshalling code is normally generated automatically for all managed types that implement one or more +/// Windows Runtime interfaces. Projected Windows Runtime types are an exception, as they are backed by native +/// objects and are marshalled by unwrapping the underlying native object directly, which means they normally +/// never require a CCW. This attribute allows explicitly opting a projected class type into CCW marshalling +/// code generation, for the niche scenarios where such support is required. +/// +/// +/// One such scenario involves using a projected collection type as an items source for a XAML control. +/// Consider using a DependencyObjectCollection as the items source for an ItemsControl. The +/// control will query the object assigned to its items source (which is marshalled as an IInspectable +/// value) for the IBindableIterable and IIterable<IInspectable> interfaces. This normally +/// succeeds when assigning most collection types. However, DependencyObjectCollection implements neither +/// of those interfaces. It implements IIterable<DependencyObject>, and Windows Runtime generic +/// interfaces are not covariant on the native side, meaning each generic instantiation of a given generic +/// Windows Runtime interface has a completely different IID. +/// +/// +/// To handle this, XAML controls perform the following steps: +/// +/// They query the source object for IBindableIterable and IIterable<IInspectable>. +/// If that fails, they obtain a CCW from the source object and query that CCW instead. +/// +/// +/// +/// This might seem surprising, given that a projected type is backed by a native object, and so it would just +/// be unwrapped when marshalled, with no CCW involved. The .NET runtime has dedicated logic for this case, +/// where it will perform the following steps: +/// +/// It creates an RCW (Runtime Callable Wrapper) for the native object. +/// It uses ComWrappers to obtain a CCW for that RCW, to be used as a proxy. +/// +/// That is, in this scenario the reference from the control to the object is transformed from a direct +/// reference into a reference to a CCW that wraps an RCW over the original native object. +/// +/// +/// Because the RCW implements IEnumerable<DependencyObject>, and that interface is covariant in +/// C#, marshalling it to native makes the query for IIterable<IInspectable> succeed, by going +/// through this reference cycle with the proxy object. This works because CsWinRT computes all variant versions +/// of each implemented interface on managed objects, and generates vtables for all of them. As a consequence, +/// such native objects can only be used as items sources from .NET applications, and not from C++ applications. +/// +/// +/// Applying this attribute to a projected class type explicitly requests that a CCW vtable be generated for it, +/// which would not normally be present, since the type is just a projection. +/// +/// +[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true, Inherited = false)] +public sealed class WindowsRuntimeNativeExposedTypeAttribute : Attribute +{ + /// + /// Creates a new instance with the specified parameters. + /// + /// The projected Windows Runtime class type to generate CCW marshalling code for. + public WindowsRuntimeNativeExposedTypeAttribute(Type type) + { + Type = type; + } + + /// + /// Gets the projected Windows Runtime class type to generate CCW marshalling code for. + /// + public Type Type { get; } +} From a8e80e0b484baf0a01a1def8584d634e81d092a4 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 2 Jul 2026 15:12:40 -0700 Subject: [PATCH 2/8] Generate CCW marshalling code for native exposed types in the interop generator Discover '[assembly: WindowsRuntimeNativeExposedType(typeof(X))]' attributes and route the referenced types through the user-defined type CCW pipeline, bypassing the check that normally skips projected Windows Runtime types. All other discovery filters still apply, so opting in a type that does not need CCW support is a safe no-op. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Discovery/InteropTypeDiscovery.cs | 14 +++- .../Errors/WellKnownInteropExceptions.cs | 16 ++++ .../Generation/InteropGenerator.Discover.cs | 78 +++++++++++++++++++ .../References/InteropReferences.cs | 5 ++ 4 files changed, 110 insertions(+), 3 deletions(-) diff --git a/src/WinRT.Interop.Generator/Discovery/InteropTypeDiscovery.cs b/src/WinRT.Interop.Generator/Discovery/InteropTypeDiscovery.cs index c044ab3e13..2595eb1256 100644 --- a/src/WinRT.Interop.Generator/Discovery/InteropTypeDiscovery.cs +++ b/src/WinRT.Interop.Generator/Discovery/InteropTypeDiscovery.cs @@ -80,6 +80,11 @@ public static void TryTrackTypeHierarchyType( /// The instance to use. /// The instance to use. /// The module currently being analyzed. + /// + /// Whether the type was explicitly opted into CCW marshalling code generation via + /// [WindowsRuntimeNativeExposedType]. When this is , projected Windows + /// Runtime types are not skipped, as they would otherwise never get CCW marshalling code generated. + /// /// /// This method expects to either be non-generic, or /// to have be a fully constructed signature for it. @@ -91,7 +96,8 @@ public static void TryTrackExposedUserDefinedType( InteropGeneratorDiscoveryState discoveryState, InteropDefinitions interopDefinitions, InteropReferences interopReferences, - ModuleDefinition module) + ModuleDefinition module, + bool isNativeExposedType = false) { // Ignore types that should explicitly be excluded if (TypeExclusions.IsExcluded(typeSignature, interopReferences)) @@ -112,8 +118,10 @@ public static void TryTrackExposedUserDefinedType( return; } - // We can skip all projected Windows Runtime types early, as they don't need CCW support - if (typeDefinition.IsProjectedWindowsRuntimeType) + // We can skip all projected Windows Runtime types early, as they don't need CCW support. The only + // exception is when a type has been explicitly opted into CCW marshalling code generation, in which + // case we do want to process it as any other user-defined type (see the analyzer for more details). + if (typeDefinition.IsProjectedWindowsRuntimeType && !isNativeExposedType) { return; } diff --git a/src/WinRT.Interop.Generator/Errors/WellKnownInteropExceptions.cs b/src/WinRT.Interop.Generator/Errors/WellKnownInteropExceptions.cs index 6df46e720a..0c215a73b1 100644 --- a/src/WinRT.Interop.Generator/Errors/WellKnownInteropExceptions.cs +++ b/src/WinRT.Interop.Generator/Errors/WellKnownInteropExceptions.cs @@ -848,6 +848,22 @@ public static WellKnownInteropException DiscoverActivationFactoryTypesError(stri return Exception(97, $"Failed to discover activation factory types for module '{name}'.", exception); } + /// + /// Failed to discover native exposed types. + /// + public static WellKnownInteropException DiscoverNativeExposedTypesError(string? name, Exception exception) + { + return Exception(98, $"Failed to discover native exposed types (from '[WindowsRuntimeNativeExposedType]' attributes) for module '{name}'.", exception); + } + + /// + /// Failed to resolve a type specified in a '[WindowsRuntimeNativeExposedType]' attribute. + /// + public static WellKnownInteropWarning NativeExposedTypeNotResolvedWarning(TypeSignature typeSignature, ModuleDefinition module) + { + return Warning(99, $"Failed to resolve the type '{typeSignature}' specified in a '[WindowsRuntimeNativeExposedType]' attribute while processing module '{module}': CCW marshalling code for it will not be generated."); + } + /// /// Creates a new exception with the specified id and message. /// diff --git a/src/WinRT.Interop.Generator/Generation/InteropGenerator.Discover.cs b/src/WinRT.Interop.Generator/Generation/InteropGenerator.Discover.cs index 42760ba3c2..8d7db67314 100644 --- a/src/WinRT.Interop.Generator/Generation/InteropGenerator.Discover.cs +++ b/src/WinRT.Interop.Generator/Generation/InteropGenerator.Discover.cs @@ -270,6 +270,11 @@ private static void LoadAndProcessModule( args.Token.ThrowIfCancellationRequested(); + // Discover all types explicitly opted into CCW marshalling code generation + DiscoverNativeExposedTypes(args, discoveryState, module); + + args.Token.ThrowIfCancellationRequested(); + // Discover all generic type instantiations DiscoverGenericTypeInstantiations(args, discoveryState, module); @@ -367,6 +372,79 @@ private static void DiscoverExposedUserDefinedTypes( } } + /// + /// Discovers all types explicitly opted into CCW marshalling code generation via [WindowsRuntimeNativeExposedType]. + /// + /// The arguments for this invocation. + /// The discovery state for this invocation. + /// The module currently being analyzed. + /// + /// Projected Windows Runtime types are normally skipped when discovering exposed user-defined types, as they are backed + /// by native objects and never need CCW marshalling code. This method handles the niche cases where a projected type + /// does need such code, by processing the types explicitly requested through the attribute as any other user-defined type. + /// + private static void DiscoverNativeExposedTypes( + InteropGeneratorArgs args, + InteropGeneratorDiscoveryState discoveryState, + ModuleDefinition module) + { + try + { + // Optimization: only implementation assemblies can carry the attribute, as Windows Runtime + // reference assemblies only contain projections and no user authored assembly metadata. + if (module.Assembly is not { IsWindowsRuntimeReferenceAssembly: false } assembly) + { + return; + } + + InteropReferences interopReferences = CreateDiscoveryInteropReferences(module); + InteropDefinitions interopDefinitions = new( + interopReferences: interopReferences, + windowsRuntimeSdkProjectionModule: discoveryState.WindowsRuntimeSdkProjectionModule!, + windowsRuntimeSdkXamlProjectionModule: discoveryState.WindowsRuntimeSdkXamlProjectionModule, + windowsRuntimeProjectionModule: discoveryState.WindowsRuntimeProjectionModule, + windowsRuntimeComponentModule: discoveryState.WindowsRuntimeComponentModule); + + // Enumerate all '[assembly: WindowsRuntimeNativeExposedType(typeof())]' attributes on the module + foreach (CustomAttribute attribute in assembly.FindCustomAttributes("WindowsRuntime.InteropServices"u8, "WindowsRuntimeNativeExposedTypeAttribute"u8)) + { + args.Token.ThrowIfCancellationRequested(); + + // Match '[WindowsRuntimeNativeExposedType(typeof())]' and extract the exposed type + if (attribute.Signature is not { FixedArguments: [{ Element: TypeSignature exposedType }] }) + { + continue; + } + + // Resolve the exposed type to its definition. If we can't, it likely means a reference is + // missing. We just warn and move on, as the rest of the discovery can still be completed. + if (!exposedType.TryResolve(interopReferences.RuntimeContext, out TypeDefinition? typeDefinition)) + { + WellKnownInteropExceptions.NativeExposedTypeNotResolvedWarning(exposedType, module).LogOrThrow(args.TreatWarningsAsErrors); + + continue; + } + + // Track the exposed type as a user-defined type, bypassing the check that would normally skip + // projected types. All other filters still apply, so if the type is not actually a projected + // type that would benefit from CCW support (e.g. it's an interface), this will just be a no-op. + InteropTypeDiscovery.TryTrackExposedUserDefinedType( + typeDefinition: typeDefinition, + typeSignature: typeDefinition.ToTypeSignature(), + args: args, + discoveryState: discoveryState, + interopDefinitions: interopDefinitions, + interopReferences: interopReferences, + module: module, + isNativeExposedType: true); + } + } + catch (Exception e) + { + WellKnownInteropExceptions.DiscoverNativeExposedTypesError(module.Name, e).ThrowOrAttach(e); + } + } + /// /// Discovers all generic type instantiations in a given assembly. /// diff --git a/src/WinRT.Interop.Generator/References/InteropReferences.cs b/src/WinRT.Interop.Generator/References/InteropReferences.cs index 77c1e2ed9d..045b8e5556 100644 --- a/src/WinRT.Interop.Generator/References/InteropReferences.cs +++ b/src/WinRT.Interop.Generator/References/InteropReferences.cs @@ -745,6 +745,11 @@ public InteropReferences( /// public TypeReference WindowsRuntimeMappedTypeAttribute => field ??= _windowsRuntimeModule.CreateTypeReference("WindowsRuntime.InteropServices"u8, "WindowsRuntimeMappedTypeAttribute"u8); + /// + /// Gets the for WindowsRuntime.InteropServices.WindowsRuntimeNativeExposedTypeAttribute. + /// + public TypeReference WindowsRuntimeNativeExposedTypeAttribute => field ??= _windowsRuntimeModule.CreateTypeReference("WindowsRuntime.InteropServices"u8, "WindowsRuntimeNativeExposedTypeAttribute"u8); + /// /// Gets the for WindowsRuntime.InteropServices.WindowsRuntimeComWrappersTypeMapGroup. /// From bf1c19e0ff12f7752bd3e186f37ea03b30c3bdc9 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 2 Jul 2026 15:25:10 -0700 Subject: [PATCH 3/8] Add analyzer diagnostics for '[WindowsRuntimeNativeExposedType]' usage Add three warning diagnostics (CSWINRT2018, CSWINRT2019, CSWINRT2020) reported by a new 'WindowsRuntimeNativeExposedTypeAnalyzer'. CSWINRT2018 warns when the target type cannot be instantiated (interfaces, abstract or static classes, and generic type definitions), CSWINRT2019 warns when the target type is not a projected Windows Runtime class (for which CCW marshalling code is already generated automatically), and CSWINRT2020 warns when the same type is targeted by more than one application of the attribute in the assembly. Applications in generated code are excluded from the diagnostics, so that a duplicate spanning user code and generated code only warns on the user application. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../AnalyzerReleases.Shipped.md | 5 +- ...WindowsRuntimeNativeExposedTypeAnalyzer.cs | 207 ++++++++++++++++++ .../Diagnostics/DiagnosticDescriptors.cs | 42 ++++ 3 files changed, 253 insertions(+), 1 deletion(-) create mode 100644 src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/WindowsRuntimeNativeExposedTypeAnalyzer.cs diff --git a/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md b/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md index 8acb415788..e1c6e605f8 100644 --- a/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md +++ b/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md @@ -23,4 +23,7 @@ CSWINRT2013 | WindowsRuntime.SourceGenerator | Warning | Invalid 'ContractVersio CSWINRT2014 | WindowsRuntime.SourceGenerator | Warning | API contract type missing 'ContractVersionAttribute' CSWINRT2015 | WindowsRuntime.SourceGenerator | Info | Public authored type missing version metadata CSWINRT2016 | WindowsRuntime.SourceGenerator | Warning | Public authored type missing 'ContractVersionAttribute' -CSWINRT2017 | WindowsRuntime.SourceGenerator | Warning | Public authored type mixing '[ContractVersion]' and '[Version]' \ No newline at end of file +CSWINRT2017 | WindowsRuntime.SourceGenerator | Warning | Public authored type mixing '[ContractVersion]' and '[Version]' +CSWINRT2018 | WindowsRuntime.SourceGenerator | Warning | '[WindowsRuntimeNativeExposedType]' target type cannot be instantiated +CSWINRT2019 | WindowsRuntime.SourceGenerator | Warning | '[WindowsRuntimeNativeExposedType]' target type is not a projected class +CSWINRT2020 | WindowsRuntime.SourceGenerator | Warning | Duplicate '[WindowsRuntimeNativeExposedType]' target type \ No newline at end of file diff --git a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/WindowsRuntimeNativeExposedTypeAnalyzer.cs b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/WindowsRuntimeNativeExposedTypeAnalyzer.cs new file mode 100644 index 0000000000..b73d9db1d9 --- /dev/null +++ b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/WindowsRuntimeNativeExposedTypeAnalyzer.cs @@ -0,0 +1,207 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace WindowsRuntime.SourceGenerator.Diagnostics; + +/// +/// A diagnostic analyzer that validates uses of [WindowsRuntimeNativeExposedType]. The attribute is only +/// meaningful when applied with a concrete, non generic projected Windows Runtime class type, as CCW marshalling +/// code is generated automatically for every other kind of type. It also reports redundant applications of the +/// attribute that target a type already used by another application in the same assembly. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class WindowsRuntimeNativeExposedTypeAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = + [ + DiagnosticDescriptors.NativeExposedTypeNotInstantiable, + DiagnosticDescriptors.NativeExposedTypeNotProjectedClass, + DiagnosticDescriptors.NativeExposedTypeDuplicate + ]; + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationAction(static context => + { + // Get the '[WindowsRuntimeNativeExposedType]' symbol + if (context.Compilation.GetTypeByMetadataName("WindowsRuntime.InteropServices.WindowsRuntimeNativeExposedTypeAttribute") is not { } nativeExposedTypeAttributeType) + { + return; + } + + // Get the '[WindowsRuntimeMetadata]' symbol, which is used to detect projected Windows Runtime types + if (context.Compilation.GetTypeByMetadataName("WindowsRuntime.WindowsRuntimeMetadataAttribute") is not { } windowsRuntimeMetadataAttributeType) + { + return; + } + + IAssemblySymbol assemblySymbol = context.Compilation.Assembly; + + // Collect the valid target types across all applications of the attribute in the assembly (including + // the ones in generated code), so that a type used both in user code and in generated code is still + // detected as a duplicate. This list is only used to report duplicate applications of the attribute. + List validTargetTypes = []; + + foreach (AttributeData attribute in assemblySymbol.GetAttributes(nativeExposedTypeAttributeType)) + { + if (attribute is { ConstructorArguments: [{ Value: ITypeSymbol targetType }] } && + Classify(targetType, windowsRuntimeMetadataAttributeType) is NativeExposedTypeKind.Valid) + { + validTargetTypes.Add(targetType); + } + } + + // Classify and report each application of the attribute. Applications in generated code are + // suppressed by the analysis framework, because this analyzer opts out of generated code analysis, + // so only applications authored in user code are ever reported. + foreach (AttributeData attribute in assemblySymbol.GetAttributes(nativeExposedTypeAttributeType)) + { + // Skip malformed applications, eg. where the argument does not bind to a type + if (attribute is not { ConstructorArguments: [{ Value: ITypeSymbol targetType }] }) + { + continue; + } + + Location? location = attribute.GetArgumentLocation(0, context.CancellationToken) ?? attribute.GetLocation(context.CancellationToken); + + NativeExposedTypeKind kind = Classify(targetType, windowsRuntimeMetadataAttributeType); + + // The type cannot be instantiated, so no CCW would ever be created for it + if (kind is NativeExposedTypeKind.NotInstantiable) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.NativeExposedTypeNotInstantiable, + location, + targetType)); + } + + // The type is not a projected class, so CCW marshalling code is already generated for it + else if (kind is NativeExposedTypeKind.NotProjectedClass) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.NativeExposedTypeNotProjectedClass, + location, + targetType)); + } + + // The type is valid, but it is also targeted by another application of the attribute + else if (CountOccurrences(validTargetTypes, targetType) > 1) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.NativeExposedTypeDuplicate, + location, + targetType)); + } + } + }); + } + + /// + /// Classifies a target type used with [WindowsRuntimeNativeExposedType]. + /// + /// The target type to classify. + /// The [WindowsRuntimeMetadata] symbol. + /// The classification for . + private static NativeExposedTypeKind Classify(ITypeSymbol type, INamedTypeSymbol windowsRuntimeMetadataAttributeType) + { + // A type that cannot be instantiated can never have a CCW created for it, so the attribute is meaningless + if (!IsInstantiable(type)) + { + return NativeExposedTypeKind.NotInstantiable; + } + + // A valid target is a non generic projected Windows Runtime class. CCW marshalling code is generated + // automatically for every other kind of type, so the attribute is only ever needed for projected classes. + return type is INamedTypeSymbol { TypeKind: TypeKind.Class, IsGenericType: false } namedType && + namedType.HasAttributeWithType(windowsRuntimeMetadataAttributeType) + ? NativeExposedTypeKind.Valid + : NativeExposedTypeKind.NotProjectedClass; + } + + /// + /// Counts how many times a given type appears in a sequence of types. + /// + /// The sequence of types to search. + /// The type to count occurrences of. + /// The number of times appears in . + private static int CountOccurrences(List types, ITypeSymbol type) + { + int count = 0; + + foreach (ITypeSymbol candidate in types) + { + if (SymbolEqualityComparer.Default.Equals(candidate, type)) + { + count++; + } + } + + return count; + } + + /// + /// Checks whether a given type can be instantiated. + /// + /// The type to check. + /// Whether can be instantiated. + private static bool IsInstantiable(ITypeSymbol type) + { + // Array types can always be instantiated + if (type is IArrayTypeSymbol) + { + return true; + } + + // Any other non named type (eg. a pointer type or a type parameter) cannot be instantiated + if (type is not INamedTypeSymbol namedType) + { + return false; + } + + // An unbound generic type definition (eg. 'typeof(List<>)') cannot be instantiated + if (namedType.IsUnboundGenericType) + { + return false; + } + + // Interfaces, abstract classes, and static classes cannot be instantiated + if (namedType.TypeKind is TypeKind.Interface || namedType.IsAbstract || namedType.IsStatic) + { + return false; + } + + // Classes, structs, enums, and delegates can be instantiated + return namedType.TypeKind is TypeKind.Class or TypeKind.Struct or TypeKind.Enum or TypeKind.Delegate; + } + + /// + /// The classification of a target type used with [WindowsRuntimeNativeExposedType]. + /// + private enum NativeExposedTypeKind + { + /// + /// The target type cannot be instantiated (eg. an interface, an abstract class, or a generic type definition). + /// + NotInstantiable, + + /// + /// The target type can be instantiated, but it is not a projected Windows Runtime class. + /// + NotProjectedClass, + + /// + /// The target type is a valid, non generic projected Windows Runtime class. + /// + Valid + } +} diff --git a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs index 65b19e356e..84567ce6ce 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs @@ -244,4 +244,46 @@ internal static partial class DiagnosticDescriptors isEnabledByDefault: true, description: "Public types in a Windows Runtime component should not mix '[ContractVersion]' and '[Version]', as these represent two different versioning schemes. Pick one and apply it consistently across the public API surface of the component.", helpLinkUri: "https://github.com/microsoft/CsWinRT"); + + /// + /// Gets a for a [WindowsRuntimeNativeExposedType] attribute applied with a target type that cannot be instantiated. + /// + public static readonly DiagnosticDescriptor NativeExposedTypeNotInstantiable = new( + id: "CSWINRT2018", + title: "'[WindowsRuntimeNativeExposedType]' target type cannot be instantiated", + messageFormat: """The type '{0}' is used as the target of a '[WindowsRuntimeNativeExposedType]' attribute, but it cannot be instantiated. Only concrete, non generic projected Windows Runtime class types can be used with this attribute, as no CCW would ever be created for a type that cannot be instantiated.""", + category: "WindowsRuntime.SourceGenerator", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "The '[WindowsRuntimeNativeExposedType]' attribute should only be applied with concrete, non generic projected Windows Runtime class types. Types that cannot be instantiated, such as interfaces, abstract classes, static classes, and generic type definitions, have no effect when used as the target of the attribute.", + helpLinkUri: "https://github.com/microsoft/CsWinRT", + customTags: WellKnownDiagnosticTags.CompilationEnd); + + /// + /// Gets a for a [WindowsRuntimeNativeExposedType] attribute applied with a target type that is not a projected Windows Runtime class. + /// + public static readonly DiagnosticDescriptor NativeExposedTypeNotProjectedClass = new( + id: "CSWINRT2019", + title: "'[WindowsRuntimeNativeExposedType]' target type is not a projected class", + messageFormat: """The type '{0}' is used as the target of a '[WindowsRuntimeNativeExposedType]' attribute, but it is not a projected Windows Runtime class type. CCW marshalling code is already generated automatically for all other types (such as user defined types, value types, delegates, arrays, and generic instantiations), so applying the attribute to '{0}' has no effect.""", + category: "WindowsRuntime.SourceGenerator", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "The '[WindowsRuntimeNativeExposedType]' attribute should only be applied with projected Windows Runtime class types. CCW marshalling code is generated automatically for all other types, so applying the attribute to them has no effect.", + helpLinkUri: "https://github.com/microsoft/CsWinRT", + customTags: WellKnownDiagnosticTags.CompilationEnd); + + /// + /// Gets a for a [WindowsRuntimeNativeExposedType] attribute applied with a target type that is already used by another application of the attribute in the same assembly. + /// + public static readonly DiagnosticDescriptor NativeExposedTypeDuplicate = new( + id: "CSWINRT2020", + title: "Duplicate '[WindowsRuntimeNativeExposedType]' target type", + messageFormat: """The type '{0}' is used as the target of more than one '[WindowsRuntimeNativeExposedType]' attribute in the same assembly. CCW marshalling code is only generated once per type, so the additional applications of the attribute for '{0}' have no effect and can be removed.""", + category: "WindowsRuntime.SourceGenerator", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "A given type should only be used as the target of a single '[WindowsRuntimeNativeExposedType]' attribute in an assembly. CCW marshalling code is only generated once per type, so additional applications of the attribute for the same type have no effect.", + helpLinkUri: "https://github.com/microsoft/CsWinRT", + customTags: WellKnownDiagnosticTags.CompilationEnd); } \ No newline at end of file From 144649473dd9ccbb9563ee9888d659651faa54bb Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 2 Jul 2026 15:44:54 -0700 Subject: [PATCH 4/8] Add analyzer tests for WindowsRuntimeNativeExposedTypeAttribute Add unit tests covering all three diagnostics (CSWINRT2018/2019/2020) for the 'WindowsRuntimeNativeExposedTypeAnalyzer', including valid projected class usage, non-instantiable and non-projected-class targets, duplicate applications, and suppression of diagnostics for applications that appear in generated code. Extend the analyzer test helper to allow adding an additional generated source file, so the generated-code suppression behavior can be validated. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Helpers/CSharpAnalyzerTest{TAnalyzer}.cs | 12 +- ...WindowsRuntimeNativeExposedTypeAnalyzer.cs | 229 ++++++++++++++++++ 2 files changed, 240 insertions(+), 1 deletion(-) create mode 100644 src/Tests/SourceGenerator2Test/Test_WindowsRuntimeNativeExposedTypeAnalyzer.cs diff --git a/src/Tests/SourceGenerator2Test/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs b/src/Tests/SourceGenerator2Test/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs index ad67904e34..8345282892 100644 --- a/src/Tests/SourceGenerator2Test/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs +++ b/src/Tests/SourceGenerator2Test/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs @@ -60,12 +60,14 @@ protected override ParseOptions CreateParseOptions() /// Whether to enable unsafe blocks. /// The language version to use to run the test. /// Whether to set the "CsWinRTComponent" MSBuild property to . + /// An additional source file to add to the compilation as generated code, or to not add one. public static Task VerifyAnalyzerAsync( string source, ReadOnlySpan expectedDiagnostics = default, bool allowUnsafeBlocks = true, LanguageVersion languageVersion = LanguageVersion.CSharp14, - bool isCsWinRTComponent = false) + bool isCsWinRTComponent = false, + string generatedSource = null) { CSharpAnalyzerTest test = new(allowUnsafeBlocks, languageVersion) { TestCode = source }; @@ -75,6 +77,14 @@ public static Task VerifyAnalyzerAsync( test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(Button).Assembly.Location)); test.TestState.ExpectedDiagnostics.AddRange([.. expectedDiagnostics]); + // Add an additional source file that is treated as generated code (the '.g.cs' suffix is recognized by the + // analysis framework). This is used to validate that diagnostics are suppressed for applications of the + // attribute that appear in generated code, when the analyzer opts out of generated code analysis. + if (generatedSource is not null) + { + test.TestState.Sources.Add(("NativeExposedTypes.g.cs", generatedSource)); + } + // Configure the desired MSBuild properties via a global analyzer config file if (isCsWinRTComponent) { diff --git a/src/Tests/SourceGenerator2Test/Test_WindowsRuntimeNativeExposedTypeAnalyzer.cs b/src/Tests/SourceGenerator2Test/Test_WindowsRuntimeNativeExposedTypeAnalyzer.cs new file mode 100644 index 0000000000..85dd530542 --- /dev/null +++ b/src/Tests/SourceGenerator2Test/Test_WindowsRuntimeNativeExposedTypeAnalyzer.cs @@ -0,0 +1,229 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using WindowsRuntime.SourceGenerator.Diagnostics; +using WindowsRuntime.SourceGenerator.Tests.Helpers; + +namespace WindowsRuntime.SourceGenerator.Tests; + +using VerifyCS = CSharpAnalyzerTest; + +/// +/// Tests for . +/// +[TestClass] +public sealed class Test_WindowsRuntimeNativeExposedTypeAnalyzer +{ + [TestMethod] + public async Task ProjectedClass_DoesNotWarn() + { + const string source = """ + using WindowsRuntime.InteropServices; + + [assembly: WindowsRuntimeNativeExposedType(typeof(Microsoft.UI.Xaml.DependencyObjectCollection))] + """; + + await VerifyCS.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task Interface_Warns() + { + const string source = """ + using WindowsRuntime.InteropServices; + + [assembly: WindowsRuntimeNativeExposedType({|CSWINRT2018:typeof(IMyInterface)|})] + + public interface IMyInterface; + """; + + await VerifyCS.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task AbstractClass_Warns() + { + const string source = """ + using WindowsRuntime.InteropServices; + + [assembly: WindowsRuntimeNativeExposedType({|CSWINRT2018:typeof(MyClass)|})] + + public abstract class MyClass; + """; + + await VerifyCS.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task StaticClass_Warns() + { + const string source = """ + using WindowsRuntime.InteropServices; + + [assembly: WindowsRuntimeNativeExposedType({|CSWINRT2018:typeof(MyClass)|})] + + public static class MyClass; + """; + + await VerifyCS.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task GenericTypeDefinition_Warns() + { + const string source = """ + using System.Collections.Generic; + using WindowsRuntime.InteropServices; + + [assembly: WindowsRuntimeNativeExposedType({|CSWINRT2018:typeof(List<>)|})] + """; + + await VerifyCS.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task UserDefinedClass_Warns() + { + const string source = """ + using WindowsRuntime.InteropServices; + + [assembly: WindowsRuntimeNativeExposedType({|CSWINRT2019:typeof(MyClass)|})] + + public sealed class MyClass; + """; + + await VerifyCS.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task ValueType_Warns() + { + const string source = """ + using WindowsRuntime.InteropServices; + + [assembly: WindowsRuntimeNativeExposedType({|CSWINRT2019:typeof(MyStruct)|})] + + public struct MyStruct; + """; + + await VerifyCS.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task Delegate_Warns() + { + const string source = """ + using WindowsRuntime.InteropServices; + + [assembly: WindowsRuntimeNativeExposedType({|CSWINRT2019:typeof(MyDelegate)|})] + + public delegate void MyDelegate(); + """; + + await VerifyCS.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task ArrayType_Warns() + { + const string source = """ + using WindowsRuntime.InteropServices; + + [assembly: WindowsRuntimeNativeExposedType({|CSWINRT2019:typeof(int[])|})] + """; + + await VerifyCS.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task ClosedGenericType_Warns() + { + const string source = """ + using System.Collections.Generic; + using WindowsRuntime.InteropServices; + + [assembly: WindowsRuntimeNativeExposedType({|CSWINRT2019:typeof(List)|})] + """; + + await VerifyCS.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task ProjectedStruct_Warns() + { + const string source = """ + using WindowsRuntime.InteropServices; + + [assembly: WindowsRuntimeNativeExposedType({|CSWINRT2019:typeof(Windows.Foundation.Point)|})] + """; + + await VerifyCS.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task DuplicateProjectedClass_BothWarn() + { + const string source = """ + using WindowsRuntime.InteropServices; + + [assembly: WindowsRuntimeNativeExposedType({|CSWINRT2020:typeof(Microsoft.UI.Xaml.DependencyObjectCollection)|})] + [assembly: WindowsRuntimeNativeExposedType({|CSWINRT2020:typeof(Microsoft.UI.Xaml.DependencyObjectCollection)|})] + """; + + await VerifyCS.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task DuplicateNonProjectedClass_WarnsAsNotProjectedClass() + { + const string source = """ + using WindowsRuntime.InteropServices; + + [assembly: WindowsRuntimeNativeExposedType({|CSWINRT2019:typeof(MyClass)|})] + [assembly: WindowsRuntimeNativeExposedType({|CSWINRT2019:typeof(MyClass)|})] + + public sealed class MyClass; + """; + + await VerifyCS.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task DuplicateAcrossUserAndGeneratedCode_OnlyUserWarns() + { + const string source = """ + using WindowsRuntime.InteropServices; + + [assembly: WindowsRuntimeNativeExposedType({|CSWINRT2020:typeof(Microsoft.UI.Xaml.DependencyObjectCollection)|})] + """; + + const string generatedSource = """ + using WindowsRuntime.InteropServices; + + [assembly: WindowsRuntimeNativeExposedType(typeof(Microsoft.UI.Xaml.DependencyObjectCollection))] + """; + + await VerifyCS.VerifyAnalyzerAsync(source, generatedSource: generatedSource); + } + + [TestMethod] + public async Task ApplicationInGeneratedCodeOnly_DoesNotWarn() + { + const string source = """ + using WindowsRuntime.InteropServices; + + [assembly: WindowsRuntimeNativeExposedType(typeof(Microsoft.UI.Xaml.DependencyObjectCollection))] + """; + + const string generatedSource = """ + using WindowsRuntime.InteropServices; + + [assembly: WindowsRuntimeNativeExposedType(typeof(MyClass))] + + public sealed class MyClass; + """; + + await VerifyCS.VerifyAnalyzerAsync(source, generatedSource: generatedSource); + } +} From f49fbac1a4428ce4b88d39c1a13c4e492a1f11dd Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 2 Jul 2026 17:10:14 -0700 Subject: [PATCH 5/8] Resolve the IID for 'IStringable' when generating CCW code for projected types When the interop generator generates CCW marshalling code for a projected type opted in via '[WindowsRuntimeNativeExposedType]', the type's vtable set includes the manually-projected 'IStringable' interface, which every projected Windows Runtime class implements. Resolving its interface entry routes through 'WellKnownInterfaceIIDs.get_IID', which was missing a case for 'IStringable' and so threw CSWINRTINTEROPGEN0052. This path was never reached before, because managed user-defined types use the built-in 'IStringable' native entry instead of carrying the projected interface in their vtable set. Add the missing case so that CCW code can be generated for projected types. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../References/WellKnownInterfaceIIDs.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/WinRT.Interop.Generator/References/WellKnownInterfaceIIDs.cs b/src/WinRT.Interop.Generator/References/WellKnownInterfaceIIDs.cs index fafda0e0fb..4e86a0a3d7 100644 --- a/src/WinRT.Interop.Generator/References/WellKnownInterfaceIIDs.cs +++ b/src/WinRT.Interop.Generator/References/WellKnownInterfaceIIDs.cs @@ -90,6 +90,8 @@ _ when SignatureComparer.IgnoreVersion.Equals(interfaceType, interopReferences.I => "Windows_Foundation_IAsyncAction", _ when SignatureComparer.IgnoreVersion.Equals(interfaceType, interopReferences.IVectorChangedEventArgs) => "Windows_Foundation_Collections_IVectorChangedEventArgs", + _ when SignatureComparer.IgnoreVersion.Equals(interfaceType, interopReferences.IStringable) + => "IStringable", _ when SignatureComparer.IgnoreVersion.Equals(interfaceType, interopReferences.IBuffer) => "Windows_Storage_Streams_IBuffer", _ when SignatureComparer.IgnoreVersion.Equals(interfaceType, interopReferences.IInputStream) From 7a56a711d87de1ddf70c520e882fc8ea13512f1e Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 2 Jul 2026 17:10:36 -0700 Subject: [PATCH 6/8] Add tests for native exposed type CCW generation Add a 'NativeExposedType' functional test and a 'NativeExposedTypeTests' unit test that opt a projected type into CCW marshalling code generation via '[WindowsRuntimeNativeExposedType]' and assert that the interop generator registered a proxy type map association for it, while a projected type that was not opted in has no such association. The functional test is a self-contained executable that runs in both JIT and Native AOT, and is wired into 'cswinrt.slnx' and the functional test list in 'build.cmd'. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../NativeExposedType.csproj | 17 +++++++ .../NativeExposedType/Program.cs | 49 ++++++++++++++++++ src/Tests/UnitTest/NativeExposedTypeTests.cs | 51 +++++++++++++++++++ src/build.cmd | 2 +- src/cswinrt.slnx | 8 +++ 5 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 src/Tests/FunctionalTests/NativeExposedType/NativeExposedType.csproj create mode 100644 src/Tests/FunctionalTests/NativeExposedType/Program.cs create mode 100644 src/Tests/UnitTest/NativeExposedTypeTests.cs diff --git a/src/Tests/FunctionalTests/NativeExposedType/NativeExposedType.csproj b/src/Tests/FunctionalTests/NativeExposedType/NativeExposedType.csproj new file mode 100644 index 0000000000..d588e3b38a --- /dev/null +++ b/src/Tests/FunctionalTests/NativeExposedType/NativeExposedType.csproj @@ -0,0 +1,17 @@ + + + + Exe + $(FunctionalTestsBuildTFMs) + x86;x64 + win-x86;win-x64 + $(MSBuildProjectDirectory)\..\PublishProfiles\win-$(Platform).pubxml + true + + + + + + + + diff --git a/src/Tests/FunctionalTests/NativeExposedType/Program.cs b/src/Tests/FunctionalTests/NativeExposedType/Program.cs new file mode 100644 index 0000000000..538d3f1d92 --- /dev/null +++ b/src/Tests/FunctionalTests/NativeExposedType/Program.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +using Windows.Data.Json; +using WindowsRuntime.InteropServices; + +#pragma warning disable CSWINRT3001 // Type or member is obsolete + +// The interop generator normally skips projected types when generating CCW marshalling code, as they are backed +// by native objects and never need to be exposed to native code through a CCW. Opting 'JsonArray' in via the +// '[WindowsRuntimeNativeExposedType]' attribute forces the interop generator to also emit CCW marshalling code +// for it, just like it does for any user defined type. This registers a proxy type map association for the type, +// which the checks below then verify is present (and absent for a projected type that was not opted in). +[assembly: WindowsRuntimeNativeExposedType(typeof(JsonArray))] + +namespace NativeExposedType; + +internal static class Program +{ + private static int Main() + { + IReadOnlyDictionary proxyTypeMapping = GetComWrappersProxyTypeMapping(); + + // 'JsonArray' was explicitly opted into CCW marshalling code generation, so the interop generator must + // have generated a proxy for it and registered the associated proxy type map entry. + if (!proxyTypeMapping.TryGetValue(typeof(JsonArray), out _)) + { + return 101; + } + + // 'JsonObject' is a projected type that was not opted into CCW marshalling code generation. The interop + // generator must have skipped it, as projected types are backed by native objects and never need CCW + // marshalling code generated for them by default. + if (proxyTypeMapping.TryGetValue(typeof(JsonObject), out _)) + { + return 102; + } + + return 100; + } + + // Retrieves the proxy type map used by CsWinRT to resolve CCW marshalling info for managed objects + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The proxy type map is always preserved by the interop generator.")] + private static IReadOnlyDictionary GetComWrappersProxyTypeMapping() + { + return TypeMapping.GetOrCreateProxyTypeMapping(); + } +} diff --git a/src/Tests/UnitTest/NativeExposedTypeTests.cs b/src/Tests/UnitTest/NativeExposedTypeTests.cs new file mode 100644 index 0000000000..34e3c1fac8 --- /dev/null +++ b/src/Tests/UnitTest/NativeExposedTypeTests.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +using TestComponentCSharp; +using Windows.Data.Json; +using WindowsRuntime.InteropServices; + +#pragma warning disable CSWINRT3001 // Type or member is obsolete + +// The interop generator normally skips projected types when generating CCW marshalling code, as they are backed +// by native objects and never need to be exposed to native code through a CCW. Opting 'CustomIterableTest' in via +// the '[WindowsRuntimeNativeExposedType]' attribute forces the interop generator to also emit CCW marshalling code +// for it, just like it does for any user defined type. This registers a proxy type map association for the type, +// which the tests below then verify is present (and absent for a projected type that was not opted in). +[assembly: WindowsRuntimeNativeExposedType(typeof(CustomIterableTest))] + +namespace UnitTest +{ + [TestClass] + public class NativeExposedTypeTests + { + [TestMethod] + public void NativeExposedType_IsRegisteredInComWrappersProxyTypeMapping() + { + IReadOnlyDictionary proxyTypeMapping = GetComWrappersProxyTypeMapping(); + + // 'CustomIterableTest' was explicitly opted into CCW marshalling code generation, so the interop + // generator must have generated a proxy for it and registered the associated proxy type map entry. + Assert.IsTrue(proxyTypeMapping.TryGetValue(typeof(CustomIterableTest), out _)); + } + + [TestMethod] + public void ProjectedType_WithoutOptIn_IsNotRegisteredInComWrappersProxyTypeMapping() + { + IReadOnlyDictionary proxyTypeMapping = GetComWrappersProxyTypeMapping(); + + // 'JsonObject' is a projected type that was not opted into CCW marshalling code generation. The interop + // generator must have skipped it, as projected types are backed by native objects and never need CCW + // marshalling code generated for them by default. + Assert.IsFalse(proxyTypeMapping.TryGetValue(typeof(JsonObject), out _)); + } + + // Retrieves the proxy type map used by CsWinRT to resolve CCW marshalling info for managed objects + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "The proxy type map is always preserved by the interop generator.")] + private static IReadOnlyDictionary GetComWrappersProxyTypeMapping() + { + return TypeMapping.GetOrCreateProxyTypeMapping(); + } + } +} diff --git a/src/build.cmd b/src/build.cmd index 51f471e172..e3fd0fbee0 100644 --- a/src/build.cmd +++ b/src/build.cmd @@ -68,7 +68,7 @@ if "%cswinrt_assembly_version%"=="" set cswinrt_assembly_version=0.0.0.0 if "%cswinrt_baseline_breaking_compat_errors%"=="" set cswinrt_baseline_breaking_compat_errors=false if "%cswinrt_baseline_assembly_version_compat_errors%"=="" set cswinrt_baseline_assembly_version_compat_errors=false -set cswinrt_functional_tests=JsonValueFunctionCalls, ClassActivation, Structs, Events, DynamicInterfaceCasting, Collections, Async, DerivedClassActivation, DerivedClassAsBaseClass, CCW +set cswinrt_functional_tests=JsonValueFunctionCalls, ClassActivation, Structs, Events, DynamicInterfaceCasting, Collections, Async, DerivedClassActivation, DerivedClassAsBaseClass, CCW, NativeExposedType if "%cswinrt_platform%" EQU "x86" set run_functional_tests=true if "%cswinrt_platform%" EQU "x64" set run_functional_tests=true diff --git a/src/cswinrt.slnx b/src/cswinrt.slnx index 27e04f3c4e..6bb694e6e2 100644 --- a/src/cswinrt.slnx +++ b/src/cswinrt.slnx @@ -400,6 +400,14 @@ + + + + + + + + From 3c5c000ecb06cad11fce9c1ed7eda257c3ea34c2 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 2 Jul 2026 18:43:59 -0700 Subject: [PATCH 7/8] Improve marshaller diagnostics and ComputeVtables handling Enhance debugging info and error handling for WindowsRuntimeComWrappers marshalling. - Include the runtime attribute type in NotSupportedException messages for GetOrCreateComInterfaceForObject and CreateObject to make failures easier to diagnose. - Change ComputeVtables to return null (with count=0) when not overridden; WindowsRuntimeMarshallingInfo now detects null vtable entries and throws a detailed NotSupportedException that includes the marshaller type and metadata provider type, and guidance for enabling marshalling or filing an issue. - Add pragma to suppress IDE0046 alongside IDE0008. This improves developer diagnostics when custom marshallers are incomplete. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- ...wsRuntimeComWrappersMarshallerAttribute.cs | 17 ++++++------- .../WindowsRuntimeMarshallingInfo.cs | 24 ++++++++++++++++++- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/WinRT.Runtime2/InteropServices/Attributes/WindowsRuntimeComWrappersMarshallerAttribute.cs b/src/WinRT.Runtime2/InteropServices/Attributes/WindowsRuntimeComWrappersMarshallerAttribute.cs index 77a19c1874..ec9e2ad6ea 100644 --- a/src/WinRT.Runtime2/InteropServices/Attributes/WindowsRuntimeComWrappersMarshallerAttribute.cs +++ b/src/WinRT.Runtime2/InteropServices/Attributes/WindowsRuntimeComWrappersMarshallerAttribute.cs @@ -74,8 +74,8 @@ public abstract unsafe class WindowsRuntimeComWrappersMarshallerAttribute : Attr public virtual void* GetOrCreateComInterfaceForObject(object value) { [MethodImpl(MethodImplOptions.NoInlining)] - static NotSupportedException GetNotSupportedException() - => new($"The current '{nameof(WindowsRuntimeComWrappersMarshallerAttribute)}' implementation does not support '{nameof(GetOrCreateComInterfaceForObject)}'."); + NotSupportedException GetNotSupportedException() + => new($"The current '{nameof(WindowsRuntimeComWrappersMarshallerAttribute)}' implementation ('{GetType()}') does not support '{nameof(GetOrCreateComInterfaceForObject)}'."); throw GetNotSupportedException(); } @@ -108,11 +108,12 @@ static NotSupportedException GetNotSupportedException() /// public virtual ComWrappers.ComInterfaceEntry* ComputeVtables(out int count) { - [MethodImpl(MethodImplOptions.NoInlining)] - static NotSupportedException GetNotSupportedException() - => new($"The current '{nameof(WindowsRuntimeComWrappersMarshallerAttribute)}' implementation does not support '{nameof(ComputeVtables)}'."); + // This method does not throw an exception like the others in this type. In case of failure, + // the code in 'WindowsRuntimeMarshallingInfo' will detect the returned 'null' value and + // throw an exception from there, so additional debugging info can be included as well. + count = 0; - throw GetNotSupportedException(); + return null; } /// @@ -134,8 +135,8 @@ static NotSupportedException GetNotSupportedException() public virtual object CreateObject(void* value, out CreatedWrapperFlags wrapperFlags) { [MethodImpl(MethodImplOptions.NoInlining)] - static NotSupportedException GetNotSupportedException() - => new($"The current '{nameof(WindowsRuntimeComWrappersMarshallerAttribute)}' implementation does not support '{nameof(CreateObject)}'."); + NotSupportedException GetNotSupportedException() + => new($"The current '{nameof(WindowsRuntimeComWrappersMarshallerAttribute)}' implementation ('{GetType()}') does not support '{nameof(CreateObject)}'."); throw GetNotSupportedException(); } diff --git a/src/WinRT.Runtime2/InteropServices/TypeMapInfo/WindowsRuntimeMarshallingInfo.cs b/src/WinRT.Runtime2/InteropServices/TypeMapInfo/WindowsRuntimeMarshallingInfo.cs index 06c31b568f..f9e48020bb 100644 --- a/src/WinRT.Runtime2/InteropServices/TypeMapInfo/WindowsRuntimeMarshallingInfo.cs +++ b/src/WinRT.Runtime2/InteropServices/TypeMapInfo/WindowsRuntimeMarshallingInfo.cs @@ -10,7 +10,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -#pragma warning disable IDE0008 +#pragma warning disable IDE0008, IDE0046 namespace WindowsRuntime.InteropServices; @@ -590,6 +590,28 @@ unsafe WindowsRuntimeVtableInfo InitializeVtableInfo() // Delegate to the vtable provider to produce the first vtable entries ComWrappers.ComInterfaceEntry* vtableEntries = comWrappersMarshaller.ComputeVtables(out int count); + // When the attribute instance did not override 'ComputeVtables', it will just return 'null', + // which we can detect here. The reason for this is that we want to include the type of the + // object we failed to marshal in the exception message, but that wouldn't be available from + // the 'ComputeVtables' method itself. Returning 'null' is just an efficient way to signal the + // failure, rather than having that method throw and then catch and re-throw from here. + if (vtableEntries is null) + { + // Throws an exception with a message that includes the type of the object we failed to marshal + [DoesNotReturn] + [StackTraceHidden] + void ThrowNotSupportedException(WindowsRuntimeComWrappersMarshallerAttribute comWrappersMarshaller) + { + throw new NotSupportedException( + $"The current '{nameof(WindowsRuntimeComWrappersMarshallerAttribute)}' implementation ('{comWrappersMarshaller.GetType()}') associated with the " + + $"metadata provider type '{_metadataProviderType}' does not support '{nameof(WindowsRuntimeComWrappersMarshallerAttribute.ComputeVtables)}'. " + + $"If marshalling instances of this type is required, consider using '{nameof(WindowsRuntimeNativeExposedTypeAttribute)}' to explicitly enable generating " + + $"marshalling code for this scenario. Additionally, please file an issue at https://github.com/microsoft/CsWinRT."); + } + + ThrowNotSupportedException(comWrappersMarshaller); + } + return _vtableInfo ??= new(vtableEntries, count); } From 7355477e36311588e0d89a6792de596662dcdae6 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 3 Jul 2026 16:54:56 -0700 Subject: [PATCH 8/8] Clean up analyzer logic and remove pragma Refactor WindowsRuntimeNativeExposedTypeAnalyzer: initialize validTargetTypes correctly, enumerate assembly attributes via context.Compilation, simplify Classify return logic (explicit guard + valid return), and relax CountOccurrences to accept IEnumerable. Also remove IDE0046 from WindowsRuntimeMarshallingInfo pragma disables. These edits improve clarity, correctness of attribute enumeration, and reduce unnecessary suppressed warnings. --- ...WindowsRuntimeNativeExposedTypeAnalyzer.cs | 28 +++++++++---------- .../WindowsRuntimeMarshallingInfo.cs | 2 +- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/WindowsRuntimeNativeExposedTypeAnalyzer.cs b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/WindowsRuntimeNativeExposedTypeAnalyzer.cs index b73d9db1d9..4f7ff205ee 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/WindowsRuntimeNativeExposedTypeAnalyzer.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/WindowsRuntimeNativeExposedTypeAnalyzer.cs @@ -45,14 +45,12 @@ public override void Initialize(AnalysisContext context) return; } - IAssemblySymbol assemblySymbol = context.Compilation.Assembly; + List validTargetTypes = []; // Collect the valid target types across all applications of the attribute in the assembly (including // the ones in generated code), so that a type used both in user code and in generated code is still // detected as a duplicate. This list is only used to report duplicate applications of the attribute. - List validTargetTypes = []; - - foreach (AttributeData attribute in assemblySymbol.GetAttributes(nativeExposedTypeAttributeType)) + foreach (AttributeData attribute in context.Compilation.Assembly.GetAttributes(nativeExposedTypeAttributeType)) { if (attribute is { ConstructorArguments: [{ Value: ITypeSymbol targetType }] } && Classify(targetType, windowsRuntimeMetadataAttributeType) is NativeExposedTypeKind.Valid) @@ -64,7 +62,7 @@ public override void Initialize(AnalysisContext context) // Classify and report each application of the attribute. Applications in generated code are // suppressed by the analysis framework, because this analyzer opts out of generated code analysis, // so only applications authored in user code are ever reported. - foreach (AttributeData attribute in assemblySymbol.GetAttributes(nativeExposedTypeAttributeType)) + foreach (AttributeData attribute in context.Compilation.Assembly.GetAttributes(nativeExposedTypeAttributeType)) { // Skip malformed applications, eg. where the argument does not bind to a type if (attribute is not { ConstructorArguments: [{ Value: ITypeSymbol targetType }] }) @@ -84,19 +82,17 @@ public override void Initialize(AnalysisContext context) location, targetType)); } - - // The type is not a projected class, so CCW marshalling code is already generated for it else if (kind is NativeExposedTypeKind.NotProjectedClass) { + // The type is not a projected class, so CCW marshalling code is already generated for it context.ReportDiagnostic(Diagnostic.Create( DiagnosticDescriptors.NativeExposedTypeNotProjectedClass, location, targetType)); } - - // The type is valid, but it is also targeted by another application of the attribute else if (CountOccurrences(validTargetTypes, targetType) > 1) { + // The type is valid, but it is also targeted by another application of the attribute context.ReportDiagnostic(Diagnostic.Create( DiagnosticDescriptors.NativeExposedTypeDuplicate, location, @@ -122,10 +118,14 @@ private static NativeExposedTypeKind Classify(ITypeSymbol type, INamedTypeSymbol // A valid target is a non generic projected Windows Runtime class. CCW marshalling code is generated // automatically for every other kind of type, so the attribute is only ever needed for projected classes. - return type is INamedTypeSymbol { TypeKind: TypeKind.Class, IsGenericType: false } namedType && - namedType.HasAttributeWithType(windowsRuntimeMetadataAttributeType) - ? NativeExposedTypeKind.Valid - : NativeExposedTypeKind.NotProjectedClass; + if (type is not INamedTypeSymbol { TypeKind: TypeKind.Class, IsGenericType: false } namedType || + !namedType.HasAttributeWithType(windowsRuntimeMetadataAttributeType)) + { + return NativeExposedTypeKind.NotProjectedClass; + } + + // The type is a valid, non-generic and projected runtime class + return NativeExposedTypeKind.Valid; } /// @@ -134,7 +134,7 @@ private static NativeExposedTypeKind Classify(ITypeSymbol type, INamedTypeSymbol /// The sequence of types to search. /// The type to count occurrences of. /// The number of times appears in . - private static int CountOccurrences(List types, ITypeSymbol type) + private static int CountOccurrences(IEnumerable types, ITypeSymbol type) { int count = 0; diff --git a/src/WinRT.Runtime2/InteropServices/TypeMapInfo/WindowsRuntimeMarshallingInfo.cs b/src/WinRT.Runtime2/InteropServices/TypeMapInfo/WindowsRuntimeMarshallingInfo.cs index f9e48020bb..391b17dce3 100644 --- a/src/WinRT.Runtime2/InteropServices/TypeMapInfo/WindowsRuntimeMarshallingInfo.cs +++ b/src/WinRT.Runtime2/InteropServices/TypeMapInfo/WindowsRuntimeMarshallingInfo.cs @@ -10,7 +10,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -#pragma warning disable IDE0008, IDE0046 +#pragma warning disable IDE0008 namespace WindowsRuntime.InteropServices;