Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,18 @@
name="AuthoringTest.CustomInterfaceGuidClass"
threadingModel="both"
xmlns="urn:schemas-microsoft-com:winrt.v1" />
<activatableClass
name="AuthoringTest.CustomOverloadNamesClass"
threadingModel="both"
xmlns="urn:schemas-microsoft-com:winrt.v1" />
<activatableClass
name="AuthoringTest.OverloadCollisionClass"
threadingModel="both"
xmlns="urn:schemas-microsoft-com:winrt.v1" />
<activatableClass
name="AuthoringTest.DefaultOverloadNotFirstClass"
threadingModel="both"
xmlns="urn:schemas-microsoft-com:winrt.v1" />
<activatableClass
name="AuthoringTest.NonActivatableType"
threadingModel="both"
Expand Down
99 changes: 99 additions & 0 deletions src/Tests/AuthoringConsumptionTest/test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -736,6 +736,105 @@ TEST(AuthoringTest, CustomInterfaceGuid)
EXPECT_EQ(customInterface.HelloWorld(), L"Hello World!");
}

TEST(AuthoringTest, CustomOverloadNames)
{
// Test interface-level overloads with user-specified OverloadAttribute names
CustomOverloadNamesClass obj;
EXPECT_EQ(obj.Lookup(hstring(L"test")), hstring(L"found:test"));
EXPECT_EQ(obj.Lookup(42), 420);

// Through the interface
ICustomOverloadNames iface = obj;
EXPECT_EQ(iface.Lookup(hstring(L"hello")), hstring(L"found:hello"));
EXPECT_EQ(iface.Lookup(7), 70);

// Test class-level overloads with mixed user-specified and auto-generated names
EXPECT_EQ(obj.Transform(hstring(L"abc")), hstring(L"ABC"));
EXPECT_EQ(obj.Transform(5), 10);
EXPECT_EQ(obj.Transform(1.0), 1.5);

// Verify the user-specified ABI names actually appear in the generated projection.
// The abi<ICustomOverloadNames>::type vtable has virtual methods named exactly
// after the [Overload("...")] values. If the winmd had auto-generated names
// (e.g. "Lookup2") instead of "LookupByIndex", these calls would fail to compile
using abi_type = winrt::impl::abi_t<ICustomOverloadNames>;
auto raw = static_cast<abi_type*>(winrt::get_abi(iface));
{
int32_t result = 0;
EXPECT_EQ(raw->LookupByIndex(7, &result), S_OK);
EXPECT_EQ(result, 70);
}
{
bool result = false;
EXPECT_EQ(raw->LookupByFlag(false, &result), S_OK);
EXPECT_TRUE(result);
}

// Same check for the class-level synthesized interface
using class_abi_type = winrt::impl::abi_t<ICustomOverloadNamesClassClass>;
ICustomOverloadNamesClassClass classIface = obj.as<ICustomOverloadNamesClassClass>();
auto rawClass = static_cast<class_abi_type*>(winrt::get_abi(classIface));
{
int32_t result = 0;
EXPECT_EQ(rawClass->TransformNumber(5, &result), S_OK);
EXPECT_EQ(result, 10);
}
{
// The auto-generated overload coexists with the author-named one: because "TransformNumber"
// is author-specified, it does not consume a numeric suffix, so Transform(double) becomes
// "Transform2" (not "Transform3")
double result = 0.0;
EXPECT_EQ(rawClass->Transform2(1.0, &result), S_OK);
EXPECT_EQ(result, 1.5);
}
}

TEST(AuthoringTest, CustomOverloadNamesCollision)
{
// The author names one overload "M2" (matching the auto-generated pattern), so the
// auto-generated name for the remaining overload must skip "M2" and become "M3"
OverloadCollisionClass obj;
EXPECT_EQ(obj.M(hstring(L"abc")), hstring(L"abc"));
EXPECT_EQ(obj.M(21), 42);
EXPECT_TRUE(obj.M(false));

using class_abi_type = winrt::impl::abi_t<IOverloadCollisionClassClass>;
IOverloadCollisionClassClass classIface = obj.as<IOverloadCollisionClassClass>();
auto raw = static_cast<class_abi_type*>(winrt::get_abi(classIface));
{
int32_t result = 0;
EXPECT_EQ(raw->M2(21, &result), S_OK);
EXPECT_EQ(result, 42);
}
{
bool result = false;
EXPECT_EQ(raw->M3(false, &result), S_OK);
EXPECT_TRUE(result);
}
}

