From f199e77ad75425c985dd45fbab685192b1282014 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 18 Jun 2026 15:32:21 -0700 Subject: [PATCH 1/9] 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 a5f165948..041a8a9d5 100644 --- a/src/WinRT.Projection.Writer/Factories/CustomAttributeFactory.cs +++ b/src/WinRT.Projection.Writer/Factories/CustomAttributeFactory.cs @@ -459,6 +459,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)}\")]"); + } + } + /// /// Writes the projected type-level custom attributes for . Each emitted /// attribute is on its own line, terminated with a newline. If no attributes apply, emits nothing. @@ -470,6 +495,7 @@ public static void WriteCustomAttributes(IndentedTextWriter writer, ProjectionEm public static void WriteTypeCustomAttributes(IndentedTextWriter writer, ProjectionEmitContext context, TypeDefinition type, bool enablePlatformAttrib) { WriteCustomAttributes(writer, context, type, enablePlatformAttrib); + WriteObsoleteAttribute(writer, type); } /// @@ -490,6 +516,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 df394f58c4e4a50176c511832a596673cb115646 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 18 Jun 2026 15:32:36 -0700 Subject: [PATCH 2/9] 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 | 39 ++++++++++++++++ .../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, 244 insertions(+) diff --git a/src/WinRT.Projection.Writer/Builders/ProjectionFileBuilder.cs b/src/WinRT.Projection.Writer/Builders/ProjectionFileBuilder.cs index 44c4c8faa..c66c9af34 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 c2b5d789c..103c77630 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 900167fbb..e18768932 100644 --- a/src/WinRT.Projection.Writer/Factories/ClassMembersFactory.WriteClassMembers.cs +++ b/src/WinRT.Projection.Writer/Factories/ClassMembersFactory.WriteClassMembers.cs @@ -84,6 +84,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 efd44d6af..6e186a9cb 100644 --- a/src/WinRT.Projection.Writer/Factories/ClassMembersFactory.WriteInterfaceMembers.cs +++ b/src/WinRT.Projection.Writer/Factories/ClassMembersFactory.WriteInterfaceMembers.cs @@ -260,6 +260,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; @@ -316,6 +323,9 @@ private static void WriteInterfaceMembers(IndentedTextWriter writer, ProjectionE functionName: accessorName, interopType: genericInteropType, parameterList: $"WindowsRuntimeObjectReference thisReference{accessorParams}"); + + CustomAttributeFactory.WriteObsoleteAttribute(writer, method); + writer.WriteLine(isMultiline: true, $$""" {{platformTrimmed}} {{access}}{{methodSpecForThis}}{{ret}} {{name}}({{parms}}) => {{body}} @@ -325,6 +335,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); @@ -366,6 +378,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 @@ -375,9 +396,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) { @@ -416,6 +442,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); @@ -528,6 +561,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 1694c6611..b3d2942d1 100644 --- a/src/WinRT.Projection.Writer/Factories/ConstructorFactory.AttributedTypes.cs +++ b/src/WinRT.Projection.Writer/Factories/ConstructorFactory.AttributedTypes.cs @@ -69,6 +69,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); @@ -111,6 +118,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"; @@ -118,6 +134,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 002fa51c6..18e2424d2 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 9a49411cf..3c91df21f 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); // Only emit Windows.Foundation.Metadata attributes that have a projected form // (Overload, DefaultOverload, AttributeUsage, Experimental). WriteMethodCustomAttributes(writer, 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 62fd1b734..9d2b12011 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 5275e3681..382da94cf 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 4a06e9457b32f6d1dc34ced248bc6044df214763 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 18 Jun 2026 15:32:47 -0700 Subject: [PATCH 3/9] 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 df2b8e48b036f76f4ecc428f7d9c4bd5d7b467e0 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 18 Jun 2026 16:20:33 -0700 Subject: [PATCH 4/9] 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 764cc8000..d4f73b2b5 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 283f6ef41..5b4ed5fed 100644 --- a/src/WinRT.WinMD.Generator/Writers/WinMDWriter.Members.cs +++ b/src/WinRT.WinMD.Generator/Writers/WinMDWriter.Members.cs @@ -224,6 +224,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) { @@ -245,7 +248,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; @@ -275,7 +278,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; @@ -290,8 +293,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); } /// @@ -319,7 +325,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); } /// @@ -353,6 +362,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; @@ -376,7 +387,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; @@ -422,7 +433,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 928318644f6d4b7d7dc655e318abccb6463b10a2 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 18 Jun 2026 16:32:17 -0700 Subject: [PATCH 5/9] 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 12352265e..84ab048c8 100644 --- a/src/Tests/AuthoringConsumptionTest/test.cpp +++ b/src/Tests/AuthoringConsumptionTest/test.cpp @@ -878,10 +878,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 9eeb3111c..66d64155e 100644 --- a/src/Tests/AuthoringTest/Program.cs +++ b/src/Tests/AuthoringTest/Program.cs @@ -2210,12 +2210,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 9df1f8ee25c49dbfbaad13a01f2c7494174cbb1b Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 18 Jun 2026 17:06:46 -0700 Subject: [PATCH 6/9] 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 ae451e57bebc36c0e43a3db3d88bdbcc6c1001a6 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 18 Jun 2026 21:53:47 -0700 Subject: [PATCH 7/9] 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 84ab048c8..3ec9fc814 100644 --- a/src/Tests/AuthoringConsumptionTest/test.cpp +++ b/src/Tests/AuthoringConsumptionTest/test.cpp @@ -885,20 +885,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 4c559d9c3..54ae79f40 100644 --- a/src/WinRT.Projection.Writer/Factories/ComponentFactory.cs +++ b/src/WinRT.Projection.Writer/Factories/ComponentFactory.cs @@ -147,7 +147,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; } @@ -159,7 +163,7 @@ private static void WriteAdditionalActivationFactoryMethods( { foreach (MethodDefinition method in info.Type.Methods) { - if (method.IsConstructor) + if (method.IsConstructor || method.IsRemoved) { continue; } @@ -168,10 +172,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 874513404800eef0b215a9ff5cc1dd3be0fa762d Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 18 Jun 2026 22:08:26 -0700 Subject: [PATCH 8/9] 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 3ec9fc814..84776c14e 100644 --- a/src/Tests/AuthoringConsumptionTest/test.cpp +++ b/src/Tests/AuthoringConsumptionTest/test.cpp @@ -879,7 +879,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"); @@ -890,14 +894,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 c5e747a164e30b14ad6e6ec0c32d9920f8137b64 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 18 Jun 2026 22:16:36 -0700 Subject: [PATCH 9/9] 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 54ae79f40..1f2f41d4f 100644 --- a/src/WinRT.Projection.Writer/Factories/ComponentFactory.cs +++ b/src/WinRT.Projection.Writer/Factories/ComponentFactory.cs @@ -78,7 +78,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) {