Skip to content
Open
16 changes: 14 additions & 2 deletions src/Tests/AuthoringConsumptionTest/test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -878,10 +878,22 @@ TEST(AuthoringTest, AsyncMethodClass)
TEST(AuthoringTest, DeprecatedMembersClass)
{
DeprecatedMembersClass obj;

// 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"");
EXPECT_EQ(obj.NewProp(), L"");
EXPECT_EQ(obj.OldProp(), L"OldProp");
EXPECT_EQ(obj.NewProp(), L"NewProp");

DeprecatedMembersClass::OldStatic();
DeprecatedMembersClass::NewStatic();

auto oldToken = obj.OldEvent(auto_revoke, [](IInspectable const&, int32_t const&) {});
auto newToken = obj.NewEvent(auto_revoke, [](IInspectable const&, int32_t const&) {});
}

TEST(AuthoringTest, FullFeaturedClass)
Expand Down
26 changes: 24 additions & 2 deletions src/Tests/AuthoringTest/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<int> OldEvent;

[Windows.Foundation.Metadata.Deprecated("RemovedEvent is gone", Windows.Foundation.Metadata.DeprecationType.Remove, 2u)]
public event System.EventHandler<int> RemovedEvent;

public string NewProp => "";
public event System.EventHandler<int> NewEvent;
}

// Class implementing INotifyPropertyChanged + custom interface
Expand Down
7 changes: 7 additions & 0 deletions src/WinRT.Projection.Writer/Builders/ProjectionFileBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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}),");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,5 +94,50 @@ public bool HasWindowsFoundationMetadataAttribute(string name)
{
return member.GetAttribute(WellKnownNamespaces.WindowsFoundationMetadata, name);
}

/// <summary>
/// Gets whether the member carries a <c>[Windows.Foundation.Metadata.Deprecated]</c> attribute.
/// </summary>
public bool IsDeprecated => member.HasWindowsFoundationMetadataAttribute("DeprecatedAttribute");

/// <summary>
/// Gets whether the member is marked as removed: it carries a
/// <c>[Windows.Foundation.Metadata.Deprecated]</c> attribute whose <c>DeprecationType</c>
/// is <c>Remove</c>. A removed member is omitted from the projection, while its ABI vtable
/// slot is preserved (stubbed to return <c>E_NOTIMPL</c>) for binary compatibility.
/// </summary>
/// <remarks>
/// <c>DeprecatedAttribute(string message, DeprecationType type, ...)</c>: the second fixed
/// argument is the <c>DeprecationType</c> enum, where <c>Deprecate</c> is 0 and <c>Remove</c> is 1.
/// </remarks>
public bool IsRemoved => member.GetWindowsFoundationMetadataAttribute("DeprecatedAttribute") is { Signature.FixedArguments: [_, { Element: 1 }, ..] };

/// <summary>
/// Gets whether the member is deprecated but not removed (i.e. it is projected with an
/// <c>[Obsolete]</c> attribute rather than being omitted).
/// </summary>
public bool IsDeprecatedNotRemoved => member.IsDeprecated && !member.IsRemoved;

/// <summary>
/// Gets the message from the member's <c>[Windows.Foundation.Metadata.Deprecated]</c>
/// attribute (the first fixed argument), or <see langword="null"/> if the member is not
/// deprecated or the attribute carries no message.
/// </summary>
/// <remarks>
/// <c>DeprecatedAttribute(string message, ...)</c>: the first fixed argument is the message.
/// AsmResolver returns <c>Utf8String</c> for string custom-attribute args, so it is converted.
/// </remarks>
public string? DeprecatedMessage
{
get
{
if (member.GetWindowsFoundationMetadataAttribute("DeprecatedAttribute") is not { Signature.FixedArguments: [{ Element: { } message }, ..] })
{
return null;
}

return message.ToString();
}
}
}
}
14 changes: 10 additions & 4 deletions src/WinRT.Projection.Writer/Extensions/TypeDefinitionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -195,16 +195,22 @@ public void MarkRequiredInterfacesVisited(MetadataCache cache, HashSet<TypeDefin
}

/// <summary>
/// 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 (<c>[Deprecated(DeprecationType.Remove)]</c>).
/// </summary>
/// <returns><see langword="true"/> if the type has a default constructor; otherwise <see langword="false"/>.</returns>
public bool HasDefaultConstructor()
/// <remarks>
/// A removed default constructor is omitted from the projection, so the type is no longer
/// default-activatable: the activation factory cannot emit <c>new T()</c> for it (the C# compiler
/// treats a call to a removed member as an error), and default activation returns <c>E_NOTIMPL</c> instead.
/// </remarks>
/// <returns><see langword="true"/> if the type has a non-removed default constructor; otherwise <see langword="false"/>.</returns>
public bool HasActivatableDefaultConstructor()
{
foreach (MethodDefinition m in type.Methods)
{
if (m.IsDefaultConstructor)
{
return true;
return !m.IsRemoved;
}
}

Expand Down
64 changes: 61 additions & 3 deletions src/WinRT.Projection.Writer/Factories/AbiInterfaceFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -371,16 +371,41 @@ public static nint Vtable
// this order: methods first, then properties (setter before getter), then events.
HashSet<MethodDefinition> 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<MethodDefinition, PropertyDefinition> 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)
{
string vm = AbiTypeHelpers.GetVirtualMethodName(type, 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);
}
Expand All @@ -391,6 +416,18 @@ void EmitOneDoAbi(MethodDefinition method)
private static unsafe int Do_Abi_{{vm}}({{doAbiParams}})
""");

if (removed)
{
writer.WriteLine();
writer.WriteLine("""
{
return unchecked((int)0x80004001);
}
""");

return;
}

if (eventMap is not null && eventMap.TryGetValue(method, out EventDefinition? evt2))
{
if (evt2.AddMethod == method)
Expand Down Expand Up @@ -453,6 +490,27 @@ void EmitOneDoAbi(MethodDefinition method)
}
}

/// <summary>
/// Determines whether the CCW entry for <paramref name="method"/> must be stubbed because the
/// member is removed (<c>DeprecationType.Remove</c>). 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.
/// </summary>
private static bool IsAbiMemberRemoved(MethodDefinition method, Dictionary<MethodDefinition, EventDefinition>? eventMap, Dictionary<MethodDefinition, PropertyDefinition> 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;
}

/// <summary>
/// Emits the per-interface marshaller class (<c>{Name}Marshaller</c>) with the boxing/unboxing helpers used by user code to marshal references across the ABI.
/// </summary>
Expand Down
36 changes: 36 additions & 0 deletions src/WinRT.Projection.Writer/Factories/AbiInterfaceIDicFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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);

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand All @@ -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);

Expand Down Expand Up @@ -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);
Expand Down
Loading