diff --git a/src/Tests/AuthoringConsumptionTest/AuthoringConsumptionTest.exe.manifest b/src/Tests/AuthoringConsumptionTest/AuthoringConsumptionTest.exe.manifest
index a288e18c7..83ef29153 100644
--- a/src/Tests/AuthoringConsumptionTest/AuthoringConsumptionTest.exe.manifest
+++ b/src/Tests/AuthoringConsumptionTest/AuthoringConsumptionTest.exe.manifest
@@ -118,6 +118,18 @@
name="AuthoringTest.CustomInterfaceGuidClass"
threadingModel="both"
xmlns="urn:schemas-microsoft-com:winrt.v1" />
+
+
+
::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;
+ auto raw = static_cast(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 classIface = obj.as();
+ auto rawClass = static_cast(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 classIface = obj.as();
+ auto raw = static_cast(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;
+ auto raw = static_cast(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");
diff --git a/src/Tests/AuthoringTest/Program.cs b/src/Tests/AuthoringTest/Program.cs
index 9eeb3111c..c7adaa37f 100644
--- a/src/Tests/AuthoringTest/Program.cs
+++ b/src/Tests/AuthoringTest/Program.cs
@@ -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;
diff --git a/src/WinRT.WinMD.Generator/Writers/WinMDWriter.Attributes.cs b/src/WinRT.WinMD.Generator/Writers/WinMDWriter.Attributes.cs
index 764cc8000..25147f287 100644
--- a/src/WinRT.WinMD.Generator/Writers/WinMDWriter.Attributes.cs
+++ b/src/WinRT.WinMD.Generator/Writers/WinMDWriter.Attributes.cs
@@ -356,7 +356,7 @@ private void CopyCustomAttributes(IHasCustomAttribute source, IHasCustomAttribut
///
///
/// Filters out attributes that are handled separately by the generator (e.g., [Guid],
- /// [Version]), compiler-generated attributes (e.g., from System.Runtime.CompilerServices),
+ /// [Version], [Overload]), compiler-generated attributes (e.g., from System.Runtime.CompilerServices),
/// non-public attribute types, and attributes with unreadable signatures.
///
/// The custom attribute to evaluate.
@@ -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;
diff --git a/src/WinRT.WinMD.Generator/Writers/WinMDWriter.Finalization.cs b/src/WinRT.WinMD.Generator/Writers/WinMDWriter.Finalization.cs
index 8a5a0b6e9..289969a51 100644
--- a/src/WinRT.WinMD.Generator/Writers/WinMDWriter.Finalization.cs
+++ b/src/WinRT.WinMD.Generator/Writers/WinMDWriter.Finalization.cs
@@ -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;
}
@@ -426,31 +429,118 @@ private bool IsProjectionEquivalent(string dotNetTypeName, string winrtTypeName)
///
///
/// Windows Runtime requires that overloaded methods have unique names. This method finds method groups
- /// with the same name and assigns [Overload("MethodName2")], [Overload("MethodName3")],
- /// 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 [DefaultOverload], or the first in metadata order when none is marked), which keeps
+ /// the original name. When the author has applied [Overload("...")] on a method, that name is honored
+ /// as-is; otherwise a unique sequential name ([Overload("MethodName2")], [Overload("MethodName3")],
+ /// etc.) is generated, skipping any name already used by another member or a previously assigned overload.
///
/// The type to add overload attributes to.
private void AddOverloadAttributesForType(TypeDefinition type)
{
- // Group methods by name to find overloaded methods
- IEnumerable> methodGroups = type.Methods
- .Where(m => !m.IsConstructor && !m.IsSpecialName)
- .GroupBy(m => m.Name?.Value ?? "")
- .Where(g => g.Count() > 1);
+ List 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 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 group in methodGroups)
+ // Group methods by name to find overloaded methods
+ foreach (IGrouping 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);
}
}
}
+ ///
+ /// Checks whether a method is marked with [Windows.Foundation.Metadata.DefaultOverload].
+ ///
+ /// The method to check.
+ /// if the method has the attribute; otherwise, .
+ private static bool HasDefaultOverloadAttribute(MethodDefinition method)
+ {
+ return method.FindCustomAttributes("Windows.Foundation.Metadata", "DefaultOverloadAttribute").Any();
+ }
+
+ ///
+ /// Records the overload name explicitly specified by the author via
+ /// [Windows.Foundation.Metadata.Overload("...")] on an input method, so it can be honored
+ /// by during finalization.
+ ///
+ ///
+ /// The author-applied [Overload] attribute is intentionally not copied verbatim to the output method
+ /// (see ShouldCopyAttribute); it is re-emitted by 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.
+ ///
+ /// The input to read the attribute from.
+ /// The output to associate the name with.
+ 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()!;
+ }
+ }
+
///
/// Adds an [Overload] attribute to a method.
///
diff --git a/src/WinRT.WinMD.Generator/Writers/WinMDWriter.Members.cs b/src/WinRT.WinMD.Generator/Writers/WinMDWriter.Members.cs
index 283f6ef41..5562f0130 100644
--- a/src/WinRT.WinMD.Generator/Writers/WinMDWriter.Members.cs
+++ b/src/WinRT.WinMD.Generator/Writers/WinMDWriter.Members.cs
@@ -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);
}
///
diff --git a/src/WinRT.WinMD.Generator/Writers/WinMDWriter.cs b/src/WinRT.WinMD.Generator/Writers/WinMDWriter.cs
index e7af3b363..aab5d89d3 100644
--- a/src/WinRT.WinMD.Generator/Writers/WinMDWriter.cs
+++ b/src/WinRT.WinMD.Generator/Writers/WinMDWriter.cs
@@ -79,6 +79,17 @@ internal sealed partial class WinMDWriter
///
private readonly Dictionary _typeReferenceCache = new(StringComparer.Ordinal);
+ ///
+ /// Maps an output method to the overload name explicitly specified by the author via
+ /// [Windows.Foundation.Metadata.Overload("...")].
+ ///
+ ///
+ /// Populated while methods are emitted (see ) and consumed
+ /// during finalization by to honor author-specified overload
+ /// names instead of auto-generating sequential ones.
+ ///
+ private readonly Dictionary _userSpecifiedOverloadNames = new();
+
///
/// Creates a new instance.
///