From b284d5508fbefc493acb00b49d2440cf00ffef95 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 18 Jun 2026 11:42:17 -0700 Subject: [PATCH 1/3] Honor author-specified [Overload] names in the WinMD generator Port custom overload name support from PR #2275 (CsWinRT 2.x) to the CsWinRT 3.0 WinMD generator (cswinrtwinmdgen). When an authored method carries [Windows.Foundation.Metadata.Overload("Name")], emit that name in the generated .winmd instead of an auto-generated sequential one. windows-rs consumers require explicit overload names because Rust has no method overloading. - Record author-specified overload names while emitting interface methods and honor them in AddOverloadAttributesForType; stop copying the [Overload] attribute verbatim so it is never duplicated. - Select the default overload (which keeps the original name) from the [DefaultOverload] attribute rather than metadata order, so an author-specified name is not dropped when the default is not declared first. - Auto-generated names skip any name already used by another method or an author-specified overload, avoiding collisions. - Emit [Overload] only on interfaces (authored and synthesized), matching the Windows Runtime metadata convention; runtime classes expose their ABI through interfaces (consistent with CsWinRT 2.x). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Writers/WinMDWriter.Attributes.cs | 7 +- .../Writers/WinMDWriter.Finalization.cs | 118 +++++++++++++++--- .../Writers/WinMDWriter.Members.cs | 3 + .../Writers/WinMDWriter.cs | 11 ++ 4 files changed, 123 insertions(+), 16 deletions(-) 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. /// From 2e204daaf6922d1e95d305f2124afc114304bccb Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 18 Jun 2026 11:42:27 -0700 Subject: [PATCH 2/3] Port custom overload name tests from PR #2275 Add the ICustomOverloadNames interface and CustomOverloadNamesClass authoring types, and the CustomOverloadNames consumption test. The test validates that author-specified [Overload] names (LookupByIndex, LookupByFlag, TransformNumber) appear in the generated projection's ABI vtables, on both an authored interface and a class's synthesized interface. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AuthoringConsumptionTest.exe.manifest | 4 ++ src/Tests/AuthoringConsumptionTest/test.cpp | 45 +++++++++++++++++++ src/Tests/AuthoringTest/Program.cs | 31 +++++++++++++ 3 files changed, 80 insertions(+) diff --git a/src/Tests/AuthoringConsumptionTest/AuthoringConsumptionTest.exe.manifest b/src/Tests/AuthoringConsumptionTest/AuthoringConsumptionTest.exe.manifest index a288e18c7..625959dda 100644 --- a/src/Tests/AuthoringConsumptionTest/AuthoringConsumptionTest.exe.manifest +++ b/src/Tests/AuthoringConsumptionTest/AuthoringConsumptionTest.exe.manifest @@ -118,6 +118,10 @@ 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); + } +} + 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..3f24f1069 100644 --- a/src/Tests/AuthoringTest/Program.cs +++ b/src/Tests/AuthoringTest/Program.cs @@ -1891,6 +1891,37 @@ 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; + } + public sealed class NonActivatableType { private readonly string _text; From dfd60dbe71d15c325ec6415b09a02b220c68792b Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 18 Jun 2026 11:47:21 -0700 Subject: [PATCH 3/3] Add overload-name coverage for auto-generation, collisions, and default selection Extend the overload-name tests to cover behaviors beyond the original PR: - Verify the auto-generated ABI name (Transform2) coexists with an author- specified one (TransformNumber) in the same overload group. - OverloadCollisionClass: an author-specified overload name matching the auto pattern ("M2") forces the auto-generated name of the remaining overload to skip it ("M3"). - DefaultOverloadNotFirstClass / IDefaultOverloadNotFirst: [DefaultOverload] on a non-first overload keeps the original ABI name, while the author-specified name ("GetByIndex") on the first overload is still honored. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AuthoringConsumptionTest.exe.manifest | 8 +++ src/Tests/AuthoringConsumptionTest/test.cpp | 54 +++++++++++++++++++ src/Tests/AuthoringTest/Program.cs | 30 +++++++++++ 3 files changed, 92 insertions(+) diff --git a/src/Tests/AuthoringConsumptionTest/AuthoringConsumptionTest.exe.manifest b/src/Tests/AuthoringConsumptionTest/AuthoringConsumptionTest.exe.manifest index 625959dda..83ef29153 100644 --- a/src/Tests/AuthoringConsumptionTest/AuthoringConsumptionTest.exe.manifest +++ b/src/Tests/AuthoringConsumptionTest/AuthoringConsumptionTest.exe.manifest @@ -122,6 +122,14 @@ name="AuthoringTest.CustomOverloadNamesClass" threadingModel="both" xmlns="urn:schemas-microsoft-com:winrt.v1" /> + + 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) diff --git a/src/Tests/AuthoringTest/Program.cs b/src/Tests/AuthoringTest/Program.cs index 3f24f1069..c7adaa37f 100644 --- a/src/Tests/AuthoringTest/Program.cs +++ b/src/Tests/AuthoringTest/Program.cs @@ -1922,6 +1922,36 @@ public sealed class CustomOverloadNamesClass : ICustomOverloadNames 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;