TEST(AuthoringTest, CustomOverloadNamesDefaultNotFirst)
{
// [DefaultOverload] is on the second-declared overload, so it keeps the original ABI name
// ("Get") while the author-specified name on the first overload ("GetByIndex") is honored
DefaultOverloadNotFirstClass obj;
EXPECT_EQ(obj.Get(5), 105);
EXPECT_EQ(obj.Get(hstring(L"abc")), hstring(L"key:abc"));

IDefaultOverloadNotFirst iface = obj;
EXPECT_EQ(iface.Get(7), 107);
EXPECT_EQ(iface.Get(hstring(L"z")), hstring(L"key:z"));

// The author-specified name lives on the non-default (first-declared) overload
using abi_type = winrt::impl::abi_t<IDefaultOverloadNotFirst>;
auto raw = static_cast<abi_type*>(winrt::get_abi(iface));
{
int32_t result = 0;
EXPECT_EQ(raw->GetByIndex(7, &result), S_OK);
EXPECT_EQ(result, 107);
}
}

TEST(AuthoringTest, NonActivatableFactory)
{
EXPECT_EQ(NonActivatableFactory::Create().GetText(), L"Test123");
Expand Down
61 changes: 61 additions & 0 deletions src/Tests/AuthoringTest/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1891,6 +1891,67 @@ public sealed class CustomInterfaceGuidClass : ICustomInterfaceGuid
public string HelloWorld() => "Hello World!";
}

// Test that user-specified [OverloadAttribute] names are emitted in the winmd.
// The interface has 3 overloads of Lookup where the non-default ones carry custom ABI names
public interface ICustomOverloadNames
{
[Windows.Foundation.Metadata.DefaultOverload()]
string Lookup(string key);

[Windows.Foundation.Metadata.Overload("LookupByIndex")]
int Lookup(int index);

[Windows.Foundation.Metadata.Overload("LookupByFlag")]
bool Lookup(bool flag);
}

// Test a mix of user-specified and auto-generated OverloadAttribute names
public sealed class CustomOverloadNamesClass : ICustomOverloadNames
{
public string Lookup(string key) => "found:" + key;
public int Lookup(int index) => index * 10;
public bool Lookup(bool flag) => !flag;

// Class-level overloads: first gets a custom name, second is auto-generated
[Windows.Foundation.Metadata.DefaultOverload()]
public string Transform(string input) => input.ToUpper();

[Windows.Foundation.Metadata.Overload("TransformNumber")]
public int Transform(int value) => value * 2;

public double Transform(double value) => value + 0.5;
}

// Auto-generated overload names must skip any name the author already claimed: here the author
// names one overload "M2" (the auto-generated pattern), so the remaining overload becomes "M3"
public sealed class OverloadCollisionClass
{
[Windows.Foundation.Metadata.DefaultOverload()]
public string M(string s) => s;

[Windows.Foundation.Metadata.Overload("M2")]
public int M(int i) => i * 2;

public bool M(bool b) => !b;
}

// [DefaultOverload] is on a non-first overload: it must keep the original name while the
// author-specified name on the first-declared overload is still honored
public interface IDefaultOverloadNotFirst
{
[Windows.Foundation.Metadata.Overload("GetByIndex")]
int Get(int index);

[Windows.Foundation.Metadata.DefaultOverload()]
string Get(string key);
}

public sealed class DefaultOverloadNotFirstClass : IDefaultOverloadNotFirst
{
public int Get(int index) => index + 100;
public string Get(string key) => "key:" + key;
}

