From bf1b1cf748370b1f53788e4ce9ae96cd3f026417 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 18 Jun 2026 15:32:21 -0700 Subject: [PATCH 01/13] Add deprecation helpers and [Obsolete] emission to the projection writer Port [Windows.Foundation.Metadata.Deprecated] handling from PR #2376 (CsWinRT 2.x) to the CsWinRT 3.0 projection writer. windows-rs and other consumers surface deprecated/removed WinRT APIs via this attribute. - Add IsDeprecated / IsRemoved / IsDeprecatedNotRemoved / DeprecatedMessage extension members that read the DeprecatedAttribute (the second fixed argument is DeprecationType, where Deprecate = 0 and Remove = 1). - Add CustomAttributeFactory.WriteObsoleteAttribute, which emits [System.Obsolete(message)] for a deprecated-but-not-removed member, and wire it into the projected type-level attributes (classes, interfaces, enums, structs, delegates). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../IHasCustomAttributeExtensions.cs | 39 +++++++++++++++++++ .../Factories/CustomAttributeFactory.cs | 27 +++++++++++++ 2 files changed, 66 insertions(+) diff --git a/src/WinRT.Projection.Writer/Extensions/IHasCustomAttributeExtensions.cs b/src/WinRT.Projection.Writer/Extensions/IHasCustomAttributeExtensions.cs index 1e7f310f0..b16b0c738 100644 --- a/src/WinRT.Projection.Writer/Extensions/IHasCustomAttributeExtensions.cs +++ b/src/WinRT.Projection.Writer/Extensions/IHasCustomAttributeExtensions.cs @@ -94,5 +94,44 @@ public bool HasWindowsFoundationMetadataAttribute(string name) { return member.GetAttribute(WellKnownNamespaces.WindowsFoundationMetadata, name); } + + /// + /// Gets whether the member carries a [Windows.Foundation.Metadata.Deprecated] attribute. + /// + public bool IsDeprecated => member.HasWindowsFoundationMetadataAttribute("DeprecatedAttribute"); + + /// + /// Gets whether the member is marked as removed: it carries a + /// [Windows.Foundation.Metadata.Deprecated] attribute whose DeprecationType + /// is Remove. A removed member is omitted from the projection, while its ABI vtable + /// slot is preserved (stubbed to return E_NOTIMPL) for binary compatibility. + /// + /// + /// DeprecatedAttribute(string message, DeprecationType type, ...): the second fixed + /// argument is the DeprecationType enum, where Deprecate is 0 and Remove is 1. + /// + public bool IsRemoved => + member.GetWindowsFoundationMetadataAttribute("DeprecatedAttribute") is { Signature.FixedArguments: [_, { Element: int deprecationType }, ..] } + && deprecationType == 1; + + /// + /// Gets whether the member is deprecated but not removed (i.e. it is projected with an + /// [Obsolete] attribute rather than being omitted). + /// + public bool IsDeprecatedNotRemoved => member.IsDeprecated && !member.IsRemoved; + + /// + /// Gets the message from the member's [Windows.Foundation.Metadata.Deprecated] + /// attribute (the first fixed argument), or if the member is not + /// deprecated or the attribute carries no message. + /// + /// + /// DeprecatedAttribute(string message, ...): the first fixed argument is the message. + /// AsmResolver returns Utf8String for string custom-attribute args, so it is converted. + /// + public string? DeprecatedMessage => + member.GetWindowsFoundationMetadataAttribute("DeprecatedAttribute") is { Signature.FixedArguments: [{ Element: { } message }, ..] } + ? message.ToString() + : null; } } \ No newline at end of file diff --git a/src/WinRT.Projection.Writer/Factories/CustomAttributeFactory.cs b/src/WinRT.Projection.Writer/Factories/CustomAttributeFactory.cs index 22e6c1660..064264059 100644 --- a/src/WinRT.Projection.Writer/Factories/CustomAttributeFactory.cs +++ b/src/WinRT.Projection.Writer/Factories/CustomAttributeFactory.cs @@ -484,6 +484,31 @@ public static void WriteCustomAttributes(IndentedTextWriter writer, ProjectionEm } } + /// + /// Writes a [System.Obsolete] attribute when is deprecated but + /// not removed. Removed members are omitted from the projection entirely, so they get no attribute. + /// + /// The writer to emit to. + /// The member to inspect for [Windows.Foundation.Metadata.Deprecated]. + public static void WriteObsoleteAttribute(IndentedTextWriter writer, IHasCustomAttribute member) + { + if (!member.IsDeprecatedNotRemoved) + { + return; + } + + string? message = member.DeprecatedMessage; + + if (string.IsNullOrEmpty(message)) + { + writer.WriteLine("[global::System.Obsolete]"); + } + else + { + writer.WriteLine($"[global::System.Obsolete(@\"{EscapeVerbatimString(message)}\")]"); + } + } + /// /// Returns whether a Windows Runtime metadata attribute application should be carried over to the projection. /// @@ -542,6 +567,7 @@ private static bool ShouldCarryOverAttribute(ProjectionEmitContext context, stri public static void WriteTypeCustomAttributes(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type, bool enablePlatformAttrib) { WriteCustomAttributes(writer, context, type, enablePlatformAttrib); + WriteObsoleteAttribute(writer, type); } /// @@ -562,6 +588,7 @@ internal static void WriteTypeCustomAttributesBody(IndentedTextWriter writer, Pr int before = writer.Length; WriteCustomAttributes(writer, context, type, enablePlatformAttrib); + WriteObsoleteAttribute(writer, type); // If anything was written, the buffer ends with a trailing newline that came from the // last attribute's WriteLine. Trim it so the callback can be inlined into a multiline From 385a3ef7699a48c8f8a1e7bccc8c472cc92a5f83 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 18 Jun 2026 15:32:36 -0700 Subject: [PATCH 02/13] Skip removed APIs and annotate deprecated members in the projection A WinRT API marked [Deprecated(..., DeprecationType.Remove, ...)] is omitted from the generated projection; a merely deprecated API is annotated with [Obsolete]. - Skip fully removed types in the per-namespace generation loops (type-map attributes, projected types, ABI types, and generated IIDs). - Honor removal/deprecation on interface member signatures, runtime class members, static members, factory and composable constructors, enum fields, and the IDynamicInterfaceCastable forwarders. - MIDL places [Deprecated] on the property getter / event add accessor (not on the Property/Event row), so removal and deprecation are checked on the accessor. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Builders/ProjectionFileBuilder.cs | 7 +++ .../Factories/AbiInterfaceIDicFactory.cs | 36 +++++++++++++++ .../Factories/ClassFactory.cs | 46 +++++++++++++++++++ .../ClassMembersFactory.WriteClassMembers.cs | 5 ++ ...assMembersFactory.WriteInterfaceMembers.cs | 38 +++++++++++++++ .../ConstructorFactory.AttributedTypes.cs | 18 ++++++++ .../ConstructorFactory.Composable.cs | 11 +++++ .../Factories/InterfaceFactory.cs | 36 +++++++++++++++ .../ProjectionGenerator.GeneratedIids.cs | 6 +++ .../ProjectionGenerator.Namespace.cs | 24 ++++++++++ .../Models/PropertyAccessorState.cs | 7 +++ .../Models/StaticPropertyAccessorState.cs | 9 ++++ 12 files changed, 243 insertions(+) diff --git a/src/WinRT.Projection.Writer/Builders/ProjectionFileBuilder.cs b/src/WinRT.Projection.Writer/Builders/ProjectionFileBuilder.cs index 5bc218b1a..b2f3550b5 100644 --- a/src/WinRT.Projection.Writer/Builders/ProjectionFileBuilder.cs +++ b/src/WinRT.Projection.Writer/Builders/ProjectionFileBuilder.cs @@ -120,11 +120,18 @@ public enum {{typeName}} : {{enumUnderlyingType}} continue; } + // Skip enum fields removed via '[Deprecated(..., DeprecationType.Remove, ...)]' + if (field.IsRemoved) + { + continue; + } + string fieldName = field.GetRawName(); string constantValue = field.Constant.FormatLiteral(); // Emits per-enum-field '[SupportedOSPlatform]' when the field has a '[ContractVersion]' CustomAttributeFactory.WritePlatformAttribute(writer, context, field); + CustomAttributeFactory.WriteObsoleteAttribute(writer, field); writer.WriteLine($"{fieldName} = unchecked(({enumUnderlyingType}){constantValue}),"); } diff --git a/src/WinRT.Projection.Writer/Factories/AbiInterfaceIDicFactory.cs b/src/WinRT.Projection.Writer/Factories/AbiInterfaceIDicFactory.cs index a8a5ff988..7fdca41a5 100644 --- a/src/WinRT.Projection.Writer/Factories/AbiInterfaceIDicFactory.cs +++ b/src/WinRT.Projection.Writer/Factories/AbiInterfaceIDicFactory.cs @@ -246,6 +246,12 @@ internal static void WriteInterfaceIdicImplMembersForInheritedInterface(Indented foreach (MethodDefinition method in type.GetNonSpecialMethods()) { + // Removed members are omitted from the projected interface, so emit no DIM forwarder + if (method.IsRemoved) + { + continue; + } + MethodSignatureInfo sig = new(method); string mname = method.GetRawName(); @@ -259,6 +265,13 @@ internal static void WriteInterfaceIdicImplMembersForInheritedInterface(Indented foreach (PropertyDefinition prop in type.Properties) { (MethodDefinition? getter, MethodDefinition? setter) = prop.GetMethods(); + + // MIDL places '[Deprecated]' on the accessor (the getter for read/write properties) + if ((getter ?? setter) is { IsRemoved: true }) + { + continue; + } + string pname = prop.GetRawName(); string propType = InterfaceFactory.WritePropType(context, prop); @@ -290,6 +303,11 @@ internal static void WriteInterfaceIdicImplMembersForInheritedInterface(Indented foreach (EventDefinition evt in type.Events) { + if (evt.AddMethod is { IsRemoved: true }) + { + continue; + } + string evtName = evt.GetRawName(); writer.WriteLine(); IndentedTextWriterCallback eventType = TypedefNameWriter.WriteEventType(context, evt); @@ -367,6 +385,12 @@ internal static void WriteInterfaceIdicImplMembersForInterface(IndentedTextWrite foreach (MethodDefinition method in type.GetNonSpecialMethods()) { + // Removed members are omitted from the projected interface, so emit no DIM forwarder + if (method.IsRemoved) + { + continue; + } + MethodSignatureInfo sig = new(method); string mname = method.GetRawName(); IndentedTextWriterCallback ret = MethodFactory.WriteProjectionReturnType(context, sig); @@ -388,6 +412,13 @@ internal static void WriteInterfaceIdicImplMembersForInterface(IndentedTextWrite foreach (PropertyDefinition prop in type.Properties) { (MethodDefinition? getter, MethodDefinition? setter) = prop.GetMethods(); + + // MIDL places '[Deprecated]' on the accessor (the getter for read/write properties) + if ((getter ?? setter) is { IsRemoved: true }) + { + continue; + } + string pname = prop.GetRawName(); string propType = InterfaceFactory.WritePropType(context, prop); @@ -446,6 +477,11 @@ void WriteSetter(IndentedTextWriter writer) // dispatch through the static ABI Methods class's event accessor (returns an EventSource). foreach (EventDefinition evt in type.Events) { + if (evt.AddMethod is { IsRemoved: true }) + { + continue; + } + string evtName = evt.GetRawName(); writer.WriteLine(); IndentedTextWriterCallback eventType = TypedefNameWriter.WriteEventType(context, evt); diff --git a/src/WinRT.Projection.Writer/Factories/ClassFactory.cs b/src/WinRT.Projection.Writer/Factories/ClassFactory.cs index 1aff3fdc8..368a9d4fa 100644 --- a/src/WinRT.Projection.Writer/Factories/ClassFactory.cs +++ b/src/WinRT.Projection.Writer/Factories/ClassFactory.cs @@ -265,6 +265,13 @@ public static void WriteStaticClassMembers(IndentedTextWriter writer, Projection TypeDefinition staticIface = factory.Type; + // Skip static members from a removed static factory interface: the interface is omitted + // from the projection and ABI, so its IID / ABI Methods class would not exist to dispatch to. + if (staticIface.IsRemoved) + { + continue; + } + // Compute the objref name for this static factory interface. string objRef = ObjRefNameGenerator.GetObjRefName(context, staticIface); @@ -284,10 +291,18 @@ public static void WriteStaticClassMembers(IndentedTextWriter writer, Projection // Methods foreach (MethodDefinition method in staticIface.GetNonSpecialMethods()) { + // Skip removed static methods (omitted from the projection) + if (method.IsRemoved) + { + continue; + } + MethodSignatureInfo sig = new(method); string mname = method.GetRawName(); writer.WriteLine(); + CustomAttributeFactory.WriteObsoleteAttribute(writer, method); + writer.WriteIf(!string.IsNullOrEmpty(platformAttribute), platformAttribute); IndentedTextWriterCallback ret = MethodFactory.WriteProjectionReturnType(context, sig); @@ -309,9 +324,20 @@ public static void WriteStaticClassMembers(IndentedTextWriter writer, Projection // Events: dispatch via static ABI class which returns an event source. foreach (EventDefinition evt in staticIface.Events) { + // MIDL places '[Deprecated]' on the event 'add' accessor, not on the Event row. + if (evt.AddMethod is { IsRemoved: true }) + { + continue; + } + string evtName = evt.GetRawName(); writer.WriteLine(); + if (evt.AddMethod is { } addMethod) + { + CustomAttributeFactory.WriteObsoleteAttribute(writer, addMethod); + } + writer.WriteIf(!string.IsNullOrEmpty(platformAttribute), platformAttribute); IndentedTextWriterCallback eventType = TypedefNameWriter.WriteEventType(context, evt); @@ -343,6 +369,16 @@ public static event {{eventType}} {{evtName}} { string propName = prop.GetRawName(); (MethodDefinition? getter, MethodDefinition? setter) = prop.GetMethods(); + + // MIDL places '[Deprecated]' on the accessor (the getter for read/write properties), + // not on the Property row, so removal/deprecation is checked on the accessor. + MethodDefinition? accessor = getter ?? setter; + + if (accessor is { IsRemoved: true }) + { + continue; + } + string propType = InterfaceFactory.WritePropType(context, prop); if (!properties.TryGetValue(propName, out StaticPropertyAccessorState? state)) @@ -350,9 +386,14 @@ public static event {{eventType}} {{evtName}} state = new StaticPropertyAccessorState { PropTypeText = propType, + DeprecationAccessor = accessor, }; properties[propName] = state; } + else + { + state.DeprecationAccessor ??= accessor; + } if (getter is not null && !state.HasGetter) { @@ -378,6 +419,11 @@ public static event {{eventType}} {{evtName}} StaticPropertyAccessorState s = kv.Value; writer.WriteLine(); + if (s.DeprecationAccessor is { } deprecationAccessor) + { + CustomAttributeFactory.WriteObsoleteAttribute(writer, deprecationAccessor); + } + // when getter and setter platforms match; otherwise emit per-accessor. string getterPlat = s.GetterPlatformAttribute; string setterPlat = s.SetterPlatformAttribute; diff --git a/src/WinRT.Projection.Writer/Factories/ClassMembersFactory.WriteClassMembers.cs b/src/WinRT.Projection.Writer/Factories/ClassMembersFactory.WriteClassMembers.cs index 6442a5120..0b382b4a2 100644 --- a/src/WinRT.Projection.Writer/Factories/ClassMembersFactory.WriteClassMembers.cs +++ b/src/WinRT.Projection.Writer/Factories/ClassMembersFactory.WriteClassMembers.cs @@ -86,6 +86,11 @@ public static void WriteClassMembers(IndentedTextWriter writer, ProjectionEmitCo setterPlat = string.Empty; } + if (s.DeprecationAccessor is { } deprecationAccessor) + { + CustomAttributeFactory.WriteObsoleteAttribute(writer, deprecationAccessor); + } + writer.WriteIf(!string.IsNullOrEmpty(propertyPlat), propertyPlat); writer.Write($"{s.Access}{s.MethodSpec}{s.PropTypeText} {kvp.Key}"); diff --git a/src/WinRT.Projection.Writer/Factories/ClassMembersFactory.WriteInterfaceMembers.cs b/src/WinRT.Projection.Writer/Factories/ClassMembersFactory.WriteInterfaceMembers.cs index b4c7a0f84..51c4fdfab 100644 --- a/src/WinRT.Projection.Writer/Factories/ClassMembersFactory.WriteInterfaceMembers.cs +++ b/src/WinRT.Projection.Writer/Factories/ClassMembersFactory.WriteInterfaceMembers.cs @@ -267,6 +267,13 @@ private static void WriteInterfaceMembers(IndentedTextWriter writer, ProjectionE continue; } + // Skip members removed via '[Deprecated(..., DeprecationType.Remove, ...)]': the ABI + // vtable slot is preserved separately, but they are omitted from the projected class. + if (method.IsRemoved) + { + continue; + } + // Detect a 'string ToString()' that overrides Object.ToString() and force the // 'override' modifier on the emitted member. string methodSpecForThis = methodSpec; @@ -331,6 +338,8 @@ private static void WriteInterfaceMembers(IndentedTextWriter writer, ProjectionE parameterList: $"WindowsRuntimeObjectReference thisReference{accessorParams}"); } + CustomAttributeFactory.WriteObsoleteAttribute(writer, method); + writer.WriteLine(isMultiline: true, $$""" {{platformTrimmed}} {{access}}{{methodSpecForThis}}{{ret}} {{name}}({{parms}}) => {{body}} @@ -340,6 +349,8 @@ private static void WriteInterfaceMembers(IndentedTextWriter writer, ProjectionE { writer.WriteLine(); + CustomAttributeFactory.WriteObsoleteAttribute(writer, method); + writer.WriteIf(!string.IsNullOrEmpty(platformAttribute), platformAttribute); IndentedTextWriterCallback ret = MethodFactory.WriteProjectionReturnType(context, sig); @@ -381,6 +392,15 @@ private static void WriteInterfaceMembers(IndentedTextWriter writer, ProjectionE string name = prop.GetRawName(); (MethodDefinition? getter, MethodDefinition? setter) = prop.GetMethods(); + // MIDL places '[Deprecated]' on the property accessor (the getter for read/write + // properties), not on the Property row, so removal/deprecation is checked on the accessor. + MethodDefinition? accessor = getter ?? setter; + + if (accessor is { IsRemoved: true }) + { + continue; + } + if (!propertyState.TryGetValue(name, out PropertyAccessorState? state)) { state = new PropertyAccessorState @@ -390,9 +410,14 @@ private static void WriteInterfaceMembers(IndentedTextWriter writer, ProjectionE MethodSpec = methodSpec, IsOverridable = isOverridable, OverridableInterface = isOverridable ? originalInterface : null, + DeprecationAccessor = accessor, }; propertyState[name] = state; } + else + { + state.DeprecationAccessor ??= accessor; + } if (getter is not null && !state.HasGetter) { @@ -431,6 +456,13 @@ private static void WriteInterfaceMembers(IndentedTextWriter writer, ProjectionE continue; } + // MIDL places '[Deprecated]' on the event 'add' accessor, not on the Event row. + // Skip events removed via 'DeprecationType.Remove' (the ABI slot is preserved separately). + if (evt.AddMethod is { IsRemoved: true }) + { + continue; + } + // Compute event handler type and event source type strings. TypeSignature evtSig = evt.EventType!.ToTypeSignature(false); @@ -543,6 +575,12 @@ private static void WriteInterfaceMembers(IndentedTextWriter writer, ProjectionE // Emit the public/protected event with Subscribe/Unsubscribe. writer.WriteLine(); + // MIDL places '[Deprecated]' on the event 'add' accessor, not on the Event row. + if (evt.AddMethod is { } addMethod) + { + CustomAttributeFactory.WriteObsoleteAttribute(writer, addMethod); + } + // string to each event emission. In ref mode this produces e.g. // [global::System.Runtime.Versioning.SupportedOSPlatform("Windows10.0.16299.0")]. writer.WriteIf(!string.IsNullOrEmpty(platformAttribute), platformAttribute); diff --git a/src/WinRT.Projection.Writer/Factories/ConstructorFactory.AttributedTypes.cs b/src/WinRT.Projection.Writer/Factories/ConstructorFactory.AttributedTypes.cs index 72d2093c8..bbd7b4642 100644 --- a/src/WinRT.Projection.Writer/Factories/ConstructorFactory.AttributedTypes.cs +++ b/src/WinRT.Projection.Writer/Factories/ConstructorFactory.AttributedTypes.cs @@ -64,6 +64,13 @@ public static void WriteAttributedTypes(IndentedTextWriter writer, ProjectionEmi { AttributedType factory = kv.Value; + // Skip constructors generated from a removed factory interface: the interface is omitted + // from the projection and ABI, so its IID / ABI Methods class would not exist to dispatch to. + if (factory.Type is { IsRemoved: true }) + { + continue; + } + if (factory.Activatable) { WriteFactoryConstructors(writer, context, factory.Type, classType); @@ -106,6 +113,15 @@ public static void WriteFactoryConstructors(IndentedTextWriter writer, Projectio continue; } + // Skip removed constructor overloads; the factory vtable slot is preserved (methodIndex + // still advances) so the remaining constructors dispatch through the correct slot. + if (method.IsRemoved) + { + methodIndex++; + + continue; + } + MethodSignatureInfo sig = new(method); string callbackName = (method.Name?.Value ?? "Create") + "_" + sig.Parameters.Count.ToString(CultureInfo.InvariantCulture); string argsName = callbackName + "Args"; @@ -113,6 +129,8 @@ public static void WriteFactoryConstructors(IndentedTextWriter writer, Projectio // Emit the public constructor. writer.WriteLine(); + CustomAttributeFactory.WriteObsoleteAttribute(writer, method); + writer.WriteIf(!string.IsNullOrEmpty(platformAttribute), platformAttribute); writer.Write($"public unsafe {typeName}("); diff --git a/src/WinRT.Projection.Writer/Factories/ConstructorFactory.Composable.cs b/src/WinRT.Projection.Writer/Factories/ConstructorFactory.Composable.cs index 20eb77b55..df186b163 100644 --- a/src/WinRT.Projection.Writer/Factories/ConstructorFactory.Composable.cs +++ b/src/WinRT.Projection.Writer/Factories/ConstructorFactory.Composable.cs @@ -56,6 +56,15 @@ public static void WriteComposableConstructors(IndentedTextWriter writer, Projec continue; } + // Skip removed composable constructor overloads; the factory vtable slot is preserved + // (methodIndex still advances) so the remaining constructors dispatch through the correct slot. + if (method.IsRemoved) + { + methodIndex++; + + continue; + } + // Composable factory methods have signature like: // T CreateInstance(args, object baseInterface, out object innerInterface) // For the constructor on the projected class, we exclude the trailing two params. @@ -72,6 +81,8 @@ public static void WriteComposableConstructors(IndentedTextWriter writer, Projec writer.WriteLine(); + CustomAttributeFactory.WriteObsoleteAttribute(writer, method); + writer.WriteIf(!string.IsNullOrEmpty(platformAttribute), platformAttribute); writer.Write(visibility); diff --git a/src/WinRT.Projection.Writer/Factories/InterfaceFactory.cs b/src/WinRT.Projection.Writer/Factories/InterfaceFactory.cs index f75c59aaa..9bd623821 100644 --- a/src/WinRT.Projection.Writer/Factories/InterfaceFactory.cs +++ b/src/WinRT.Projection.Writer/Factories/InterfaceFactory.cs @@ -218,11 +218,19 @@ public static void WriteInterfaceMemberSignatures(IndentedTextWriter writer, Pro { foreach (MethodDefinition method in type.GetNonSpecialMethods()) { + // Skip members removed via '[Deprecated(..., DeprecationType.Remove, ...)]': their ABI + // vtable slot is preserved separately, but they are omitted from the projected interface. + if (method.IsRemoved) + { + continue; + } + MethodSignatureInfo sig = new(method); // Carried-over metadata attributes ([Overload], [DefaultOverload], [Experimental]) are // reference-projection-only. WriteMethodCustomAttributes(writer, context, method); + CustomAttributeFactory.WriteObsoleteAttribute(writer, method); IndentedTextWriterCallback ret = MethodFactory.WriteProjectionReturnType(context, sig); IndentedTextWriterCallback parms = MethodFactory.WriteParameterList(context, sig); writer.WriteLine($"{ret} {method.GetRawName()}({parms});"); @@ -232,6 +240,15 @@ public static void WriteInterfaceMemberSignatures(IndentedTextWriter writer, Pro { (MethodDefinition? getter, MethodDefinition? setter) = prop.GetMethods(); + // MIDL places '[Deprecated]' on the property accessor (the getter for read/write + // properties), not on the Property row, so deprecation is checked on the accessor. + MethodDefinition? accessor = getter ?? setter; + + if (accessor is { IsRemoved: true }) + { + continue; + } + // Add 'new' when this interface has a setter-only property AND a property of the same // name exists on a base interface (typically the getter-only counterpart). This hides // the inherited member. @@ -239,6 +256,12 @@ public static void WriteInterfaceMemberSignatures(IndentedTextWriter writer, Pro && TryFindPropertyInBaseInterfaces(context.Cache, type, prop.GetRawName(), out _)) ? "new " : string.Empty; string propType = WritePropType(context, prop); + + if (accessor is not null) + { + CustomAttributeFactory.WriteObsoleteAttribute(writer, accessor); + } + writer.Write($"{newKeyword}{propType} {prop.GetRawName()} {{"); writer.WriteIf(getter is not null || setter is not null, " get;"); @@ -250,6 +273,19 @@ public static void WriteInterfaceMemberSignatures(IndentedTextWriter writer, Pro foreach (EventDefinition evt in type.Events) { + // MIDL places '[Deprecated]' on the event 'add' accessor, not on the Event row. + MethodDefinition? addMethod = evt.AddMethod; + + if (addMethod is { IsRemoved: true }) + { + continue; + } + + if (addMethod is not null) + { + CustomAttributeFactory.WriteObsoleteAttribute(writer, addMethod); + } + IndentedTextWriterCallback eventType = TypedefNameWriter.WriteEventType(context, evt); writer.WriteLine($"event {eventType} {evt.Name?.Value};"); } diff --git a/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.GeneratedIids.cs b/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.GeneratedIids.cs index 1af69abb2..f51691c67 100644 --- a/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.GeneratedIids.cs +++ b/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.GeneratedIids.cs @@ -84,6 +84,12 @@ internal void WriteGeneratedInterfaceIidsFile() continue; } + // Skip fully removed types (omitted from both the projection and the ABI) + if (type.IsRemoved) + { + continue; + } + if (type.IsGeneric) { continue; diff --git a/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Namespace.cs b/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Namespace.cs index 53b963907..d70a06b6a 100644 --- a/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Namespace.cs +++ b/src/WinRT.Projection.Writer/Generation/ProjectionGenerator.Namespace.cs @@ -48,6 +48,12 @@ internal bool ProcessNamespace(string ns, NamespaceMembers members, ProjectionGe continue; } + // Skip fully removed types (omitted from both the projection and the ABI) + if (type.IsRemoved) + { + continue; + } + if (type.IsGeneric) { continue; @@ -116,6 +122,12 @@ internal bool ProcessNamespace(string ns, NamespaceMembers members, ProjectionGe continue; } + // Skip fully removed types (omitted from both the projection and the ABI) + if (type.IsRemoved) + { + continue; + } + (string ns2, string nm2) = type.Names(); // Skip generic types and mapped types @@ -176,6 +188,12 @@ internal bool ProcessNamespace(string ns, NamespaceMembers members, ProjectionGe continue; } + // Skip fully removed types (omitted from both the projection and the ABI) + if (type.IsRemoved) + { + continue; + } + if (TypeKindResolver.Resolve(type) != TypeKind.Class) { continue; @@ -204,6 +222,12 @@ internal bool ProcessNamespace(string ns, NamespaceMembers members, ProjectionGe continue; } + // Skip fully removed types (omitted from both the projection and the ABI) + if (type.IsRemoved) + { + continue; + } + if (type.IsGeneric) { continue; diff --git a/src/WinRT.Projection.Writer/Models/PropertyAccessorState.cs b/src/WinRT.Projection.Writer/Models/PropertyAccessorState.cs index 999013a47..9d699f5ed 100644 --- a/src/WinRT.Projection.Writer/Models/PropertyAccessorState.cs +++ b/src/WinRT.Projection.Writer/Models/PropertyAccessorState.cs @@ -28,6 +28,13 @@ internal sealed class PropertyAccessorState /// public bool HasSetter { get; set; } + /// + /// Gets or sets the accessor method used to determine whether the property is deprecated (the + /// getter when present, otherwise the setter). MIDL places [Deprecated] on the accessor, + /// not on the Property row, so the projected property's [Obsolete] is derived from it. + /// + public MethodDefinition? DeprecationAccessor { get; set; } + /// /// Gets or sets the projected C# type text of the property (for the unified getter+setter declaration). /// diff --git a/src/WinRT.Projection.Writer/Models/StaticPropertyAccessorState.cs b/src/WinRT.Projection.Writer/Models/StaticPropertyAccessorState.cs index 160337221..8302b8df9 100644 --- a/src/WinRT.Projection.Writer/Models/StaticPropertyAccessorState.cs +++ b/src/WinRT.Projection.Writer/Models/StaticPropertyAccessorState.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using AsmResolver.DotNet; + namespace WindowsRuntime.ProjectionWriter.Models; /// @@ -19,6 +21,13 @@ internal sealed class StaticPropertyAccessorState /// public bool HasSetter { get; set; } + /// + /// Gets or sets the accessor method used to determine whether the static property is deprecated + /// (the getter when present, otherwise the setter). MIDL places [Deprecated] on the + /// accessor, not on the Property row. + /// + public MethodDefinition? DeprecationAccessor { get; set; } + /// /// Gets or sets the projected C# type text of the property (for the unified getter+setter declaration). /// From 1928a509c67f8c08c83115604d8f491b6321db35 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 18 Jun 2026 15:32:47 -0700 Subject: [PATCH 03/13] Stub removed interface members with E_NOTIMPL in the CCW vtable A removed member is omitted from the projected interface, but its native vtable slot must be preserved for binary compatibility. The CCW (Do_Abi) entry now returns E_NOTIMPL (0x80004001) for removed methods, property accessors, and event accessors, while the vtable layout (one function pointer per metadata method) is unchanged. The removal marker lives on the property getter / event add accessor (MIDL convention), so both accessors of a removed property/event are stubbed. Co-Authored-By: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Factories/AbiInterfaceFactory.cs | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/src/WinRT.Projection.Writer/Factories/AbiInterfaceFactory.cs b/src/WinRT.Projection.Writer/Factories/AbiInterfaceFactory.cs index 034cb5571..b5046f5d5 100644 --- a/src/WinRT.Projection.Writer/Factories/AbiInterfaceFactory.cs +++ b/src/WinRT.Projection.Writer/Factories/AbiInterfaceFactory.cs @@ -371,6 +371,23 @@ public static nint Vtable // this order: methods first, then properties (setter before getter), then events. HashSet propertyAccessors = [.. type.GetPropertyAccessors()]; + // Map each property accessor (getter/setter) to its property, so a removed property (whose + // removal marker is on the getter per the MIDL convention) can stub both accessor bodies. + Dictionary propertyMap = []; + + foreach (PropertyDefinition prop in type.Properties) + { + if (prop.GetMethod is { } propertyGetter) + { + propertyMap[propertyGetter] = prop; + } + + if (prop.SetMethod is { } propertySetter) + { + propertyMap[propertySetter] = prop; + } + } + // Local helper to emit a single Do_Abi method body for a given MethodDefinition. void EmitOneDoAbi(MethodDefinition method) { @@ -391,6 +408,20 @@ void EmitOneDoAbi(MethodDefinition method) private static unsafe int Do_Abi_{{vm}}({{doAbiParams}}) """); + // A removed member (DeprecationType.Remove) keeps its vtable slot for ABI compatibility, + // but is no longer on the projected interface, so its CCW entry returns E_NOTIMPL. + if (IsAbiMemberRemoved(method, eventMap, propertyMap)) + { + writer.WriteLine(); + writer.WriteLine(""" + { + return unchecked((int)0x80004001); + } + """); + + return; + } + if (eventMap is not null && eventMap.TryGetValue(method, out EventDefinition? evt2)) { if (evt2.AddMethod == method) @@ -453,6 +484,27 @@ void EmitOneDoAbi(MethodDefinition method) } } + /// + /// Determines whether the CCW entry for must be stubbed because the + /// member is removed (DeprecationType.Remove). The removal marker lives on the property + /// getter / event add accessor (MIDL convention), so both accessors of a removed property or + /// event are reported as removed. + /// + private static bool IsAbiMemberRemoved(MethodDefinition method, Dictionary? eventMap, Dictionary propertyMap) + { + if (eventMap is not null && eventMap.TryGetValue(method, out EventDefinition? evt)) + { + return evt.AddMethod is { IsRemoved: true }; + } + + if (propertyMap.TryGetValue(method, out PropertyDefinition? prop)) + { + return (prop.GetMethod ?? prop.SetMethod) is { IsRemoved: true }; + } + + return method.IsRemoved; + } + /// /// Emits the per-interface marshaller class ({Name}Marshaller) with the boxing/unboxing helpers used by user code to marshal references across the ABI. /// From ada2c73e3264c6835fc606bee3911d428ce4d2a4 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 18 Jun 2026 16:20:33 -0700 Subject: [PATCH 04/13] Support [Deprecated] for members of authored components Authored Windows Runtime components can mark their own APIs with [Windows.Foundation.Metadata.Deprecated], including DeprecationType.Remove, just like the Windows SDK does. Two changes make this work end to end: - WinMD generator: emit the [Deprecated] attribute for properties and events on the accessor method (the getter for properties, the 'add' accessor for events) rather than the property/event row. This matches the placement MIDL produces for the Windows SDK, so the projection writer (and other consumers such as windows-rs) resolve member deprecation the same way for authored components and the Windows SDK. Methods and types continue to carry the attribute directly. - Projection writer: a removed member's CCW entry only returns E_NOTIMPL when consuming a Windows Runtime type, where a managed object implementing the interface cannot supply the omitted member. In component (authoring) mode the dispatch target is the authored class itself, which still defines the member, so the entry keeps dispatching to that implementation. This preserves the vtable slot and binary compatibility for existing native callers. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Factories/AbiInterfaceFactory.cs | 10 ++- .../Writers/WinMDWriter.Attributes.cs | 72 ++++++++++++++++--- .../Writers/WinMDWriter.Members.cs | 29 +++++--- 3 files changed, 93 insertions(+), 18 deletions(-) diff --git a/src/WinRT.Projection.Writer/Factories/AbiInterfaceFactory.cs b/src/WinRT.Projection.Writer/Factories/AbiInterfaceFactory.cs index b5046f5d5..50a2f40e6 100644 --- a/src/WinRT.Projection.Writer/Factories/AbiInterfaceFactory.cs +++ b/src/WinRT.Projection.Writer/Factories/AbiInterfaceFactory.cs @@ -409,8 +409,14 @@ void EmitOneDoAbi(MethodDefinition method) """); // A removed member (DeprecationType.Remove) keeps its vtable slot for ABI compatibility, - // but is no longer on the projected interface, so its CCW entry returns E_NOTIMPL. - if (IsAbiMemberRemoved(method, eventMap, propertyMap)) + // but is no longer on the projected interface. When consuming a Windows Runtime type, a + // managed object implementing the interface cannot supply the removed member (it is not on + // the projected interface), so its CCW entry returns E_NOTIMPL. In component (authoring) mode + // the dispatch target is the authored implementation (the authored class for instance + // members, or the generated activation/static factory that forwards to it), which still + // defines the member, so the entry keeps dispatching to that implementation to preserve + // binary compatibility for existing native callers. + if (!context.Settings.Component && IsAbiMemberRemoved(method, eventMap, propertyMap)) { writer.WriteLine(); writer.WriteLine(""" diff --git a/src/WinRT.WinMD.Generator/Writers/WinMDWriter.Attributes.cs b/src/WinRT.WinMD.Generator/Writers/WinMDWriter.Attributes.cs index 25147f287..29dfa5a79 100644 --- a/src/WinRT.WinMD.Generator/Writers/WinMDWriter.Attributes.cs +++ b/src/WinRT.WinMD.Generator/Writers/WinMDWriter.Attributes.cs @@ -331,24 +331,80 @@ private int GetVersion(TypeDefinition type) /// /// The source element to copy attributes from. /// The target element to copy attributes to. - private void CopyCustomAttributes(IHasCustomAttribute source, IHasCustomAttribute target) + /// + /// Whether to skip the [Windows.Foundation.Metadata.Deprecated] attribute. This is set when + /// copying to a property or event row, because the deprecation attribute is emitted on the accessor + /// method instead (see ). + /// + private void CopyCustomAttributes(IHasCustomAttribute source, IHasCustomAttribute target, bool skipDeprecated = false) { foreach (CustomAttribute attribute in source.CustomAttributes) { - if (!ShouldCopyAttribute(attribute, _runtimeContext)) + // The '[Deprecated]' attribute on properties and events is emitted on the accessor method + // (matching MIDL), so it is skipped here when copying attributes to the property or event row + if (skipDeprecated && IsDeprecatedAttribute(attribute)) { continue; } - if (ImportAttributeConstructor(attribute.Constructor) is not MemberReference importedCtor) - { - continue; - } + CopyCustomAttribute(attribute, target); + } + } - CustomAttributeSignature clonedSignature = CloneAttributeSignature(attribute.Signature); + /// + /// Copies a single custom attribute from a source element to a target element, applying the same + /// filtering and import logic as . + /// + /// The custom attribute to copy. + /// The target element to copy the attribute to. + private void CopyCustomAttribute(CustomAttribute attribute, IHasCustomAttribute target) + { + if (!ShouldCopyAttribute(attribute, _runtimeContext)) + { + return; + } - target.CustomAttributes.Add(new CustomAttribute(importedCtor, clonedSignature)); + if (ImportAttributeConstructor(attribute.Constructor) is not MemberReference importedCtor) + { + return; } + + CustomAttributeSignature clonedSignature = CloneAttributeSignature(attribute.Signature); + + target.CustomAttributes.Add(new CustomAttribute(importedCtor, clonedSignature)); + } + + /// + /// Copies the [Windows.Foundation.Metadata.Deprecated] attribute (if any) from a property or + /// event onto its accessor method (the getter for properties, the add accessor for events). + /// + /// + /// Windows Runtime metadata places the deprecation attribute on the accessor method rather than the + /// property or event row (this is the placement MIDL produces). Emitting it on the accessor keeps + /// authored components consistent with the Windows SDK, so that both CsWinRT and other consumers + /// (e.g. windows-rs) resolve member deprecation the same way. + /// + /// The source property or event to read the attribute from. + /// The accessor method (or fallback element) to copy the attribute to. + private void CopyDeprecatedAttributeToAccessor(IHasCustomAttribute source, IHasCustomAttribute accessor) + { + foreach (CustomAttribute attribute in source.CustomAttributes) + { + if (IsDeprecatedAttribute(attribute)) + { + CopyCustomAttribute(attribute, accessor); + } + } + } + + /// + /// Returns whether the given custom attribute is a [Windows.Foundation.Metadata.Deprecated] attribute. + /// + /// The custom attribute to evaluate. + /// if the attribute is the deprecation attribute; otherwise, . + private static bool IsDeprecatedAttribute(CustomAttribute attribute) + { + return attribute.Constructor?.DeclaringType?.FullName == "Windows.Foundation.Metadata.DeprecatedAttribute"; } /// diff --git a/src/WinRT.WinMD.Generator/Writers/WinMDWriter.Members.cs b/src/WinRT.WinMD.Generator/Writers/WinMDWriter.Members.cs index dd89151f1..8cfaeb9bf 100644 --- a/src/WinRT.WinMD.Generator/Writers/WinMDWriter.Members.cs +++ b/src/WinRT.WinMD.Generator/Writers/WinMDWriter.Members.cs @@ -274,6 +274,9 @@ private void AddPropertyToType(TypeDefinition outputType, PropertyDefinition inp attributes: PropertyAttributes.None, signature: isStatic ? PropertySignature.CreateStatic(propertyType) : PropertySignature.CreateInstance(propertyType)); + MethodDefinition? getter = null; + MethodDefinition? setter = null; + // Add getter if (inputProperty.GetMethod is not null) { @@ -295,7 +298,7 @@ private void AddPropertyToType(TypeDefinition outputType, PropertyDefinition inp ? MethodSignature.CreateStatic(propertyType) : MethodSignature.CreateInstance(propertyType); - MethodDefinition getter = new("get_" + inputProperty.Name.Value, attributes, getSignature); + getter = new("get_" + inputProperty.Name.Value, attributes, getSignature); if (!isInterfaceParent) { getter.ImplAttributes = MethodImplAttributes.Runtime | MethodImplAttributes.Managed; @@ -325,7 +328,7 @@ private void AddPropertyToType(TypeDefinition outputType, PropertyDefinition inp ? MethodSignature.CreateStatic(_outputModule.CorLibTypeFactory.Void, [propertyType]) : MethodSignature.CreateInstance(_outputModule.CorLibTypeFactory.Void, [propertyType]); - MethodDefinition setter = new("put_" + inputProperty.Name.Value, attributes, setSignature); + setter = new("put_" + inputProperty.Name.Value, attributes, setSignature); if (!isInterfaceParent) { setter.ImplAttributes = MethodImplAttributes.Runtime | MethodImplAttributes.Managed; @@ -340,8 +343,11 @@ private void AddPropertyToType(TypeDefinition outputType, PropertyDefinition inp outputType.Properties.Add(outputProperty); - // Copy custom attributes from the input property - CopyCustomAttributes(inputProperty, outputProperty); + // Copy custom attributes from the input property. The '[Deprecated]' attribute is emitted on the + // accessor (the getter, or the setter for write-only properties) rather than the property row, + // matching the placement used by MIDL so that property deprecation resolves consistently + CopyCustomAttributes(inputProperty, outputProperty, skipDeprecated: true); + CopyDeprecatedAttributeToAccessor(inputProperty, getter ?? setter ?? (IHasCustomAttribute)outputProperty); } /// @@ -369,7 +375,10 @@ private void AddSetterOnlyPropertyToType(TypeDefinition outputType, PropertyDefi outputType.Properties.Add(outputProperty); - CopyCustomAttributes(inputProperty, outputProperty); + // Copy custom attributes from the input property. The '[Deprecated]' attribute is emitted on the + // setter accessor rather than the property row, matching the placement used by MIDL + CopyCustomAttributes(inputProperty, outputProperty, skipDeprecated: true); + CopyDeprecatedAttributeToAccessor(inputProperty, setter); } /// @@ -403,6 +412,8 @@ private void AddEventToType(TypeDefinition outputType, EventDefinition inputEven // For interface parents (synthesized interfaces), always use instance signatures bool isStatic = !isInterfaceParent && inputEvent.AddMethod?.IsStatic == true; + MethodDefinition adder; + // Add method { MethodAttributes attributes = MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName; @@ -426,7 +437,7 @@ private void AddEventToType(TypeDefinition outputType, EventDefinition inputEven ? MethodSignature.CreateStatic(tokenSignature, [handlerSignature]) : MethodSignature.CreateInstance(tokenSignature, [handlerSignature]); - MethodDefinition adder = new("add_" + inputEvent.Name.Value, attributes, addSignature); + adder = new("add_" + inputEvent.Name.Value, attributes, addSignature); if (!isInterfaceParent) { adder.ImplAttributes = MethodImplAttributes.Runtime | MethodImplAttributes.Managed; @@ -472,7 +483,9 @@ private void AddEventToType(TypeDefinition outputType, EventDefinition inputEven outputType.Events.Add(outputEvent); - // Copy custom attributes from the input event - CopyCustomAttributes(inputEvent, outputEvent); + // Copy custom attributes from the input event. The '[Deprecated]' attribute is emitted on the + // 'add' accessor rather than the event row, matching the placement used by MIDL + CopyCustomAttributes(inputEvent, outputEvent, skipDeprecated: true); + CopyDeprecatedAttributeToAccessor(inputEvent, adder); } } \ No newline at end of file From 740cdb485f68c5282af1e871245a48ce85cb931e Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 18 Jun 2026 16:32:17 -0700 Subject: [PATCH 05/13] Add authoring test coverage for [Deprecated] members Extend the DeprecatedMembersClass authoring component and its consumption test to cover [Windows.Foundation.Metadata.Deprecated] across every member kind (method, property, event) and both deprecation types (Deprecate and Remove): - AuthoringTest: add removed (DeprecationType.Remove) method and property, and events of each deprecation kind. Building the component exercises the WinMD generator (which now emits the attribute on the accessor) and the component projection (where removed members keep dispatching to the authored class). - AuthoringConsumptionTest: call the removed members to confirm their vtable slot still dispatches to the implementation (a removed member that incorrectly returned E_NOTIMPL would fail here), and subscribe to events of each kind. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Tests/AuthoringConsumptionTest/test.cpp | 21 +++++++++++++++-- src/Tests/AuthoringTest/Program.cs | 26 +++++++++++++++++++-- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/src/Tests/AuthoringConsumptionTest/test.cpp b/src/Tests/AuthoringConsumptionTest/test.cpp index b516e7111..2c04327fe 100644 --- a/src/Tests/AuthoringConsumptionTest/test.cpp +++ b/src/Tests/AuthoringConsumptionTest/test.cpp @@ -977,10 +977,27 @@ TEST(AuthoringTest, AsyncMethodClass) TEST(AuthoringTest, DeprecatedMembersClass) { DeprecatedMembersClass obj; + + // Members marked [Deprecated(DeprecationType.Deprecate)] and the new members remain fully usable. obj.OldMethod(); obj.NewMethod(); - EXPECT_EQ(obj.OldProp(), L""); - EXPECT_EQ(obj.NewProp(), L""); + EXPECT_EQ(obj.OldProp(), L"OldProp"); + EXPECT_EQ(obj.NewProp(), L"NewProp"); + + // Members marked [Deprecated(DeprecationType.Remove)] keep their vtable slot and dispatch to the + // authored implementation, so existing callers compiled against them keep working. + obj.RemovedMethod(); + EXPECT_EQ(obj.RemovedProp(), L"RemovedProp"); + + // The same applies to static members, which dispatch through the generated static factory. + DeprecatedMembersClass::OldStatic(); + DeprecatedMembersClass::RemovedStatic(); + DeprecatedMembersClass::NewStatic(); + + // Events of every deprecation kind can be subscribed to and revoked. + auto oldToken = obj.OldEvent(auto_revoke, [](IInspectable const&, int32_t const&) {}); + auto removedToken = obj.RemovedEvent(auto_revoke, [](IInspectable const&, int32_t const&) {}); + auto newToken = obj.NewEvent(auto_revoke, [](IInspectable const&, int32_t const&) {}); } TEST(AuthoringTest, FullFeaturedClass) diff --git a/src/Tests/AuthoringTest/Program.cs b/src/Tests/AuthoringTest/Program.cs index c7adaa37f..0cf3ab67d 100644 --- a/src/Tests/AuthoringTest/Program.cs +++ b/src/Tests/AuthoringTest/Program.cs @@ -2271,12 +2271,34 @@ public sealed class DeprecatedMembersClass [Windows.Foundation.Metadata.Deprecated("Use NewMethod instead", Windows.Foundation.Metadata.DeprecationType.Deprecate, 1u)] public void OldMethod() { } + [Windows.Foundation.Metadata.Deprecated("RemovedMethod is gone", Windows.Foundation.Metadata.DeprecationType.Remove, 2u)] + public void RemovedMethod() { } + public void NewMethod() { } + [Windows.Foundation.Metadata.Deprecated("Use NewStatic instead", Windows.Foundation.Metadata.DeprecationType.Deprecate, 1u)] + public static void OldStatic() { } + + [Windows.Foundation.Metadata.Deprecated("RemovedStatic is gone", Windows.Foundation.Metadata.DeprecationType.Remove, 2u)] + public static void RemovedStatic() { } + + public static void NewStatic() { } + [Windows.Foundation.Metadata.Deprecated("Use NewProp instead", Windows.Foundation.Metadata.DeprecationType.Deprecate, 1u)] - public string OldProp => ""; + public string OldProp => "OldProp"; + + [Windows.Foundation.Metadata.Deprecated("RemovedProp is gone", Windows.Foundation.Metadata.DeprecationType.Remove, 2u)] + public string RemovedProp => "RemovedProp"; + + public string NewProp => "NewProp"; + + [Windows.Foundation.Metadata.Deprecated("Use NewEvent instead", Windows.Foundation.Metadata.DeprecationType.Deprecate, 1u)] + public event System.EventHandler OldEvent; + + [Windows.Foundation.Metadata.Deprecated("RemovedEvent is gone", Windows.Foundation.Metadata.DeprecationType.Remove, 2u)] + public event System.EventHandler RemovedEvent; - public string NewProp => ""; + public event System.EventHandler NewEvent; } // Class implementing INotifyPropertyChanged + custom interface From 4a7be52ce383838838d91b4f994ccd0b94adfc4c Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 18 Jun 2026 17:06:46 -0700 Subject: [PATCH 06/13] Refactor DeprecatedAttribute parsing patterns Simplify and clarify parsing of DeprecatedAttribute arguments in IHasCustomAttributeExtensions. IsRemoved now directly pattern-matches the second fixed argument value (1) instead of binding to a local int. DeprecatedMessage was expanded from an expression-bodied property into an explicit getter with a guarded pattern check and a ToString() return. No behavior change intended; these edits improve readability and avoid unnecessary temporary variable binding. --- .../IHasCustomAttributeExtensions.cs | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/WinRT.Projection.Writer/Extensions/IHasCustomAttributeExtensions.cs b/src/WinRT.Projection.Writer/Extensions/IHasCustomAttributeExtensions.cs index b16b0c738..847839a95 100644 --- a/src/WinRT.Projection.Writer/Extensions/IHasCustomAttributeExtensions.cs +++ b/src/WinRT.Projection.Writer/Extensions/IHasCustomAttributeExtensions.cs @@ -110,9 +110,7 @@ public bool HasWindowsFoundationMetadataAttribute(string name) /// DeprecatedAttribute(string message, DeprecationType type, ...): the second fixed /// argument is the DeprecationType enum, where Deprecate is 0 and Remove is 1. /// - public bool IsRemoved => - member.GetWindowsFoundationMetadataAttribute("DeprecatedAttribute") is { Signature.FixedArguments: [_, { Element: int deprecationType }, ..] } - && deprecationType == 1; + public bool IsRemoved => member.GetWindowsFoundationMetadataAttribute("DeprecatedAttribute") is { Signature.FixedArguments: [_, { Element: 1 }, ..] }; /// /// Gets whether the member is deprecated but not removed (i.e. it is projected with an @@ -129,9 +127,17 @@ public bool HasWindowsFoundationMetadataAttribute(string name) /// DeprecatedAttribute(string message, ...): the first fixed argument is the message. /// AsmResolver returns Utf8String for string custom-attribute args, so it is converted. /// - public string? DeprecatedMessage => - member.GetWindowsFoundationMetadataAttribute("DeprecatedAttribute") is { Signature.FixedArguments: [{ Element: { } message }, ..] } - ? message.ToString() - : null; + public string? DeprecatedMessage + { + get + { + if (member.GetWindowsFoundationMetadataAttribute("DeprecatedAttribute") is not { Signature.FixedArguments: [{ Element: { } message }, ..] }) + { + return null; + } + + return message.ToString(); + } + } } } \ No newline at end of file From 5bf35bb171b65cf37c8a76a64c58cf394d461198 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 18 Jun 2026 21:53:47 -0700 Subject: [PATCH 07/13] Stub removed authored members with E_NOTIMPL in component mode A member marked [Deprecated(DeprecationType.Remove)] is omitted from the projected surface but keeps its ABI vtable slot. The previous approach made the slot dispatch to the authored implementation in component (authoring) mode, but that does not compile: the C# compiler treats a call to a [Deprecated(Remove)] member as an obsolete-as-error (CS0619), which cannot be suppressed with #pragma warning disable. Generated code therefore cannot call a removed member. Removed members now return E_NOTIMPL in both consuming and component mode, keeping the behavior consistent and the vtable layout stable for existing native callers (the slot is preserved, only stubbed): - AbiInterfaceFactory: the removed-member E_NOTIMPL stub is no longer gated on consuming mode, and the per-event ConditionalWeakTable is skipped for removed events (the stubbed accessors never use it, so it would be an unused field). - ComponentFactory: the activation/static factory class no longer emits forwarders for removed methods, properties, and events. The projected factory/static interface already omits them and their slot is stubbed, so the forwarder would only produce a call to the obsolete authored member. - AuthoringConsumptionTest: removed instance and static members, and the removed event, are now expected to throw E_NOTIMPL. The new members (which sit after the removed slots in vtable order) still dispatch correctly, proving the removed slots remain in place. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Tests/AuthoringConsumptionTest/test.cpp | 17 +++++++------ .../Factories/AbiInterfaceFactory.cs | 24 +++++++++---------- .../Factories/ComponentFactory.cs | 18 ++++++++++++-- 3 files changed, 36 insertions(+), 23 deletions(-) diff --git a/src/Tests/AuthoringConsumptionTest/test.cpp b/src/Tests/AuthoringConsumptionTest/test.cpp index 2c04327fe..a174f66c3 100644 --- a/src/Tests/AuthoringConsumptionTest/test.cpp +++ b/src/Tests/AuthoringConsumptionTest/test.cpp @@ -984,20 +984,19 @@ TEST(AuthoringTest, DeprecatedMembersClass) EXPECT_EQ(obj.OldProp(), L"OldProp"); EXPECT_EQ(obj.NewProp(), L"NewProp"); - // Members marked [Deprecated(DeprecationType.Remove)] keep their vtable slot and dispatch to the - // authored implementation, so existing callers compiled against them keep working. - obj.RemovedMethod(); - EXPECT_EQ(obj.RemovedProp(), L"RemovedProp"); - - // The same applies to static members, which dispatch through the generated static factory. DeprecatedMembersClass::OldStatic(); - DeprecatedMembersClass::RemovedStatic(); DeprecatedMembersClass::NewStatic(); - // Events of every deprecation kind can be subscribed to and revoked. auto oldToken = obj.OldEvent(auto_revoke, [](IInspectable const&, int32_t const&) {}); - auto removedToken = obj.RemovedEvent(auto_revoke, [](IInspectable const&, int32_t const&) {}); auto newToken = obj.NewEvent(auto_revoke, [](IInspectable const&, int32_t const&) {}); + + // Members marked [Deprecated(DeprecationType.Remove)] are omitted from the projection but keep their + // vtable slot, stubbed to return E_NOTIMPL. The new members above sit after the removed slots in + // vtable order and still dispatch correctly, which proves the removed slots remain in place. + EXPECT_THROW(obj.RemovedMethod(), hresult_not_implemented); + EXPECT_THROW(obj.RemovedProp(), hresult_not_implemented); + EXPECT_THROW(DeprecatedMembersClass::RemovedStatic(), hresult_not_implemented); + EXPECT_THROW(obj.RemovedEvent([](IInspectable const&, int32_t const&) {}), hresult_not_implemented); } TEST(AuthoringTest, FullFeaturedClass) diff --git a/src/WinRT.Projection.Writer/Factories/AbiInterfaceFactory.cs b/src/WinRT.Projection.Writer/Factories/AbiInterfaceFactory.cs index 50a2f40e6..1dd82c447 100644 --- a/src/WinRT.Projection.Writer/Factories/AbiInterfaceFactory.cs +++ b/src/WinRT.Projection.Writer/Factories/AbiInterfaceFactory.cs @@ -395,9 +395,17 @@ void EmitOneDoAbi(MethodDefinition method) MethodSignatureInfo sig = new(method); string mname = method.GetRawName(); - // If this method is an event add accessor, emit the per-event ConditionalWeakTable - // before the Do_Abi method. - if (eventMap is not null && eventMap.TryGetValue(method, out EventDefinition? evt) && evt.AddMethod == method) + // A removed member (DeprecationType.Remove) keeps its vtable slot for ABI compatibility, but + // is no longer part of the projection, so its CCW entry returns E_NOTIMPL. This applies in + // both consuming and component (authoring) mode: the projected interface omits the member, and + // generated code cannot dispatch to it even in component mode, because the C# compiler treats a + // call to a '[Deprecated(Remove)]' member as an obsolete-as-error (CS0619). The vtable slot is + // preserved so the layout stays stable for existing native callers. + bool removed = IsAbiMemberRemoved(method, eventMap, propertyMap); + + // If this method is an event add accessor, emit the per-event ConditionalWeakTable before the + // Do_Abi method. Removed events are stubbed to E_NOTIMPL and never use the table, so it is skipped. + if (!removed && eventMap is not null && eventMap.TryGetValue(method, out EventDefinition? evt) && evt.AddMethod == method) { EventTableFactory.EmitEventTableField(writer, context, evt, ifaceFullName); } @@ -408,15 +416,7 @@ void EmitOneDoAbi(MethodDefinition method) private static unsafe int Do_Abi_{{vm}}({{doAbiParams}}) """); - // A removed member (DeprecationType.Remove) keeps its vtable slot for ABI compatibility, - // but is no longer on the projected interface. When consuming a Windows Runtime type, a - // managed object implementing the interface cannot supply the removed member (it is not on - // the projected interface), so its CCW entry returns E_NOTIMPL. In component (authoring) mode - // the dispatch target is the authored implementation (the authored class for instance - // members, or the generated activation/static factory that forwards to it), which still - // defines the member, so the entry keeps dispatching to that implementation to preserve - // binary compatibility for existing native callers. - if (!context.Settings.Component && IsAbiMemberRemoved(method, eventMap, propertyMap)) + if (removed) { writer.WriteLine(); writer.WriteLine(""" diff --git a/src/WinRT.Projection.Writer/Factories/ComponentFactory.cs b/src/WinRT.Projection.Writer/Factories/ComponentFactory.cs index c2e29a11b..a1c6c2956 100644 --- a/src/WinRT.Projection.Writer/Factories/ComponentFactory.cs +++ b/src/WinRT.Projection.Writer/Factories/ComponentFactory.cs @@ -172,7 +172,11 @@ private static void WriteAdditionalActivationFactoryMethods( { foreach (MethodDefinition method in info.Type.Methods) { - if (method.IsConstructor) + // Removed members (DeprecationType.Remove) are omitted from the factory class: the + // projected factory/static interface drops them, their vtable slot is stubbed to + // E_NOTIMPL, and generated code cannot call the authored member anyway (the C# + // compiler treats a call to a '[Deprecated(Remove)]' member as an error). + if (method.IsConstructor || method.IsRemoved) { continue; } @@ -184,7 +188,7 @@ private static void WriteAdditionalActivationFactoryMethods( { foreach (MethodDefinition method in info.Type.Methods) { - if (method.IsConstructor) + if (method.IsConstructor || method.IsRemoved) { continue; } @@ -193,10 +197,20 @@ private static void WriteAdditionalActivationFactoryMethods( } foreach (PropertyDefinition prop in info.Type.Properties) { + if ((prop.GetMethod ?? prop.SetMethod) is { IsRemoved: true }) + { + continue; + } + WriteStaticFactoryProperty(writer, context, prop, projectedTypeName); } foreach (EventDefinition evt in info.Type.Events) { + if (evt.AddMethod is { IsRemoved: true }) + { + continue; + } + WriteStaticFactoryEvent(writer, context, evt, projectedTypeName); } } From b9849fa69cf95a8889ad16c8280a4b58c2050b72 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 18 Jun 2026 22:08:26 -0700 Subject: [PATCH 08/13] Don't reference removed members from the authoring consumption test A member marked [Deprecated(DeprecationType.Remove)] is omitted from the projection, so consuming code should not reference it. The test no longer calls the removed members: it exercises the deprecated and new members, and relies on the new members (which sit after the removed slots in vtable order) dispatching correctly to confirm the removed slots are still preserved. This also avoids depending on the C++/WinRT projection's choice to keep removed members callable. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Tests/AuthoringConsumptionTest/test.cpp | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/Tests/AuthoringConsumptionTest/test.cpp b/src/Tests/AuthoringConsumptionTest/test.cpp index a174f66c3..f9245fe3d 100644 --- a/src/Tests/AuthoringConsumptionTest/test.cpp +++ b/src/Tests/AuthoringConsumptionTest/test.cpp @@ -978,7 +978,11 @@ TEST(AuthoringTest, DeprecatedMembersClass) { DeprecatedMembersClass obj; - // Members marked [Deprecated(DeprecationType.Deprecate)] and the new members remain fully usable. + // Members marked [Deprecated(DeprecationType.Remove)] are omitted from the projection, so they are + // intentionally not referenced here. Their ABI vtable slot is still preserved (stubbed to E_NOTIMPL): + // the new members below sit after the removed slots in vtable order, so their correct dispatch confirms + // the removed slots remain in place and the layout did not shift. The deprecated members and the new + // members both remain projected and fully usable. obj.OldMethod(); obj.NewMethod(); EXPECT_EQ(obj.OldProp(), L"OldProp"); @@ -989,14 +993,6 @@ TEST(AuthoringTest, DeprecatedMembersClass) auto oldToken = obj.OldEvent(auto_revoke, [](IInspectable const&, int32_t const&) {}); auto newToken = obj.NewEvent(auto_revoke, [](IInspectable const&, int32_t const&) {}); - - // Members marked [Deprecated(DeprecationType.Remove)] are omitted from the projection but keep their - // vtable slot, stubbed to return E_NOTIMPL. The new members above sit after the removed slots in - // vtable order and still dispatch correctly, which proves the removed slots remain in place. - EXPECT_THROW(obj.RemovedMethod(), hresult_not_implemented); - EXPECT_THROW(obj.RemovedProp(), hresult_not_implemented); - EXPECT_THROW(DeprecatedMembersClass::RemovedStatic(), hresult_not_implemented); - EXPECT_THROW(obj.RemovedEvent([](IInspectable const&, int32_t const&) {}), hresult_not_implemented); } TEST(AuthoringTest, FullFeaturedClass) From 9d657c7df8280f76e217db1e12b4f15cd9818dd4 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 18 Jun 2026 22:16:36 -0700 Subject: [PATCH 09/13] Return E_NOTIMPL when a type's default constructor is removed A type whose parameterless constructor is marked [Deprecated(DeprecationType.Remove)] is no longer default-activatable: the activation factory previously emitted 'new T()', which does not compile because the C# compiler treats a call to a removed member as an obsolete-as-error (CS0619). ActivateInstance now treats such a type as non-activatable and throws (marshalling to E_NOTIMPL), consistent with how removed members are handled elsewhere. Parameterized constructors, if any, are unaffected and continue to activate through their factory interface. HasActivatableDefaultConstructor replaces HasDefaultConstructor (its only caller), returning true only for a default constructor that is not removed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Extensions/TypeDefinitionExtensions.cs | 14 ++++++++++---- .../Factories/ComponentFactory.cs | 5 ++++- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/WinRT.Projection.Writer/Extensions/TypeDefinitionExtensions.cs b/src/WinRT.Projection.Writer/Extensions/TypeDefinitionExtensions.cs index 79cf0e43a..709085b2a 100644 --- a/src/WinRT.Projection.Writer/Extensions/TypeDefinitionExtensions.cs +++ b/src/WinRT.Projection.Writer/Extensions/TypeDefinitionExtensions.cs @@ -195,16 +195,22 @@ public void MarkRequiredInterfacesVisited(MetadataCache cache, HashSet - /// Returns whether the type declares a parameterless instance constructor. + /// Returns whether the type declares a parameterless instance constructor that is usable for + /// default activation, i.e. one that is not marked as removed ([Deprecated(DeprecationType.Remove)]). /// - /// if the type has a default constructor; otherwise . - public bool HasDefaultConstructor() + /// + /// A removed default constructor is omitted from the projection, so the type is no longer + /// default-activatable: the activation factory cannot emit new T() for it (the C# compiler + /// treats a call to a removed member as an error), and default activation returns E_NOTIMPL instead. + /// + /// if the type has a non-removed default constructor; otherwise . + public bool HasActivatableDefaultConstructor() { foreach (MethodDefinition m in type.Methods) { if (m.IsDefaultConstructor) { - return true; + return !m.IsRemoved; } } diff --git a/src/WinRT.Projection.Writer/Factories/ComponentFactory.cs b/src/WinRT.Projection.Writer/Factories/ComponentFactory.cs index a1c6c2956..6a4c8b493 100644 --- a/src/WinRT.Projection.Writer/Factories/ComponentFactory.cs +++ b/src/WinRT.Projection.Writer/Factories/ComponentFactory.cs @@ -86,7 +86,10 @@ void WriteBaseInterfaceList(IndentedTextWriter writer) // Writes the body of the 'ActivateInstance' method (it throws for non-activatable types) void WriteActivateInstanceBody(IndentedTextWriter writer) { - bool isActivatable = !type.IsStatic && type.HasDefaultConstructor(); + // A type whose default constructor is removed ([Deprecated(DeprecationType.Remove)]) is no longer + // default-activatable: 'new T()' cannot be emitted (it would call the removed authored member), + // so default activation falls through to the 'throw' below, which marshals to E_NOTIMPL. + bool isActivatable = !type.IsStatic && type.HasActivatableDefaultConstructor(); if (isActivatable) { From 7346824e45f7a0531b9f2239bf0bef5797a30c9e Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 27 Jul 2026 13:50:53 -0700 Subject: [PATCH 10/13] Skip removed factories when deciding ref-mode constructor emission 'WriteAttributedTypes' skips factory interfaces and factory methods that are marked '[Deprecated(..., DeprecationType.Remove, ...)]', but the two predicates that mirror it in reference-projection mode did not, so a class whose activation or composable factory was entirely removed was reported as emitting constructors when it emits none. The synthetic non-public parameterless constructor was then skipped and the C# compiler synthesized an implicit public one in its place, putting a constructor on the reference surface that the implementation projection does not have (so a consumer's 'new T()' would compile and then fail at runtime with a missing method). 'EmitsParameterlessConstructor' now skips removed factories and overloads, and the inline 'hasRefModeCtors' loop in 'ClassFactory' is replaced by a new 'EmitsAnyConstructor' next to it, so both predicates stay in sync with what 'WriteAttributedTypes' actually emits. The per-factory-interface part is exposed as 'HasActivatableFactoryMethod' on 'TypeDefinitionExtensions', alongside the existing 'HasActivatableDefaultConstructor'. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: c7bf41f2-a686-4f26-ae40-a5a21d432ede --- .../Extensions/TypeDefinitionExtensions.cs | 24 +++++++++ .../Factories/ClassFactory.cs | 18 ++----- .../ConstructorFactory.AttributedTypes.cs | 50 ++++++++++++++++++- 3 files changed, 76 insertions(+), 16 deletions(-) diff --git a/src/WinRT.Projection.Writer/Extensions/TypeDefinitionExtensions.cs b/src/WinRT.Projection.Writer/Extensions/TypeDefinitionExtensions.cs index 709085b2a..b41636af5 100644 --- a/src/WinRT.Projection.Writer/Extensions/TypeDefinitionExtensions.cs +++ b/src/WinRT.Projection.Writer/Extensions/TypeDefinitionExtensions.cs @@ -217,6 +217,30 @@ public bool HasActivatableDefaultConstructor() return false; } + /// + /// Returns whether the type (an activation or composable factory interface) declares at least one + /// method that still projects to a constructor, i.e. one that is neither special nor marked as + /// removed ([Deprecated(DeprecationType.Remove)]). + /// + /// + /// A factory interface with no such methods contributes no constructors to the projected class, so + /// (in reference-projection mode) the class needs a synthetic non-public parameterless constructor + /// to suppress the C# compiler's implicit public default constructor. + /// + /// if the factory interface has a non-removed factory method; otherwise . + public bool HasActivatableFactoryMethod() + { + foreach (MethodDefinition m in type.GetNonSpecialMethods()) + { + if (!m.IsRemoved) + { + return true; + } + } + + return false; + } + /// /// Returns whether the type has a base type that is not /// (i.e. the type derives from a real WinRT/.NET class). diff --git a/src/WinRT.Projection.Writer/Factories/ClassFactory.cs b/src/WinRT.Projection.Writer/Factories/ClassFactory.cs index 368a9d4fa..709fe3cd4 100644 --- a/src/WinRT.Projection.Writer/Factories/ClassFactory.cs +++ b/src/WinRT.Projection.Writer/Factories/ClassFactory.cs @@ -636,23 +636,11 @@ void WriteCtorBody(IndentedTextWriter writer) // // Sealed classes can never be a base, so they only need case 1; unsealed classes need a // parameterless ctor whenever they don't already emit one (which also covers case 1). + // Both checks live in 'ConstructorFactory' so they stay in sync with what it actually emits + // (in particular, they must agree on which factories and overloads are skipped as removed). if (type.IsSealed) { - bool hasRefModeCtors = false; - foreach (KeyValuePair kv in AttributedTypes.Get(type, context.Cache)) - { - AttributedType factory = kv.Value; - - // Activatable always emits at least one ctor; a composable factory only emits ctors - // when it has methods (a composable factory with no methods emits none). - if (factory.Activatable || (factory.Composable && factory.Type is not null && factory.Type.Methods.Count > 0)) - { - hasRefModeCtors = true; - break; - } - } - - if (!hasRefModeCtors) + if (!ConstructorFactory.EmitsAnyConstructor(type, context.Cache)) { RefModeStubFactory.EmitSyntheticPrivateCtor(writer, typeName, isSealed: true); } diff --git a/src/WinRT.Projection.Writer/Factories/ConstructorFactory.AttributedTypes.cs b/src/WinRT.Projection.Writer/Factories/ConstructorFactory.AttributedTypes.cs index bbd7b4642..2cbc4418c 100644 --- a/src/WinRT.Projection.Writer/Factories/ConstructorFactory.AttributedTypes.cs +++ b/src/WinRT.Projection.Writer/Factories/ConstructorFactory.AttributedTypes.cs @@ -225,6 +225,47 @@ public static void WriteFactoryConstructors(IndentedTextWriter writer, Projectio } } + /// + /// Determines whether emits at least one public constructor for + /// the given runtime class. + /// + /// + /// Used in reference-projection mode to decide whether a sealed class needs a synthetic non-public + /// parameterless constructor to suppress the C# compiler's implicit public default constructor + /// (see ). Emitting it matters for more than + /// tidiness: the implementation projection never emits an implicit public default constructor, so + /// leaving one on the reference surface would let consumers compile a new T() call that fails + /// at runtime against the implementation projection. + /// + public static bool EmitsAnyConstructor(TypeDefinition classType, MetadataCache cache) + { + foreach (KeyValuePair kv in AttributedTypes.Get(classType, cache)) + { + AttributedType factory = kv.Value; + + // A removed factory interface is skipped entirely by 'WriteAttributedTypes', so it emits nothing + if (factory.Type is { IsRemoved: true }) + { + continue; + } + + // A default '[Activatable(uint version)]' (no factory interface) always emits 'public TypeName()' + if (factory.Activatable && factory.Type is null) + { + return true; + } + + // Both activation and composable factories emit one constructor per factory method, so a factory + // whose methods are all special or removed (or which has none at all) emits no constructors. + if ((factory.Activatable || factory.Composable) && factory.Type is { } factoryType && factoryType.HasActivatableFactoryMethod()) + { + return true; + } + } + + return false; + } + /// /// Determines whether emits at least one parameterless public /// constructor for the given runtime class (a default [Activatable] ctor, an activation-factory @@ -241,6 +282,12 @@ public static bool EmitsParameterlessConstructor(TypeDefinition classType, Metad { AttributedType factory = kv.Value; + // A removed factory interface is skipped entirely by 'WriteAttributedTypes', so it emits nothing + if (factory.Type is { IsRemoved: true }) + { + continue; + } + // A default '[Activatable(uint version)]' (no factory interface) emits 'public TypeName()'. if (factory.Activatable && factory.Type is null) { @@ -259,7 +306,8 @@ public static bool EmitsParameterlessConstructor(TypeDefinition classType, Metad { foreach (MethodDefinition method in factory.Type.Methods) { - if (method.IsSpecial) + // Special methods and removed overloads emit no constructor + if (method.IsSpecial || method.IsRemoved) { continue; } From 5509ca34ecbfd19caf06aa3a2cf6dd0be60e3d6b Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 27 Jul 2026 14:05:23 -0700 Subject: [PATCH 11/13] Add test coverage for deprecated and removed constructors 'TestComponentCSharp' gains three runtime classes that cover the constructor shapes the projection has to handle: one with a deprecated constructor, a removed one, and a live one declared after it (so the removed factory slot has to stay in place for the live one to dispatch correctly), plus a sealed (activatable) and an unsealed (composable) class whose only constructor is removed. The unit tests assert both projections. The reference projection is checked at compile time with an overload pair whose 'new()' constrained member is only a candidate when the type really exposes a public parameterless constructor, which is what catches a class that dropped every constructor without emitting a non-public one in its place (the C# compiler would then synthesize an implicit public one that the implementation projection does not have). The implementation projection is checked at run time through reflection, and by constructing and calling the types. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: c7bf41f2-a686-4f26-ae40-a5a21d432ede --- .../TestComponentCSharp/DeprecatedClasses.cpp | 60 ++++++++++++++++ .../TestComponentCSharp/DeprecatedClasses.h | 62 +++++++++++++++++ .../TestComponentCSharp.idl | 50 ++++++++++++++ .../TestComponentCSharp.vcxproj | 2 + .../TestComponentCSharp.vcxproj.filters | 2 + .../UnitTest/TestComponentCSharp_Tests.cs | 68 +++++++++++++++++++ 6 files changed, 244 insertions(+) create mode 100644 src/Tests/TestComponentCSharp/DeprecatedClasses.cpp create mode 100644 src/Tests/TestComponentCSharp/DeprecatedClasses.h diff --git a/src/Tests/TestComponentCSharp/DeprecatedClasses.cpp b/src/Tests/TestComponentCSharp/DeprecatedClasses.cpp new file mode 100644 index 000000000..d55d5b0c5 --- /dev/null +++ b/src/Tests/TestComponentCSharp/DeprecatedClasses.cpp @@ -0,0 +1,60 @@ +#include "pch.h" +#include "DeprecatedClasses.h" +#include "DeprecatedConstructorClass.g.cpp" +#include "RemovedActivationClass.g.cpp" +#include "RemovedComposableClass.g.cpp" + +namespace winrt::TestComponentCSharp::implementation +{ + // The value encodes which constructor ran, so a test can prove the surviving constructor still + // dispatches through its original factory slot even though the one before it was removed. + DeprecatedConstructorClass::DeprecatedConstructorClass(int32_t first) + { + m_value = first; + } + + DeprecatedConstructorClass::DeprecatedConstructorClass(int32_t first, int32_t second) + { + m_value = first + second; + } + + DeprecatedConstructorClass::DeprecatedConstructorClass(int32_t first, int32_t second, int32_t third) + { + m_value = first + second + third; + } + + int32_t DeprecatedConstructorClass::Value() + { + return m_value; + } + + RemovedActivationClass::RemovedActivationClass(int32_t initialValue) + { + m_value = initialValue; + } + + TestComponentCSharp::RemovedActivationClass RemovedActivationClass::Create(int32_t initialValue) + { + return winrt::make(initialValue); + } + + int32_t RemovedActivationClass::Value() + { + return m_value; + } + + TestComponentCSharp::RemovedComposableClass RemovedComposableClass::Create(int32_t initialValue) + { + return winrt::make(initialValue); + } + + RemovedComposableClass::RemovedComposableClass(int32_t initialValue) + { + m_value = initialValue; + } + + int32_t RemovedComposableClass::Value() + { + return m_value; + } +} diff --git a/src/Tests/TestComponentCSharp/DeprecatedClasses.h b/src/Tests/TestComponentCSharp/DeprecatedClasses.h new file mode 100644 index 000000000..1f26ec32f --- /dev/null +++ b/src/Tests/TestComponentCSharp/DeprecatedClasses.h @@ -0,0 +1,62 @@ +#pragma once +#include "DeprecatedConstructorClass.g.h" +#include "RemovedActivationClass.g.h" +#include "RemovedComposableClass.g.h" + +namespace winrt::TestComponentCSharp::implementation +{ + // Each constructor stores a distinct value so a test can tell which factory slot was + // actually dispatched to (the removed overloads keep their slot, but are not projected). + struct DeprecatedConstructorClass : DeprecatedConstructorClassT + { + DeprecatedConstructorClass(int32_t first); + DeprecatedConstructorClass(int32_t first, int32_t second); + DeprecatedConstructorClass(int32_t first, int32_t second, int32_t third); + + int32_t Value(); + + private: + int32_t m_value{ 0 }; + }; + + struct RemovedActivationClass : RemovedActivationClassT + { + RemovedActivationClass(int32_t initialValue); + + static TestComponentCSharp::RemovedActivationClass Create(int32_t initialValue); + + int32_t Value(); + + private: + int32_t m_value{ 0 }; + }; + + struct RemovedComposableClass : RemovedComposableClassT + { + // The parameterless constructor is the one the (removed) composable factory method uses; + // the other is implementation-only, for the static factory method below + RemovedComposableClass() = default; + explicit RemovedComposableClass(int32_t initialValue); + + static TestComponentCSharp::RemovedComposableClass Create(int32_t initialValue); + + int32_t Value(); + + private: + int32_t m_value{ 0 }; + }; +} +namespace winrt::TestComponentCSharp::factory_implementation +{ + struct DeprecatedConstructorClass : DeprecatedConstructorClassT + { + }; + + struct RemovedActivationClass : RemovedActivationClassT + { + }; + + struct RemovedComposableClass : RemovedComposableClassT + { + }; +} diff --git a/src/Tests/TestComponentCSharp/TestComponentCSharp.idl b/src/Tests/TestComponentCSharp/TestComponentCSharp.idl index d56bdf92c..ac08a3aa9 100644 --- a/src/Tests/TestComponentCSharp/TestComponentCSharp.idl +++ b/src/Tests/TestComponentCSharp/TestComponentCSharp.idl @@ -798,6 +798,56 @@ And this is another one" Int32 Value{ get; set; }; } + // A constructor has no metadata row of its own: it is projected as a method on the activation + // (or composable) factory interface, and that method is where '[deprecated]' lives. These three + // classes cover the constructor cases the projection has to handle. + + // A deprecated constructor is projected with '[Obsolete]'; a removed one is dropped from the + // projection while its factory vtable slot is preserved, so the constructor declared after it + // must still dispatch through the correct slot. + [default_interface] + runtimeclass DeprecatedConstructorClass + { + [deprecated("Use the three-argument constructor instead", deprecate, 1)] + DeprecatedConstructorClass(Int32 first); + + [deprecated("The two-argument constructor is gone", remove, 1)] + DeprecatedConstructorClass(Int32 first, Int32 second); + + DeprecatedConstructorClass(Int32 first, Int32 second, Int32 third); + + Int32 Value{ get; }; + } + + // Every constructor is removed, so the (sealed, activatable) class cannot be constructed from the + // projection at all and instances come from the static factory method instead. + // + // Note: the constructor parameter cannot be named 'value'. MIDL projects a constructor as + // 'CreateInstance' on the activation factory interface and names its '[out, retval]' parameter + // 'value', so a user parameter of that name collides with it (MIDL5161). + [default_interface] + runtimeclass RemovedActivationClass + { + [deprecated("Use RemovedActivationClass.Create instead", remove, 1)] + RemovedActivationClass(Int32 initialValue); + + static RemovedActivationClass Create(Int32 initialValue); + + Int32 Value{ get; }; + } + + // As above, but unsealed, so its factory is composable rather than activatable. + [default_interface] + unsealed runtimeclass RemovedComposableClass + { + [deprecated("Use RemovedComposableClass.Create instead", remove, 1)] + RemovedComposableClass(); + + static RemovedComposableClass Create(Int32 initialValue); + + Int32 Value{ get; }; + } + // Compile time test for sub windows namespace namespace Windows { diff --git a/src/Tests/TestComponentCSharp/TestComponentCSharp.vcxproj b/src/Tests/TestComponentCSharp/TestComponentCSharp.vcxproj index 180eb5b9d..02e88f05d 100644 --- a/src/Tests/TestComponentCSharp/TestComponentCSharp.vcxproj +++ b/src/Tests/TestComponentCSharp/TestComponentCSharp.vcxproj @@ -88,6 +88,7 @@ + @@ -111,6 +112,7 @@ + diff --git a/src/Tests/TestComponentCSharp/TestComponentCSharp.vcxproj.filters b/src/Tests/TestComponentCSharp/TestComponentCSharp.vcxproj.filters index 489731f49..5626e9dd3 100644 --- a/src/Tests/TestComponentCSharp/TestComponentCSharp.vcxproj.filters +++ b/src/Tests/TestComponentCSharp/TestComponentCSharp.vcxproj.filters @@ -21,6 +21,7 @@ + @@ -40,6 +41,7 @@ + diff --git a/src/Tests/UnitTest/TestComponentCSharp_Tests.cs b/src/Tests/UnitTest/TestComponentCSharp_Tests.cs index a811b7de3..e7edfb6a5 100644 --- a/src/Tests/UnitTest/TestComponentCSharp_Tests.cs +++ b/src/Tests/UnitTest/TestComponentCSharp_Tests.cs @@ -2000,6 +2000,74 @@ public void TestFactoriesWithExplicitlyImplementedIUnknown() Assert.AreEqual(22, cls2.Value); } + [TestMethod] + public void TestDeprecatedConstructors() + { + // A '[deprecated]' constructor is still projected, just with '[Obsolete]' on it +#pragma warning disable CS0618 + var deprecated = new DeprecatedConstructorClass(1); +#pragma warning restore CS0618 + + Assert.AreEqual(1, deprecated.Value); + + // The two-argument constructor is '[deprecated(remove)]', so it is not projected at all. + // Its factory vtable slot is preserved though, which is exactly what lets the three-argument + // constructor declared after it still dispatch through the right slot (it sums its arguments, + // so a wrong slot would either fail or produce a different value). + Assert.IsNull(typeof(DeprecatedConstructorClass).GetConstructor([typeof(int), typeof(int)])); + + var live = new DeprecatedConstructorClass(1, 2, 3); + + Assert.AreEqual(6, live.Value); + + static bool IsObsolete(params Type[] parameterTypes) + { + ConstructorInfo constructor = typeof(DeprecatedConstructorClass).GetConstructor(parameterTypes); + + Assert.IsNotNull(constructor); + + return constructor.GetCustomAttribute() is not null; + } + + Assert.IsTrue(IsObsolete(typeof(int))); + Assert.IsFalse(IsObsolete(typeof(int), typeof(int), typeof(int))); + } + + [TestMethod] + public void TestRemovedConstructors() + { + // Every constructor of these two classes is '[deprecated(remove)]', so neither is + // constructible from the projection: the activatable (sealed) one and the composable + // (unsealed) one both have to drop their only constructor. + // + // 'HasPublicParameterlessConstructor' resolves the 'new()' constrained overload only when the + // type *as compiled against* really exposes a public parameterless constructor, so it asserts + // the reference projection's surface. That matters because dropping every constructor without + // emitting a non-public one in its place would let the C# compiler synthesize an implicit + // public parameterless constructor, which the implementation projection does not have. + Assert.IsTrue(HasPublicParameterlessConstructor()); + Assert.IsFalse(HasPublicParameterlessConstructor()); + Assert.IsFalse(HasPublicParameterlessConstructor()); + + // The implementation projection agrees with the reference projection above + Assert.AreEqual(0, typeof(RemovedActivationClass).GetConstructors().Length); + Assert.AreEqual(0, typeof(RemovedComposableClass).GetConstructors().Length); + + // Both types are still fully usable through their static factory methods + Assert.AreEqual(42, RemovedActivationClass.Create(42).Value); + Assert.AreEqual(42, RemovedComposableClass.Create(42).Value); + } + + /// + /// Compile-time probe for a public parameterless constructor: the new() constrained + /// overload is only a candidate when has one, so a call resolves to + /// the fallback overload otherwise. + /// + private static bool HasPublicParameterlessConstructor() where T : new() => true; + + /// + private static bool HasPublicParameterlessConstructor(int _ = 0) => false; + [TestMethod] public void TestStaticMembers() { From cbff6a9313fa45f96e34f1274c08d34ff8c227e9 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 27 Jul 2026 22:52:19 -0700 Subject: [PATCH 12/13] Suppress obsolete diagnostics in generated projection code Now that deprecated Windows Runtime APIs are projected with '[Obsolete]', generated projection code that references them fails to compile under 'TreatWarningsAsErrors' (CS0618). This is not a theoretical concern: the Windows SDK projection does not build, because 'ComInteropExtensions.cs' wraps 'IPlayToManagerInterop' in terms of the deprecated 'Windows.Media.PlayTo.PlayToManager', and the generated projection for 'WindowsRuntime.Internal.winmd' names it in the interop signatures. The '[Obsolete]' attribute is guidance for consumers of a projection, not for the projection itself, which has to name a deprecated type to project it at all. Generated projection files therefore suppress CS0612/CS0618, alongside the diagnostics they already suppress. The suppressions are per-file, so user code that calls a deprecated API still gets the warning. Three emission paths need it: the shared file prelude used by every per-namespace projection file, the centralized 'typeof'-based lookup classes (which name every projected type, deprecated ones included), and the hand-written 'ComInteropExtensions.cs' base resource, which only gets the auto-generated banner rather than the full prelude. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: c7bf41f2-a686-4f26-ae40-a5a21d432ede --- .../Extensions/ProjectionWriterExtensions.cs | 1 + .../Factories/MetadataAttributeFactory.cs | 2 ++ .../Resources/Base/ComInteropExtensions.cs | 5 +++++ 3 files changed, 8 insertions(+) diff --git a/src/WinRT.Projection.Writer/Extensions/ProjectionWriterExtensions.cs b/src/WinRT.Projection.Writer/Extensions/ProjectionWriterExtensions.cs index 860263526..fd9cf8c27 100644 --- a/src/WinRT.Projection.Writer/Extensions/ProjectionWriterExtensions.cs +++ b/src/WinRT.Projection.Writer/Extensions/ProjectionWriterExtensions.cs @@ -53,6 +53,7 @@ public void WriteFileHeader(ProjectionEmitContext context) #pragma warning disable CSWINRT3001 // "Type or member '...' is a private implementation detail" #pragma warning disable CSWINRT3002 // "Type '...' is a private implementation detail" #pragma warning disable CS8500 // This takes the address of, gets the size of, or declares a pointer to a managed type + #pragma warning disable CS0612, CS0618 // "'...' is obsolete" (deprecated Windows Runtime APIs are projected as '[Obsolete]', but generated code still has to reference them) """); } diff --git a/src/WinRT.Projection.Writer/Factories/MetadataAttributeFactory.cs b/src/WinRT.Projection.Writer/Factories/MetadataAttributeFactory.cs index aff7bfa81..b98847425 100644 --- a/src/WinRT.Projection.Writer/Factories/MetadataAttributeFactory.cs +++ b/src/WinRT.Projection.Writer/Factories/MetadataAttributeFactory.cs @@ -605,6 +605,7 @@ void WriteMetadataEntries(IndentedTextWriter writer) using WindowsRuntime; #pragma warning disable CSWINRT3001 + #pragma warning disable CS0612, CS0618 namespace ABI; @@ -653,6 +654,7 @@ void WriteInterfaceMappings(IndentedTextWriter writer) using WindowsRuntime; #pragma warning disable CSWINRT3001 + #pragma warning disable CS0612, CS0618 namespace ABI; diff --git a/src/WinRT.Projection.Writer/Resources/Base/ComInteropExtensions.cs b/src/WinRT.Projection.Writer/Resources/Base/ComInteropExtensions.cs index b62492647..ac07dabe9 100644 --- a/src/WinRT.Projection.Writer/Resources/Base/ComInteropExtensions.cs +++ b/src/WinRT.Projection.Writer/Resources/Base/ComInteropExtensions.cs @@ -25,6 +25,11 @@ // minimum Windows SDK that is currently supported. See this mapping // in the Windows SDK projection project. The two should be kept in sync. +// Some of the types wrapped below are deprecated in the Windows SDK, so they are projected with +// '[Obsolete]'. That is guidance for consumers of these extensions, not for the extensions +// themselves, which have to name those types to wrap them. +#pragma warning disable CS0612, CS0618 + using System; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; From 32f8f156d75e86bdb7b51708878070ebeed5822e Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 28 Jul 2026 09:14:26 -0700 Subject: [PATCH 13/13] Suppress the obsolete diagnostic in the PlayToManager interop test 'Windows.Media.PlayTo.PlayToManager' is deprecated in the Windows SDK, so it is now projected with '[Obsolete]'. 'ComInteropTests.TestPlayToManager' covers its interop extensions and therefore has to name it, which breaks the Release legs (warnings are treated as errors there, which is why only they failed). This is the feature behaving as intended: unlike the generated projection code suppressed in the previous commit, this is ordinary consumer code, and the warning correctly points out that it calls a deprecated API. The test covers those extensions deliberately, so it acknowledges the diagnostic with a narrow suppression at the call site, matching how the suite already handles the '[Experimental]' ('CS8305') and obsolete-override ('CS0672') tests. 'PlayToManager' is the only deprecated type this test class touches, and the compiler reported no other obsolete usage across the solution build. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: c7bf41f2-a686-4f26-ae40-a5a21d432ede --- src/Tests/UnitTest/ComInteropTests.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Tests/UnitTest/ComInteropTests.cs b/src/Tests/UnitTest/ComInteropTests.cs index 18907ece2..1c034f65a 100644 --- a/src/Tests/UnitTest/ComInteropTests.cs +++ b/src/Tests/UnitTest/ComInteropTests.cs @@ -76,8 +76,13 @@ public void TestInputPane() [TestMethod] public void TestPlayToManager() { + // 'PlayToManager' is deprecated in the Windows SDK, so it is projected with '[Obsolete]'. This + // test covers its interop extensions regardless, so suppress 'CS0618' to keep the intentional + // usage from breaking the build (warnings are treated as errors). +#pragma warning disable CS0618 Assert.ThrowsExactly(() => PlayToManager.GetForWindow(new IntPtr(0))); PlayToManager.ShowPlayToUIForWindow(new IntPtr(0)); +#pragma warning restore CS0618 } [TestMethod]