public sealed class NonActivatableType
{
private readonly string _text;
Expand Down
7 changes: 5 additions & 2 deletions src/WinRT.WinMD.Generator/Writers/WinMDWriter.Attributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ private void CopyCustomAttributes(IHasCustomAttribute source, IHasCustomAttribut
/// </summary>
/// <remarks>
/// Filters out attributes that are handled separately by the generator (e.g., <c>[Guid]</c>,
/// <c>[Version]</c>), compiler-generated attributes (e.g., from <c>System.Runtime.CompilerServices</c>),
/// <c>[Version]</c>, <c>[Overload]</c>), compiler-generated attributes (e.g., from <c>System.Runtime.CompilerServices</c>),
/// non-public attribute types, and attributes with unreadable signatures.
/// </remarks>
/// <param name="attribute">The custom attribute to evaluate.</param>
Expand All @@ -371,11 +371,14 @@ private static bool ShouldCopyAttribute(CustomAttribute attribute, RuntimeContex
return false;
}

// Skip attributes already handled separately by the generator
// Skip attributes already handled separately by the generator.
// '[Overload]' is emitted by 'AddOverloadAttributesForType', which honors any author-specified
// name (see 'RecordUserSpecifiedOverloadName'), so copying it here would produce duplicates.
if (attributeTypeName is
"System.Runtime.InteropServices.GuidAttribute" or
"WindowsRuntime.Xaml.GeneratedCustomPropertyProviderAttribute" or
"Windows.Foundation.Metadata.VersionAttribute" or
"Windows.Foundation.Metadata.OverloadAttribute" or
"System.Reflection.DefaultMemberAttribute")
{
return false;
Expand Down
118 changes: 104 additions & 14 deletions src/WinRT.WinMD.Generator/Writers/WinMDWriter.Finalization.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,10 +81,13 @@ public void FinalizeGeneration()
CopyCustomAttributes(declaration.InputType, declaration.OutputType);
}

// Phase 4: Add overload attributes for methods with the same name
// Phase 4: Add overload attributes for overloaded methods. Only interfaces (authored and
// synthesized) carry '[Overload]' attributes, since a runtime class exposes its members
// through interfaces (where the Windows Runtime ABI method names live). Emitting them on a
// class as well would be redundant and could conflict with the names on its interfaces.
foreach ((string _, TypeDeclaration declaration) in typeDeclarations)
{
if (declaration.OutputType is null)
if (declaration.OutputType is not { IsInterface: true })
{
continue;
}
Expand Down Expand Up @@ -426,31 +429,118 @@ private bool IsProjectionEquivalent(string dotNetTypeName, string winrtTypeName)
/// </summary>
/// <remarks>
/// Windows Runtime requires that overloaded methods have unique names. This method finds method groups
/// with the same name and assigns <c>[Overload("MethodName2")]</c>, <c>[Overload("MethodName3")]</c>,
/// etc. to the second, third, and subsequent overloads.
/// with the same name and assigns a unique name to every overload except the default one (the method
/// marked with <c>[DefaultOverload]</c>, or the first in metadata order when none is marked), which keeps
/// the original name. When the author has applied <c>[Overload("...")]</c> on a method, that name is honored
/// as-is; otherwise a unique sequential name (<c>[Overload("MethodName2")]</c>, <c>[Overload("MethodName3")]</c>,
/// etc.) is generated, skipping any name already used by another member or a previously assigned overload.
/// </remarks>
/// <param name="type">The type to add overload attributes to.</param>
private void AddOverloadAttributesForType(TypeDefinition type)
{
// Group methods by name to find overloaded methods
IEnumerable<IGrouping<string, MethodDefinition>> methodGroups = type.Methods
.Where(m => !m.IsConstructor && !m.IsSpecialName)
.GroupBy(m => m.Name?.Value ?? "")
.Where(g => g.Count() > 1);
List<MethodDefinition> methods = [.. type.Methods.Where(m => !m.IsConstructor && !m.IsSpecialName)];

// Collect the names already in use within the type, so auto-generated overload names can avoid
// collisions: every member name (methods including accessors, properties and events) and any
// author-specified overload name. Auto-generated names are added to the set as they are produced,
// so they cannot collide with each other across groups (e.g. 'M1' + '2' and 'M' + '12').
HashSet<string> reservedNames = new(StringComparer.Ordinal);

foreach (MethodDefinition method in type.Methods)
{
_ = reservedNames.Add(method.Name?.Value ?? "");

if (_userSpecifiedOverloadNames.TryGetValue(method, out string? userOverloadName))
{
_ = reservedNames.Add(userOverloadName);
}
}

foreach (PropertyDefinition property in type.Properties)
{
_ = reservedNames.Add(property.Name?.Value ?? "");
}

foreach (EventDefinition @event in type.Events)
{
_ = reservedNames.Add(@event.Name?.Value ?? "");
}

foreach (IGrouping<string, MethodDefinition> group in methodGroups)
// Group methods by name to find overloaded methods
foreach (IGrouping<string, MethodDefinition> group in methods.GroupBy(m => m.Name?.Value ?? "").Where(g => g.Count() > 1))
{
int overloadIndex = 1;
// The default overload keeps the original (non-overloaded) name: the one marked with
// '[DefaultOverload]', or the first in metadata order when none is marked. Every other
// overload needs a unique name (author-specified when present, otherwise auto-generated).
MethodDefinition defaultMethod = group.FirstOrDefault(HasDefaultOverloadAttribute) ?? group.First();

int lastSuffix = 1;

foreach (MethodDefinition method in group.Skip(1))
foreach (MethodDefinition method in group)
{
overloadIndex++;
string overloadName = $"{group.Key}{overloadIndex}";
if (method == defaultMethod)
{
continue;
}

// Honor an author-applied '[Overload("...")]' name when present (see 'RecordUserSpecifiedOverloadName')
if (_userSpecifiedOverloadNames.TryGetValue(method, out string? overloadName))
{
AddOverloadAttribute(method, overloadName);

continue;
}

// Otherwise auto-generate the next sequential name that is not already in use (and reserve it)
do
{
overloadName = $"{group.Key}{++lastSuffix}";
}
while (!reservedNames.Add(overloadName));

AddOverloadAttribute(method, overloadName);
}
}
}

/// <summary>
/// Checks whether a method is marked with <c>[Windows.Foundation.Metadata.DefaultOverload]</c>.
/// </summary>
/// <param name="method">The method to check.</param>
/// <returns><see langword="true"/> if the method has the attribute; otherwise, <see langword="false"/>.</returns>
private static bool HasDefaultOverloadAttribute(MethodDefinition method)
{
return method.FindCustomAttributes("Windows.Foundation.Metadata", "DefaultOverloadAttribute").Any();
}

/// <summary>
/// Records the overload name explicitly specified by the author via
/// <c>[Windows.Foundation.Metadata.Overload("...")]</c> on an input method, so it can be honored
/// by <see cref="AddOverloadAttributesForType"/> during finalization.
/// </summary>
/// <remarks>
/// The author-applied <c>[Overload]</c> attribute is intentionally not copied verbatim to the output method
/// (see <c>ShouldCopyAttribute</c>); it is re-emitted by <see cref="AddOverloadAttribute"/> as the single
/// source of truth, so the overload name is applied only to genuinely overloaded methods and always
/// references the Windows Runtime contract assembly.
/// </remarks>
/// <param name="inputMethod">The input <see cref="MethodDefinition"/> to read the attribute from.</param>
/// <param name="outputMethod">The output <see cref="MethodDefinition"/> to associate the name with.</param>
private void RecordUserSpecifiedOverloadName(MethodDefinition inputMethod, MethodDefinition outputMethod)
{
if (inputMethod.FindCustomAttributes("Windows.Foundation.Metadata", "OverloadAttribute").FirstOrDefault() is not CustomAttribute attribute)
{
return;
}

// The single fixed argument is the overload name. AsmResolver stores attribute string arguments
// as 'Utf8String' (not 'System.String'), so it is matched as a non-null element and converted.
if (attribute.Signature is { FixedArguments: [{ Element: { } overloadName }] })
{
_userSpecifiedOverloadNames[outputMethod] = overloadName.ToString()!;
}
}

/// <summary>
/// Adds an <c>[Overload]</c> attribute to a method.
/// </summary>
Expand Down
3 changes: 3 additions & 0 deletions src/WinRT.WinMD.Generator/Writers/WinMDWriter.Members.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ private void AddMethodToInterface(TypeDefinition outputType, MethodDefinition in

// Copy custom attributes from the input method
CopyCustomAttributes(inputMethod, outputMethod);

// Record any author-specified '[Overload]' name so finalization can honor it
RecordUserSpecifiedOverloadName(inputMethod, outputMethod);
}

/// <summary>
Expand Down
11 changes: 11 additions & 0 deletions src/WinRT.WinMD.Generator/Writers/WinMDWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,17 @@ internal sealed partial class WinMDWriter
/// </summary>
private readonly Dictionary<string, TypeReference> _typeReferenceCache = new(StringComparer.Ordinal);

/// <summary>
/// Maps an output method to the overload name explicitly specified by the author via
/// <c>[Windows.Foundation.Metadata.Overload("...")]</c>.
/// </summary>
/// <remarks>
/// Populated while methods are emitted (see <see cref="RecordUserSpecifiedOverloadName"/>) and consumed
/// during finalization by <see cref="AddOverloadAttributesForType"/> to honor author-specified overload
/// names instead of auto-generating sequential ones.
/// </remarks>
private readonly Dictionary<MethodDefinition, string> _userSpecifiedOverloadNames = new();

/// <summary>
/// Creates a new <see cref="WinMDWriter"/> instance.
/// </summary>
Expand Down