diff --git a/nuget/Microsoft.Windows.CsWinMD.Generator.targets b/nuget/Microsoft.Windows.CsWinMD.Generator.targets new file mode 100644 index 0000000000..5590eca63b --- /dev/null +++ b/nuget/Microsoft.Windows.CsWinMD.Generator.targets @@ -0,0 +1,100 @@ + + + + + + + $(IntermediateOutputPath) + <_CsWinRTWinMDOutputPath>$(CsWinRTWinMDOutputDirectory)$(AssemblyName).winmd + + + <_RunCsWinRTWinMDGeneratorPropertyInputsCachePath Condition="'$(_RunCsWinRTWinMDGeneratorPropertyInputsCachePath)' == ''">$(IntermediateOutputPath)$(MSBuildProjectName).cswinrtwinmdgen.cache + <_RunCsWinRTWinMDGeneratorPropertyInputsCachePath>$([MSBuild]::NormalizePath('$(MSBuildProjectDirectory)', '$(_RunCsWinRTWinMDGeneratorPropertyInputsCachePath)')) + + + + + + $(_CsWinRTToolsDirectory) + + + + + + + + <_RunCsWinRTWinMDGeneratorInputsCacheToHash Include="$(CsWinRTWinMDGenEffectiveToolsDirectory)" /> + <_RunCsWinRTWinMDGeneratorInputsCacheToHash Include="$(CsWinRTUseWindowsUIXamlProjections)" /> + <_RunCsWinRTWinMDGeneratorInputsCacheToHash Include="$(AssemblyVersion)" /> + + + + + + + + + + + + + + + + + + + + + + diff --git a/nuget/Microsoft.Windows.CsWinRT.targets b/nuget/Microsoft.Windows.CsWinRT.targets index 8ae76bd4f6..d59c77c9c5 100644 --- a/nuget/Microsoft.Windows.CsWinRT.targets +++ b/nuget/Microsoft.Windows.CsWinRT.targets @@ -586,6 +586,7 @@ $(CsWinRTInternalProjection) + diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 4d76517b1c..8d3fa0ea34 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -44,6 +44,7 @@ $(MSBuildThisFileDirectory)WinRT.Interop.Generator\bin\$(Configuration)\net10.0\$(_CsWinRTToolsArchFolder) $(MSBuildThisFileDirectory)WinRT.Impl.Generator\bin\$(Configuration)\net10.0\$(_CsWinRTToolsArchFolder) $(MSBuildThisFileDirectory)WinRT.Projection.Generator\bin\$(Configuration)\net10.0\$(_CsWinRTToolsArchFolder) + $(MSBuildThisFileDirectory)WinRT.WinMD.Generator\bin\$(Configuration)\net10.0\$(_CsWinRTToolsArchFolder) AnyCPU diff --git a/src/Tests/AuthoringTest/AuthoringTest.csproj b/src/Tests/AuthoringTest/AuthoringTest.csproj index f545de49b7..c2b6147a39 100644 --- a/src/Tests/AuthoringTest/AuthoringTest.csproj +++ b/src/Tests/AuthoringTest/AuthoringTest.csproj @@ -42,7 +42,6 @@ - diff --git a/src/Tests/AuthoringTest/Program.cs b/src/Tests/AuthoringTest/Program.cs index f5b7a80f73..b4005194bf 100644 --- a/src/Tests/AuthoringTest/Program.cs +++ b/src/Tests/AuthoringTest/Program.cs @@ -1,7 +1,3 @@ -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Controls; -using Microsoft.UI.Xaml.Data; -using Microsoft.UI.Xaml.Markup; using System; using System.Collections; using System.Collections.Generic; @@ -10,18 +6,20 @@ using System.ComponentModel.DataAnnotations; using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Runtime.InteropServices.WindowsRuntime; using System.Threading; using System.Threading.Tasks; using System.Windows.Input; +using AuthoringTest; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Data; +using Microsoft.UI.Xaml.Markup; using Windows.Foundation; using Windows.Foundation.Collections; using Windows.Foundation.Metadata; using Windows.Graphics.Effects; -using WinRT; -using WinRT.Interop; +using WindowsRuntime; +using WindowsRuntime.InteropServices; +using WindowsRuntime.Xaml; #pragma warning disable CA1416 @@ -136,7 +134,7 @@ public BasicClass ReturnParameter(BasicClass basicClass) return basicClass; } - public BasicStruct[] ReturnArray([System.Runtime.InteropServices.WindowsRuntime.ReadOnlyArray] BasicStruct[] basicStructs) + public BasicStruct[] ReturnArray(ReadOnlySpan basicStructs) { BasicStruct[] copy = new BasicStruct[basicStructs.Length]; for (int idx = 0; idx < copy.Length; idx++) @@ -146,12 +144,17 @@ public BasicStruct[] ReturnArray([System.Runtime.InteropServices.WindowsRuntime. return copy; } - public int GetSum([System.Runtime.InteropServices.WindowsRuntime.ReadOnlyArray] int[] arr) + public int GetSum(ReadOnlySpan arr) { - return arr.Sum(); + int sum = 0; + foreach (int value in arr) + { + sum += value; + } + return sum; } - public void PopulateArray([System.Runtime.InteropServices.WindowsRuntime.WriteOnlyArray] int[] arr) + public void PopulateArray(Span arr) { for (int idx = 0; idx < arr.Length; idx++) { @@ -200,7 +203,7 @@ public sealed class CustomWWW : IWwwFormUrlDecoderEntry public string Value => "CsWinRT"; } - [GeneratedBindableCustomProperty] + [GeneratedCustomPropertyProvider] public sealed partial class CustomProperty { public int Number { get; } = 4; @@ -209,7 +212,7 @@ public sealed partial class CustomProperty public CustomPropertyStructType CustomPropertyStructType => new CustomPropertyStructType(); } - [GeneratedBindableCustomProperty] + [GeneratedCustomPropertyProvider] public partial struct CustomPropertyStructType { // Public WinRT struct types must have at least one field @@ -219,14 +222,14 @@ public partial struct CustomPropertyStructType public string Value => "CsWinRTFromStructType"; } - [GeneratedBindableCustomProperty] + [GeneratedCustomPropertyProvider] internal sealed partial record CustomPropertyRecordType { public int Number { get; } = 4; public string Value => "CsWinRTFromRecordType"; } - [GeneratedBindableCustomProperty] + [GeneratedCustomPropertyProvider] internal partial record struct CustomPropertyRecordStructType { public int Number => 4; @@ -241,7 +244,7 @@ public static class CustomPropertyRecordTypeFactory public static object CreateRecordStruct() => default(CustomPropertyRecordStructType); } - + public sealed partial class CustomPropertyProviderWithExplicitImplementation : ICustomPropertyProvider { public Type Type => typeof(CustomPropertyProviderWithExplicitImplementation); @@ -489,7 +492,7 @@ public IAsyncOperationWithProgress GetDoubleAsyncOperation() public IAsyncOperation GetStructAsyncOperation() { - return System.Runtime.InteropServices.WindowsRuntime.AsyncInfo.FromResult(new BasicStruct() { X = 2, Y = 4, Value = "Test" }); + return WindowsRuntime.InteropServices.AsyncInfo.FromResult(new BasicStruct() { X = 2, Y = 4, Value = "Test" }); } public IAsyncOperation GetBoolAsyncOperation() @@ -625,7 +628,7 @@ public IList GetTypeErasedProjectedArrays() } } - [WinRTRuntimeClassName("AuthoringTest.DisposableClassImpl")] + [WindowsRuntimeClassName("AuthoringTest.DisposableClassImpl")] public sealed class DisposableClass : IDisposable { public bool IsDisposed { get; set; } @@ -741,7 +744,7 @@ IEnumerator IEnumerable.GetEnumerator() } } - [WinRTRuntimeClassName("AuthoringTest.CustomReadOnlyDictionaryImpl")] + [WindowsRuntimeClassName("AuthoringTest.CustomReadOnlyDictionaryImpl")] public sealed class CustomReadOnlyDictionary : IReadOnlyDictionary { private readonly CustomDictionary _dictionary; @@ -817,7 +820,7 @@ public void Reset() } } */ - + public sealed class CustomVector : IList { private IList _list; @@ -990,7 +993,7 @@ public void RemoveAt(int index) } } - [WinRTRuntimeClassName("AuthoringTest.StaticClassImpl")] + [WindowsRuntimeClassName("AuthoringTest.StaticClassImpl")] public static class StaticClass { public static int GetNumber() @@ -1014,7 +1017,7 @@ public static void FireDelegate(double value) DelegateEvent?.Invoke(value); } } - + public static class ButtonUtils { public static Button GetButton() @@ -1120,7 +1123,7 @@ protected override Size ArrangeOverride(Size finalSize) } } - public sealed class CustomXamlServiceProvider : IXamlServiceProvider + public sealed class CustomXamlServiceProvider : IServiceProvider { public object GetService(Type type) { @@ -1301,7 +1304,7 @@ public void RunInitializer() throw new NotImplementedException(); } } - + public sealed class SingleInterfaceClass : IDouble { private double _number; @@ -1637,7 +1640,7 @@ void IList.RemoveAt(int index) _list.RemoveAt(index); } } - + public sealed class CustomDictionary2 : IDictionary { private readonly Dictionary _dictionary = new Dictionary(); @@ -1711,7 +1714,7 @@ bool IDictionary.TryGetValue(string key, out int value) public sealed class TestCollection : CollectionBase { } - + public partial interface IPartialInterface { public string GetNumberAsString(); @@ -1842,6 +1845,7 @@ public void Dispose() } } + /* public sealed class TestMixedWinRTCOMWrapper : IGraphicsEffectSource, IPublicInterface, IInternalInterface1, SomeInternalType.IInternalInterface2 { public string HelloWorld() @@ -1863,101 +1867,13 @@ unsafe int SomeInternalType.IInternalInterface2.GetNumber(int* value) return 0; } } + */ public interface IPublicInterface { string HelloWorld(); } - // Internal, classic COM interface - [global::System.Runtime.InteropServices.Guid("C7850559-8FF2-4E54-A237-6ED813F20CDC")] - [WindowsRuntimeType] - [WindowsRuntimeHelperType(typeof(IInternalInterface1))] - internal unsafe interface IInternalInterface1 - { - int GetNumber(int* value); - - [global::System.Runtime.InteropServices.Guid("C7850559-8FF2-4E54-A237-6ED813F20CDC")] - public struct Vftbl - { - public static readonly IntPtr AbiToProjectionVftablePtr = InitVtbl(); - - private static IntPtr InitVtbl() - { - Vftbl* lpVtbl = (Vftbl*)ComWrappersSupport.AllocateVtableMemory(typeof(Vftbl), sizeof(Vftbl)); - - lpVtbl->IUnknownVftbl = IUnknownVftbl.AbiToProjectionVftbl; - lpVtbl->GetNumber = &GetNumberFromAbi; - - return (IntPtr)lpVtbl; - } - - private IUnknownVftbl IUnknownVftbl; - private delegate* unmanaged[Stdcall] GetNumber; - - [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })] - private static int GetNumberFromAbi(void* thisPtr, int* value) - { - try - { - return ComWrappersSupport.FindObject((IntPtr)thisPtr).GetNumber(value); - } - catch (Exception e) - { - ExceptionHelpers.SetErrorInfo(e); - - return Marshal.GetHRForException(e); - } - } - } - } - - internal struct SomeInternalType - { - // Nested, classic COM interface - [global::System.Runtime.InteropServices.Guid("8A08E18A-8D20-4E7C-9242-857BFE1E3159")] - [WindowsRuntimeType] - [WindowsRuntimeHelperType(typeof(IInternalInterface2))] - public unsafe interface IInternalInterface2 - { - int GetNumber(int* value); - - [global::System.Runtime.InteropServices.Guid("8A08E18A-8D20-4E7C-9242-857BFE1E3159")] - public struct Vftbl - { - public static readonly IntPtr AbiToProjectionVftablePtr = InitVtbl(); - - private static IntPtr InitVtbl() - { - Vftbl* lpVtbl = (Vftbl*)ComWrappersSupport.AllocateVtableMemory(typeof(Vftbl), sizeof(Vftbl)); - - lpVtbl->IUnknownVftbl = IUnknownVftbl.AbiToProjectionVftbl; - lpVtbl->GetNumber = &GetNumberFromAbi; - - return (IntPtr)lpVtbl; - } - - private IUnknownVftbl IUnknownVftbl; - private delegate* unmanaged[Stdcall] GetNumber; - - [UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })] - private static int GetNumberFromAbi(void* thisPtr, int* value) - { - try - { - return ComWrappersSupport.FindObject((IntPtr)thisPtr).GetNumber(value); - } - catch (Exception e) - { - ExceptionHelpers.SetErrorInfo(e); - - return Marshal.GetHRForException(e); - } - } - } - } - } - [System.Runtime.InteropServices.Guid("26D8EE57-8B1B-46F4-A4F9-8C6DEEEAF53A")] public interface ICustomInterfaceGuid { @@ -1985,7 +1901,7 @@ public string GetText() } } - [WinRTRuntimeClassName("AuthoringTest.NonActivatableFactoryImpl")] + [WindowsRuntimeClassName("AuthoringTest.NonActivatableFactoryImpl")] public static class NonActivatableFactory { public static NonActivatableType Create() @@ -2015,46 +1931,431 @@ public string GetText() } } -namespace ABI.AuthoringTest +namespace AnotherNamespace { - internal static class IInternalInterface1Methods + internal partial class PartialClass3 { - public static Guid IID => typeof(global::AuthoringTest.IInternalInterface1).GUID; + public void InternalFunction() + { + } + } - public static IntPtr AbiToProjectionVftablePtr => global::AuthoringTest.IInternalInterface1.Vftbl.AbiToProjectionVftablePtr; + partial class PartialClass3 + { + public void InternalFunction2() + { + } } - internal struct SomeInternalType + internal class InternalClass { - internal static class IInternalInterface2Methods + public void InternalFunction() { - public static Guid IID => typeof(global::AuthoringTest.SomeInternalType.IInternalInterface2).GUID; + } + } - public static IntPtr AbiToProjectionVftablePtr => global::AuthoringTest.SomeInternalType.IInternalInterface2.Vftbl.AbiToProjectionVftablePtr; + // Out parameters on interface methods + public interface IOutParams + { + void GetData(out string result); + void GetStruct(out BasicStruct result); + bool TryParse(string input, out int value); + } + + // Nullable/IReference parameters + public sealed class NullableParamClass + { + public int? NullableIntProp { get; set; } + public double? NullableDoubleProp { get; set; } + public bool? NullableBoolProp { get; set; } + + public int GetValueOrDefault(int? value, int defaultValue) + { + return value ?? defaultValue; + } + + public int? TryGetValue(string key) + { + return null; } } -} -namespace AnotherNamespace -{ - internal partial class PartialClass3 + // Mapped type parameters (DateTimeOffset, TimeSpan, Uri, Exception) + public sealed class MappedTypeParamClass { - public void InternalFunction() + public DateTimeOffset GetTimestamp() + { + return DateTimeOffset.UtcNow; + } + + public void SetTimestamp(DateTimeOffset timestamp) { } + + public TimeSpan GetDuration() + { + return TimeSpan.FromSeconds(1); + } + + public void SetDuration(TimeSpan duration) { } + + public Uri GetUri() { + return new Uri("https://example.com"); + } + + public void SetUri(Uri uri) { } + + public string FormatTimestamp(DateTimeOffset timestamp, TimeSpan offset) + { + return (timestamp + offset).ToString(); } } - partial class PartialClass3 + // Mixed Span and regular array parameters + public sealed class MixedArrayClass { - public void InternalFunction2() + public void CopyToSpan(ReadOnlySpan source, Span destination) + { + source.CopyTo(destination); + } + + public int[] TransformArray(ReadOnlySpan input) + { + return input.ToArray(); + } + + public void FillWithIndex(Span buffer) + { + for (int i = 0; i < buffer.Length; i++) + { + buffer[i] = i; + } + } + + public void ProcessAndOutput(ReadOnlySpan input, out BasicStruct[] output) { + output = input.ToArray(); } } - internal class InternalClass + // Classes implementing IDisposable + custom interface + public interface ICustomResource { - public void InternalFunction() + string Name { get; } + void Reset(); + } + + public sealed class DisposableResource : IDisposable, ICustomResource + { + public string Name => "Resource"; + public void Reset() { } + public void Dispose() { } + } + + // Multiple constructors (3+) + public sealed class MultiConstructorClass + { + private readonly string _name; + private readonly int _value; + private readonly BasicStruct _data; + + public MultiConstructorClass() + { + _name = ""; + _value = 0; + _data = default; + } + + public MultiConstructorClass(string name) { + _name = name; + _value = 0; + _data = default; + } + + public MultiConstructorClass(string name, int value) + { + _name = name; + _value = value; + _data = default; + } + + public MultiConstructorClass(string name, int value, BasicStruct data) + { + _name = name; + _value = value; + _data = data; + } + + public string Name => _name; + public int Value => _value; + public BasicStruct Data => _data; + } + + // Static properties returning complex types + public sealed class StaticComplexProps + { + public static string DefaultName => "Default"; + public static BasicStruct DefaultStruct => new() { X = 1, Y = 2 }; + public static int MaxCount { get; set; } = 100; + } + + // Multiple overloaded methods with DefaultOverload + public sealed class OverloadedMethodClass + { + [Windows.Foundation.Metadata.DefaultOverload()] + public string Format(int value) + { + return value.ToString(); + } + + public string Format(double value) + { + return value.ToString("F2"); + } + + public string Format(string value) + { + return value; + } + + [Windows.Foundation.Metadata.DefaultOverload()] + public static int Parse(string text) + { + return int.Parse(text); + } + + public static int Parse(string text, int radix) + { + return Convert.ToInt32(text, radix); + } + } + + // Nested structs (2 levels) + public struct InnerStruct + { + public int A; + public int B; + } + + public struct OuterStruct + { + public InnerStruct Inner; + public int C; + } + + // Delegates with 3+ parameters and struct params + public delegate void MultiParamDelegate(int a, string b, double c); + public delegate bool StructParamDelegate(BasicStruct data, int index); + public delegate BasicStruct StructReturnDelegate(string name); + + // Flags enum edge cases + [Flags] + public enum DetailedFlags : uint + { + None = 0, + Read = 1, + Write = 2, + Execute = 4, + ReadWrite = Read | Write, + All = Read | Write | Execute + } + + // Enum without Flags (signed) + public enum Priority + { + Low = -1, + Normal = 0, + High = 1, + Critical = 2 + } + + // IAsyncAction (no return value) + public sealed class AsyncMethodClass + { + public Windows.Foundation.IAsyncAction DoWorkAsync() + { + return Task.CompletedTask.AsAsyncAction(); + } + + public Windows.Foundation.IAsyncOperation ComputeAsync() + { + return Task.FromResult(42).AsAsyncOperation(); + } + } + + // Version attribute on methods/properties + public interface IVersionedInterface + { + [Windows.Foundation.Metadata.Version(1u)] + void OriginalMethod(); + + [Windows.Foundation.Metadata.Version(2u)] + void NewerMethod(); + + [Windows.Foundation.Metadata.Version(1u)] + string Name { get; } + + [Windows.Foundation.Metadata.Version(2u)] + int Count { get; } + } + + // Deprecated members + public sealed class DeprecatedMembersClass + { + [Windows.Foundation.Metadata.Deprecated("Use NewMethod instead", Windows.Foundation.Metadata.DeprecationType.Deprecate, 1u)] + public void OldMethod() { } + + public void NewMethod() { } + + [Windows.Foundation.Metadata.Deprecated("Use NewProp instead", Windows.Foundation.Metadata.DeprecationType.Deprecate, 1u)] + public string OldProp => ""; + + public string NewProp => ""; + } + + // Class implementing INotifyPropertyChanged + custom interface + public sealed class NotifyWithCustomInterface : System.ComponentModel.INotifyPropertyChanged, ICustomResource + { + public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged; + public string Name { get; set; } = ""; + + public void Reset() + { + Name = ""; + PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(nameof(Name))); } } -} \ No newline at end of file + + // Class with both factory and static members + public sealed class FactoryAndStaticClass + { + private readonly string _id; + + public FactoryAndStaticClass(string id) + { + _id = id; + } + + public FactoryAndStaticClass(string id, int version) + { + _id = $"{id}_v{version}"; + } + + public string Id => _id; + + public static string DefaultId => "default"; + public static FactoryAndStaticClass CreateDefault() + { + return new FactoryAndStaticClass(DefaultId); + } + } + + // Interface with properties, methods, and events combined + public interface IFullFeaturedInterface + { + string Name { get; set; } + int Count { get; } + + void DoWork(); + string GetData(int index); + + event EventHandler DataChanged; + } + + public sealed class FullFeaturedClass : IFullFeaturedInterface + { + public string Name { get; set; } = ""; + public int Count => 0; + + public void DoWork() { } + public string GetData(int index) => ""; + + public event EventHandler DataChanged; + + public void RaiseDataChanged() + { + DataChanged?.Invoke(this, "changed"); + } + } + + // Contract versioning + [Windows.Foundation.Metadata.ApiContract] + public enum AnotherNamespaceContract { } + + [Windows.Foundation.Metadata.ContractVersion(typeof(AnotherNamespaceContract), 1u)] + public sealed class ContractVersionedClass + { + public string Name { get; set; } = ""; + } + + [Windows.Foundation.Metadata.ContractVersion(typeof(AnotherNamespaceContract), 2u)] + public sealed class ContractVersionedClassV2 + { + public string Name { get; set; } = ""; + public int Count { get; set; } + } + + // Class evolving across contract versions with versioned members and interfaces + public interface IContractVersionedMembersV1 + { + string TrackName { get; } + } + + [Windows.Foundation.Metadata.ContractVersion(typeof(AnotherNamespaceContract), 2u)] + public interface IContractVersionedMembersV2 + { + int Volume { get; } + } + + [Windows.Foundation.Metadata.ContractVersion(typeof(AnotherNamespaceContract), 1u)] + public sealed class ContractVersionedMembersClass : IContractVersionedMembersV1, IContractVersionedMembersV2 + { + public string TrackName { get; set; } = ""; + + [Windows.Foundation.Metadata.ContractVersion(typeof(AnotherNamespaceContract), 2u)] + public int Volume { get; set; } + + [Windows.Foundation.Metadata.ContractVersion(typeof(AnotherNamespaceContract), 2u)] + public string GetNowPlaying() => $"{TrackName} (Vol={Volume})"; + + [Windows.Foundation.Metadata.ContractVersion(typeof(AnotherNamespaceContract), 2u)] + public event System.EventHandler TrackChanged; + + public void RaiseTrackChanged() + { + TrackChanged?.Invoke(this, TrackName); + } + } + + // Class evolving across Version attribute versions with versioned members and interfaces + public interface IVersionedMembersV1 + { + string Message { get; } + } + + [Windows.Foundation.Metadata.Version(2u)] + public interface IVersionedMembersV2 + { + double Urgency { get; } + } + + [Windows.Foundation.Metadata.Version(1u)] + public sealed class VersionedMembersClass : IVersionedMembersV1, IVersionedMembersV2 + { + public string Message { get; set; } = ""; + + [Windows.Foundation.Metadata.Version(2u)] + public double Urgency { get; set; } + + [Windows.Foundation.Metadata.Version(2u)] + public string Format() => $"{Message}: {Urgency}"; + + [Windows.Foundation.Metadata.Version(2u)] + public event System.EventHandler UrgencyChanged; + + public void RaiseUrgencyChanged() + { + UrgencyChanged?.Invoke(this, Urgency); + } + } +} diff --git a/src/WinRT.Generator.Tasks/RunCsWinRTWinMDGenerator.cs b/src/WinRT.Generator.Tasks/RunCsWinRTWinMDGenerator.cs new file mode 100644 index 0000000000..b76fc7f1c2 --- /dev/null +++ b/src/WinRT.Generator.Tasks/RunCsWinRTWinMDGenerator.cs @@ -0,0 +1,159 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +namespace Microsoft.NET.Build.Tasks; + +/// +/// The custom MSBuild task that invokes the 'cswinrtwinmdgen' tool. +/// +public sealed class RunCsWinRTWinMDGenerator : ToolTask +{ + /// + /// Gets or sets the path to the compiled input assembly (.dll) to analyze. + /// + [Required] + public string? InputAssemblyPath { get; set; } + + /// + /// Gets or sets the paths to reference assembly files for type resolution. + /// + [Required] + public ITaskItem[]? ReferenceAssemblyPaths { get; set; } + + /// + /// Gets or sets the output .winmd file path. + /// + [Required] + public string? OutputWinmdPath { get; set; } + + /// + /// Gets or sets the assembly version to use for the generated WinMD. + /// + [Required] + public string? AssemblyVersion { get; set; } + + /// + /// Gets or sets whether to use Windows.UI.Xaml projections. + /// + public bool UseWindowsUIXamlProjections { get; set; } = false; + + /// + /// Gets or sets the tools directory where the 'cswinrtwinmdgen' tool is located. + /// + [Required] + public string? CsWinRTToolsDirectory { get; set; } + + /// + /// Gets or sets the architecture of 'cswinrtwinmdgen' to use. + /// + public string? CsWinRTToolsArchitecture { get; set; } + + /// + protected override string ToolName => "cswinrtwinmdgen.exe"; + + /// +#if NET10_0_OR_GREATER + [MemberNotNullWhen(true, nameof(InputAssemblyPath))] + [MemberNotNullWhen(true, nameof(ReferenceAssemblyPaths))] + [MemberNotNullWhen(true, nameof(OutputWinmdPath))] + [MemberNotNullWhen(true, nameof(AssemblyVersion))] + [MemberNotNullWhen(true, nameof(CsWinRTToolsDirectory))] +#endif + protected override bool ValidateParameters() + { + if (!base.ValidateParameters()) + { + return false; + } + + if (string.IsNullOrEmpty(InputAssemblyPath) || !File.Exists(InputAssemblyPath)) + { + Log.LogWarning("Input assembly path '{0}' is invalid or does not exist.", InputAssemblyPath); + return false; + } + + if (ReferenceAssemblyPaths is not { Length: > 0 }) + { + Log.LogWarning("Invalid 'ReferenceAssemblyPaths' input(s)."); + return false; + } + + if (string.IsNullOrEmpty(OutputWinmdPath)) + { + Log.LogWarning("Invalid 'OutputWinmdPath' input."); + return false; + } + + if (string.IsNullOrEmpty(AssemblyVersion)) + { + Log.LogWarning("Invalid 'AssemblyVersion' input."); + return false; + } + + if (CsWinRTToolsDirectory is null || !Directory.Exists(CsWinRTToolsDirectory)) + { + Log.LogWarning("Tools directory '{0}' is invalid or does not exist.", CsWinRTToolsDirectory); + return false; + } + + return true; + } + + /// + [SuppressMessage("Style", "IDE0072", Justification = "We always use 'x64' as a fallback for all other CPU architectures.")] + protected override string GenerateFullPathToTool() + { + string? effectiveArchitecture = CsWinRTToolsArchitecture; + + // Special case for when 'AnyCPU' is specified (mostly for testing scenarios). + if (effectiveArchitecture?.Equals("AnyCPU", System.StringComparison.OrdinalIgnoreCase) is true) + { + return Path.Combine(CsWinRTToolsDirectory!, ToolName); + } + + // If the architecture is not specified, determine it based on the current process architecture + effectiveArchitecture ??= RuntimeInformation.ProcessArchitecture switch + { + Architecture.X64 => "x64", + Architecture.Arm64 => "arm64", + _ => "x64" + }; + + string architectureDirectory = $"win-{effectiveArchitecture}"; + + return Path.Combine(CsWinRTToolsDirectory!, architectureDirectory, ToolName); + } + + /// + protected override string GenerateResponseFileCommands() + { + StringBuilder args = new(); + + AppendResponseFileCommand(args, "--input-assembly-path", InputAssemblyPath!); + + string referenceAssemblyPathsArg = string.Join(",", ReferenceAssemblyPaths!.Select(static path => path.ItemSpec)); + AppendResponseFileCommand(args, "--reference-assembly-paths", referenceAssemblyPathsArg); + + AppendResponseFileCommand(args, "--output-winmd-path", OutputWinmdPath!); + AppendResponseFileCommand(args, "--assembly-version", AssemblyVersion!); + AppendResponseFileCommand(args, "--use-windows-ui-xaml-projections", UseWindowsUIXamlProjections.ToString()); + + return args.ToString(); + } + + /// + /// Appends a command line argument to the response file arguments, with the right format. + /// + private static void AppendResponseFileCommand(StringBuilder args, string commandName, string commandValue) + { + _ = args.Append($"{commandName} ").AppendLine(commandValue); + } +} diff --git a/src/WinRT.WinMD.Generator/Attributes/CommandLineArgumentNameAttribute.cs b/src/WinRT.WinMD.Generator/Attributes/CommandLineArgumentNameAttribute.cs new file mode 100644 index 0000000000..78ff10de64 --- /dev/null +++ b/src/WinRT.WinMD.Generator/Attributes/CommandLineArgumentNameAttribute.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace WindowsRuntime.WinMDGenerator.Attributes; + +/// +/// An attribute indicating the name of a given command line argument. +/// +/// The command line argument name. +[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] +internal sealed class CommandLineArgumentNameAttribute(string name) : Attribute +{ + /// + /// Gets the command line argument name. + /// + public string Name { get; } = name; +} \ No newline at end of file diff --git a/src/WinRT.WinMD.Generator/Discovery/AssemblyAnalyzer.cs b/src/WinRT.WinMD.Generator/Discovery/AssemblyAnalyzer.cs new file mode 100644 index 0000000000..ac30259438 --- /dev/null +++ b/src/WinRT.WinMD.Generator/Discovery/AssemblyAnalyzer.cs @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Linq; +using AsmResolver.DotNet; + +namespace WindowsRuntime.WinMDGenerator.Discovery; + +/// +/// Analyzes a compiled assembly to discover its public WinRT API surface. +/// +internal sealed class AssemblyAnalyzer +{ + private readonly ModuleDefinition _inputModule; + + /// + /// Creates a new instance for the given input module. + /// + /// The compiled module to analyze. + public AssemblyAnalyzer(ModuleDefinition inputModule) + { + _inputModule = inputModule; + } + + /// + /// Gets the assembly name from the input module. + /// + public string AssemblyName => _inputModule.Assembly?.Name?.Value ?? _inputModule.Name!.Value; + + /// + /// Discovers all public types in the input assembly that should be included in the WinMD. + /// This includes public classes, interfaces, structs, enums, and delegates. + /// + /// A list of instances representing the public API surface. + public IReadOnlyList DiscoverPublicTypes() + { + List publicTypes = []; + + foreach (TypeDefinition type in _inputModule.TopLevelTypes) + { + CollectPublicTypes(type, publicTypes); + } + + return publicTypes; + } + + /// + /// Recursively collects public types, including nested public types. + /// + private static void CollectPublicTypes(TypeDefinition type, List publicTypes) + { + if (!IsPublicType(type)) + { + return; + } + + // We include classes, interfaces, structs, enums, and delegates + if (type.IsClass || type.IsInterface || type.IsValueType || type.IsEnum || type.IsDelegate) + { + publicTypes.Add(type); + } + + // Recurse into nested types + foreach (TypeDefinition nestedType in type.NestedTypes) + { + CollectPublicTypes(nestedType, publicTypes); + } + } + + /// + /// Checks whether a type is public (visible outside the assembly). + /// + private static bool IsPublicType(TypeDefinition type) + { + return type.IsPublic || type.IsNestedPublic; + } + + /// + /// Checks whether a type is a WinRT type (has the WindowsRuntimeMetadataAttribute). + /// + internal static bool IsWinRTType(TypeDefinition type) + { + return type.CustomAttributes.Any( + attr => attr.Constructor?.DeclaringType?.Name?.Value == "WindowsRuntimeMetadataAttribute"); + } + + /// + /// Gets the WinRT contract assembly name from WindowsRuntimeMetadataAttribute on a type, if present. + /// + internal static string? GetAssemblyForWinRTType(TypeDefinition type) + { + foreach (CustomAttribute attr in type.CustomAttributes) + { + if (attr.Constructor?.DeclaringType?.Name?.Value == "WindowsRuntimeMetadataAttribute" + && attr.Signature?.FixedArguments.Count > 0) + { + return attr.Signature.FixedArguments[0].Element?.ToString(); + } + } + + return null; + } + + /// + /// Gets the effective namespace of a type. For nested types, this walks up the + /// declaring type chain since nested types have no namespace of their own in metadata. + /// + internal static string? GetEffectiveNamespace(TypeDefinition type) + { + if (type.Namespace is { Value.Length: > 0 }) + { + return type.Namespace.Value; + } + + // For nested types, walk up to the declaring type to find the namespace + TypeDefinition? current = type.DeclaringType; + while (current != null) + { + if (current.Namespace is { Value.Length: > 0 }) + { + return current.Namespace.Value; + } + + current = current.DeclaringType; + } + + return null; + } + + /// + /// Gets the full qualified name of a type, including generic arity. + /// For nested types, uses the effective namespace from the declaring type chain. + /// + internal static string GetQualifiedName(TypeDefinition type) + { + string name = type.Name!.Value; + + string? ns = GetEffectiveNamespace(type); + return ns is { Length: > 0 } ? $"{ns}.{name}" : name; + } + + /// + /// Gets the full qualified name for an . + /// For nested types, uses the effective namespace from the declaring type chain. + /// + internal static string GetQualifiedName(ITypeDefOrRef type) + { + if (type is TypeDefinition td) + { + return GetQualifiedName(td); + } + + string name = type.Name!.Value; + string? ns = type.Namespace?.Value; + + return ns is { Length: > 0 } ? $"{ns}.{name}" : name; + } +} \ No newline at end of file diff --git a/src/WinRT.WinMD.Generator/Errors/UnhandledWinMDException.cs b/src/WinRT.WinMD.Generator/Errors/UnhandledWinMDException.cs new file mode 100644 index 0000000000..96e3700bd3 --- /dev/null +++ b/src/WinRT.WinMD.Generator/Errors/UnhandledWinMDException.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace WindowsRuntime.WinMDGenerator.Errors; + +/// +/// An unhandled exception for the WinMD generator. +/// +internal sealed class UnhandledWinMDException : Exception +{ + /// + /// The phase that failed. + /// + private readonly string _phase; + + /// + /// Creates a new instance with the specified parameters. + /// + /// The phase that failed. + /// The inner exception. + public UnhandledWinMDException(string phase, Exception exception) + : base(null, exception) + { + _phase = phase; + } + + /// + public override string ToString() + { + return + $"""error {WellKnownWinMDExceptions.ErrorPrefix}9999: The CsWinRT WinMD generator failed with an unhandled exception """ + + $"""('{InnerException!.GetType().Name}': '{InnerException!.Message}') during the {_phase} phase. This might be due to an invalid """ + + $"""configuration in the current project, but the generator should still correctly identify that and fail gracefully. Please open an """ + + $"""issue at https://github.com/microsoft/CsWinRT and provide a minimal repro, if possible."""; + } +} \ No newline at end of file diff --git a/src/WinRT.WinMD.Generator/Errors/WellKnownWinMDException.cs b/src/WinRT.WinMD.Generator/Errors/WellKnownWinMDException.cs new file mode 100644 index 0000000000..16ba08edcc --- /dev/null +++ b/src/WinRT.WinMD.Generator/Errors/WellKnownWinMDException.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace WindowsRuntime.WinMDGenerator.Errors; + +/// +/// A well known exception for the WinMD generator. +/// +internal sealed class WellKnownWinMDException : Exception +{ + /// + /// Creates a new instance with the specified parameters. + /// + /// The id of the exception. + /// The exception message. + /// The inner exception. + public WellKnownWinMDException(string id, string message, Exception? innerException) + : base(message, innerException) + { + Id = id; + } + + /// + /// Gets the id of the exception. + /// + public string Id { get; } + + /// + public override string ToString() + { + return InnerException is not null + ? $"""error {Id}: {Message} Inner exception: '{InnerException.GetType().Name}': '{InnerException.Message}'.""" + : $"""error {Id}: {Message}"""; + } +} \ No newline at end of file diff --git a/src/WinRT.WinMD.Generator/Errors/WellKnownWinMDExceptions.cs b/src/WinRT.WinMD.Generator/Errors/WellKnownWinMDExceptions.cs new file mode 100644 index 0000000000..4b69ade6bd --- /dev/null +++ b/src/WinRT.WinMD.Generator/Errors/WellKnownWinMDExceptions.cs @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace WindowsRuntime.WinMDGenerator.Errors; + +/// +/// Well-known exceptions for the WinMD generator. +/// +internal static class WellKnownWinMDExceptions +{ + /// + /// The prefix for all errors produced by this tool. + /// + public const string ErrorPrefix = "CSWINRTWINMDGEN"; + + /// + /// Some exception was thrown when trying to read the response file. + /// + public static Exception ResponseFileReadError(Exception exception) + { + return Exception(1, "Failed to read the response file to run 'cswinrtwinmdgen'.", exception); + } + + /// + /// The input response file is malformed. + /// + public static Exception MalformedResponseFile() + { + return Exception(2, "The response file is malformed and contains invalid content."); + } + + /// + /// Failed to parse an argument from the response file. + /// + public static Exception ResponseFileArgumentParsingError(string argumentName, Exception? exception = null) + { + return Exception(3, $"Failed to parse argument '{argumentName}' from response file.", exception); + } + + /// + /// Some exception was thrown when trying to load the input assembly. + /// + public static Exception InputAssemblyLoadError(Exception exception) + { + return Exception(4, "Failed to load the input assembly.", exception); + } + + /// + /// Failed to generate the WinMD file. + /// + public static Exception WinMDGenerationError(Exception exception) + { + return Exception(5, "Failed to generate the WinMD file.", exception); + } + + /// + /// Failed to write the WinMD file to disk. + /// + public static Exception WinMDWriteError(Exception exception) + { + return Exception(6, "Failed to write the WinMD file to disk.", exception); + } + + /// + /// Creates a new exception with the specified id and message. + /// + private static Exception Exception(int id, string message, Exception? innerException = null) + { + return new WellKnownWinMDException($"{ErrorPrefix}{id:0000}", message, innerException); + } +} \ No newline at end of file diff --git a/src/WinRT.WinMD.Generator/Extensions/RuntimeContextExtensions.cs b/src/WinRT.WinMD.Generator/Extensions/RuntimeContextExtensions.cs new file mode 100644 index 0000000000..f35906305a --- /dev/null +++ b/src/WinRT.WinMD.Generator/Extensions/RuntimeContextExtensions.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using AsmResolver.DotNet; + +#pragma warning disable IDE0046 + +namespace WindowsRuntime.WinMDGenerator; + +/// +/// Extensions for the type. +/// +internal static class RuntimeContextExtensions +{ + extension(RuntimeContext signature) + { + /// + /// Loads a .NET module into the context from the provided input file. + /// + /// The file path to the input executable to load. + /// The module. + /// Occurs when the image does not contain a valid .NET metadata directory. + public ModuleDefinition LoadModule(string filePath) + { + AssemblyDefinition assemblyDefinition = signature.LoadAssembly(filePath); + + // Every valid .NET assembly will always have exactly one module. In practice, we should + // never encounter an assembly with zero or more than one module, but we can still check + // and ensure that this is the case, just to guard against malformed .NET assemblies too. + if (assemblyDefinition.Modules is not [ModuleDefinition moduleDefinition]) + { + throw new BadImageFormatException(); + } + + return moduleDefinition; + } + } +} diff --git a/src/WinRT.WinMD.Generator/Extensions/WinMDExceptionExtensions.cs b/src/WinRT.WinMD.Generator/Extensions/WinMDExceptionExtensions.cs new file mode 100644 index 0000000000..c376c49110 --- /dev/null +++ b/src/WinRT.WinMD.Generator/Extensions/WinMDExceptionExtensions.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using WindowsRuntime.WinMDGenerator.Errors; + +namespace WindowsRuntime.WinMDGenerator; + +/// +/// Extensions for WinMD generator exceptions. +/// +internal static class WinMDExceptionExtensions +{ + extension(Exception exception) + { + /// + /// Gets a value indicating whether an exception is well known (and should therefore not be caught). + /// + public bool IsWellKnown => exception is OperationCanceledException or WellKnownWinMDException; + } +} \ No newline at end of file diff --git a/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.Discover.cs b/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.Discover.cs new file mode 100644 index 0000000000..42ef276ca9 --- /dev/null +++ b/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.Discover.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.IO; +using System.Linq; +using AsmResolver.DotNet; +using WindowsRuntime.WinMDGenerator.Discovery; +using WindowsRuntime.WinMDGenerator.Errors; + +namespace WindowsRuntime.WinMDGenerator.Generation; + +/// +internal static partial class WinMDGenerator +{ + /// + /// Loads the input assembly and discovers public types. + /// + private static WinMDGeneratorDiscoveryState Discover(WinMDGeneratorArgs args) + { + ModuleDefinition inputModule; + + try + { + string[] allReferencePaths = [args.InputAssemblyPath, .. args.ReferenceAssemblyPaths]; + + // Use the built-in PathAssemblyResolver with both explicit paths and search directories + // to handle type forwarding scenarios where a referenced assembly forwards to another + // assembly in the same directory. + string[] searchDirectories = allReferencePaths + .Select(Path.GetDirectoryName) + .Where(d => d is not null) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray()!; + + PathAssemblyResolver assemblyResolver = PathAssemblyResolver.FromSearchDirectories(searchDirectories); + + DotNetRuntimeInfo targetRuntime = new(".NETCoreApp", new Version(10, 0)); + RuntimeContext runtimeContext = new(targetRuntime, assemblyResolver); + + inputModule = runtimeContext.LoadModule(args.InputAssemblyPath); + } + catch (Exception e) + { + throw WellKnownWinMDExceptions.InputAssemblyLoadError(e); + } + + AssemblyAnalyzer analyzer = new(inputModule); + + return new WinMDGeneratorDiscoveryState + { + InputModule = inputModule, + AssemblyName = analyzer.AssemblyName, + PublicTypes = analyzer.DiscoverPublicTypes() + }; + } +} \ No newline at end of file diff --git a/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.Generate.cs b/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.Generate.cs new file mode 100644 index 0000000000..62e28876fb --- /dev/null +++ b/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.Generate.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.IO; +using AsmResolver.DotNet; +using WindowsRuntime.WinMDGenerator.Models; + +namespace WindowsRuntime.WinMDGenerator.Generation; + +/// +internal static partial class WinMDGenerator +{ + /// + /// Generates and writes the WinMD file from the discovered types. + /// + private static void Generate(WinMDGeneratorArgs args, WinMDGeneratorDiscoveryState state) + { + TypeMapper mapper = new(args.UseWindowsUIXamlProjections); + + WinmdWriter writer = new( + state.AssemblyName, + args.AssemblyVersion, + mapper, + state.InputModule); + + foreach (TypeDefinition type in state.PublicTypes) + { + writer.ProcessType(type); + } + + writer.FinalizeGeneration(); + + // Ensure output directory exists + string? outputDir = Path.GetDirectoryName(args.OutputWinmdPath); + if (outputDir != null) + { + _ = Directory.CreateDirectory(outputDir); + } + + writer.Write(args.OutputWinmdPath); + } +} \ No newline at end of file diff --git a/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.cs b/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.cs new file mode 100644 index 0000000000..4cfecec80c --- /dev/null +++ b/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.cs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Threading; +using ConsoleAppFramework; +using WindowsRuntime.WinMDGenerator.Errors; + +namespace WindowsRuntime.WinMDGenerator.Generation; + +/// +/// The implementation of the CsWinRT WinMD generator. +/// +internal static partial class WinMDGenerator +{ + /// + /// Runs the WinMD generator to produce a .winmd file from a compiled assembly. + /// + /// The path to the response file to use. + /// The token for the operation. + public static void Run([Argument] string inputFilePath, CancellationToken token) + { + WinMDGeneratorArgs args; + + // Phase 1: Parse the actual arguments from the response file + try + { + args = WinMDGeneratorArgs.ParseFromResponseFile(inputFilePath, token); + } + catch (Exception e) when (!e.IsWellKnown) + { + throw new UnhandledWinMDException("parsing", e); + } + + token.ThrowIfCancellationRequested(); + + ConsoleApp.Log($"Generating WinMD for assembly: {System.IO.Path.GetFileName(args.InputAssemblyPath)}"); + ConsoleApp.Log($"Output: {args.OutputWinmdPath}"); + + // Phase 2: Load and discover + WinMDGeneratorDiscoveryState discoveryState; + + try + { + discoveryState = Discover(args); + } + catch (Exception e) when (!e.IsWellKnown) + { + throw new UnhandledWinMDException("discovery", e); + } + + token.ThrowIfCancellationRequested(); + + ConsoleApp.Log($"Found {discoveryState.PublicTypes.Count} public types"); + + // Phase 3: Generate and write the WinMD + try + { + Generate(args, discoveryState); + } + catch (Exception e) when (!e.IsWellKnown) + { + throw new UnhandledWinMDException("generation", e); + } + + ConsoleApp.Log($"WinMD generated successfully: {args.OutputWinmdPath}"); + } +} \ No newline at end of file diff --git a/src/WinRT.WinMD.Generator/Generation/WinMDGeneratorArgs.Parsing.cs b/src/WinRT.WinMD.Generator/Generation/WinMDGeneratorArgs.Parsing.cs new file mode 100644 index 0000000000..0ac4c85176 --- /dev/null +++ b/src/WinRT.WinMD.Generator/Generation/WinMDGeneratorArgs.Parsing.cs @@ -0,0 +1,158 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using WindowsRuntime.WinMDGenerator.Attributes; +using WindowsRuntime.WinMDGenerator.Errors; + +#pragma warning disable IDE0046 + +namespace WindowsRuntime.WinMDGenerator.Generation; + +/// +internal partial class WinMDGeneratorArgs +{ + /// + /// Parses a instance from a target response file. + /// + /// The path to the response file. + /// The token for the operation. + /// The resulting instance. + public static WinMDGeneratorArgs ParseFromResponseFile(string path, System.Threading.CancellationToken token) + { + // If the path is a response file, it will start with the '@' character. + // This matches the default escaping 'ToolTask' uses for response files. + if (path is ['@', .. string escapedPath]) + { + path = escapedPath; + } + + string[] lines; + + // Read all lines in the response file (each line contains a single command line argument) + try + { + lines = File.ReadAllLines(path); + } + catch (Exception e) + { + throw WellKnownWinMDExceptions.ResponseFileReadError(e); + } + + return ParseFromResponseFile(lines, token); + } + + /// + /// Parses a instance from a target response file. + /// + /// The lines read from the response file. + /// The token for the operation. + /// The resulting instance. + private static WinMDGeneratorArgs ParseFromResponseFile(string[] lines, System.Threading.CancellationToken token) + { + Dictionary argsMap = []; + + // Build a map with all the commands and their values + foreach (string line in lines) + { + string trimmedLine = line.Trim(); + + if (string.IsNullOrEmpty(trimmedLine)) + { + continue; + } + + // Each line has the command line argument name followed by a space, and then the + // argument value. If there are no spaces on any given line, the file is malformed. + int indexOfSpace = trimmedLine.IndexOf(' '); + + if (indexOfSpace == -1) + { + throw WellKnownWinMDExceptions.MalformedResponseFile(); + } + + // Now we can parse the actual command line argument name and value + string argumentName = trimmedLine.AsSpan()[..indexOfSpace].ToString(); + string argumentValue = trimmedLine.AsSpan()[(indexOfSpace + 1)..].TrimEnd().ToString(); + + // We should never have duplicate commands + if (!argsMap.TryAdd(argumentName, argumentValue)) + { + throw WellKnownWinMDExceptions.MalformedResponseFile(); + } + } + + // Parse all commands to create the managed arguments to use + return new() + { + InputAssemblyPath = GetStringArgument(argsMap, nameof(InputAssemblyPath)), + ReferenceAssemblyPaths = GetStringArrayArgument(argsMap, nameof(ReferenceAssemblyPaths)), + OutputWinmdPath = GetStringArgument(argsMap, nameof(OutputWinmdPath)), + AssemblyVersion = GetStringArgument(argsMap, nameof(AssemblyVersion)), + UseWindowsUIXamlProjections = GetBooleanArgument(argsMap, nameof(UseWindowsUIXamlProjections)), + Token = token + }; + } + + /// + /// Gets the command line argument name for a property. + /// + /// The target property name. + /// The command line argument name for . + public static string GetCommandLineArgumentName(string propertyName) + { + try + { + return typeof(WinMDGeneratorArgs).GetProperty(propertyName)!.GetCustomAttribute()!.Name; + } + catch (Exception e) + { + throw WellKnownWinMDExceptions.ResponseFileArgumentParsingError(propertyName, e); + } + } + + /// + /// Parses a array argument. + /// + private static string[] GetStringArrayArgument(Dictionary argsMap, string propertyName) + { + if (argsMap.TryGetValue(GetCommandLineArgumentName(propertyName), out string? argumentValue)) + { + return argumentValue.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + } + + throw WellKnownWinMDExceptions.ResponseFileArgumentParsingError(propertyName); + } + + /// + /// Parses a argument. + /// + private static string GetStringArgument(Dictionary argsMap, string propertyName) + { + if (argsMap.TryGetValue(GetCommandLineArgumentName(propertyName), out string? argumentValue)) + { + return argumentValue; + } + + throw WellKnownWinMDExceptions.ResponseFileArgumentParsingError(propertyName); + } + + /// + /// Parses a argument. + /// + private static bool GetBooleanArgument(Dictionary argsMap, string propertyName) + { + if (argsMap.TryGetValue(GetCommandLineArgumentName(propertyName), out string? argumentValue)) + { + if (bool.TryParse(argumentValue, out bool parsedValue)) + { + return parsedValue; + } + } + + throw WellKnownWinMDExceptions.ResponseFileArgumentParsingError(propertyName); + } +} \ No newline at end of file diff --git a/src/WinRT.WinMD.Generator/Generation/WinMDGeneratorArgs.cs b/src/WinRT.WinMD.Generator/Generation/WinMDGeneratorArgs.cs new file mode 100644 index 0000000000..abd4f71269 --- /dev/null +++ b/src/WinRT.WinMD.Generator/Generation/WinMDGeneratorArgs.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading; +using WindowsRuntime.WinMDGenerator.Attributes; + +namespace WindowsRuntime.WinMDGenerator.Generation; + +/// +/// Input parameters for . +/// +internal sealed partial class WinMDGeneratorArgs +{ + /// Gets the path to the compiled input assembly (.dll) to analyze. + [CommandLineArgumentName("--input-assembly-path")] + public required string InputAssemblyPath { get; init; } + + /// Gets the input reference .dll paths for type resolution. + [CommandLineArgumentName("--reference-assembly-paths")] + public required string[] ReferenceAssemblyPaths { get; init; } + + /// Gets the output .winmd file path. + [CommandLineArgumentName("--output-winmd-path")] + public required string OutputWinmdPath { get; init; } + + /// Gets the assembly version to use for the generated WinMD. + [CommandLineArgumentName("--assembly-version")] + public required string AssemblyVersion { get; init; } + + /// Gets whether to use Windows.UI.Xaml projections. + [CommandLineArgumentName("--use-windows-ui-xaml-projections")] + public required bool UseWindowsUIXamlProjections { get; init; } + + /// Gets the token for the operation. + public required CancellationToken Token { get; init; } +} \ No newline at end of file diff --git a/src/WinRT.WinMD.Generator/Generation/WinMDGeneratorDiscoveryState.cs b/src/WinRT.WinMD.Generator/Generation/WinMDGeneratorDiscoveryState.cs new file mode 100644 index 0000000000..5a15fb99f9 --- /dev/null +++ b/src/WinRT.WinMD.Generator/Generation/WinMDGeneratorDiscoveryState.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using AsmResolver.DotNet; + +namespace WindowsRuntime.WinMDGenerator.Generation; + +/// +/// The state produced by the discovery phase of the WinMD generator. +/// +internal sealed class WinMDGeneratorDiscoveryState +{ + /// + /// Gets the loaded input module. + /// + public required ModuleDefinition InputModule { get; init; } + + /// + /// Gets the assembly name from the input module. + /// + public required string AssemblyName { get; init; } + + /// + /// Gets the public types discovered in the input assembly. + /// + public required IReadOnlyList PublicTypes { get; init; } +} \ No newline at end of file diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Attributes.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Attributes.cs new file mode 100644 index 0000000000..d25f3b3977 --- /dev/null +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Attributes.cs @@ -0,0 +1,450 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using WindowsRuntime.WinMDGenerator.Discovery; + +namespace WindowsRuntime.WinMDGenerator.Generation; + +internal sealed partial class WinmdWriter +{ + private void AddGuidAttribute(TypeDefinition outputType, TypeDefinition inputType) + { + // Check if the input type has a GuidAttribute + foreach (CustomAttribute attr in inputType.CustomAttributes) + { + if (attr.Constructor?.DeclaringType?.FullName == "System.Runtime.InteropServices.GuidAttribute" && + attr.Signature?.FixedArguments.Count > 0 && + attr.Signature.FixedArguments[0].Element is { } guidElement && + Guid.TryParse(guidElement.ToString(), out Guid guid)) + { + AddGuidAttribute(outputType, guid); + return; + } + } + + // Generate a GUID from the type name using SHA1 + string typeName = AssemblyAnalyzer.GetQualifiedName(inputType); + AddGuidAttributeFromName(outputType, typeName); + } + + private void AddGuidAttributeFromName(TypeDefinition outputType, string name) + { + Guid guid; + // CodeQL [SM02196] WinRT uses UUID v5 SHA1 to generate Guids for parameterized types. +#pragma warning disable CA5350 + byte[] hash = SHA1.HashData(Encoding.UTF8.GetBytes(name)); + guid = EncodeGuid(hash); +#pragma warning restore CA5350 + + AddGuidAttribute(outputType, guid); + } + + private void AddGuidAttribute(TypeDefinition outputType, Guid guid) + { + // Create a reference to the GuidAttribute constructor in Windows.Foundation.Metadata + TypeReference guidAttrType = GetOrCreateTypeReference( + "Windows.Foundation.Metadata", "GuidAttribute", "Windows.Foundation.FoundationContract"); + + byte[] guidBytes = guid.ToByteArray(); + + // The GuidAttribute constructor takes (uint, ushort, ushort, byte, byte, byte, byte, byte, byte, byte, byte) + MemberReference guidCtor = new(guidAttrType, ".ctor", + MethodSignature.CreateInstance( + _outputModule.CorLibTypeFactory.Void, + [_outputModule.CorLibTypeFactory.UInt32, + _outputModule.CorLibTypeFactory.UInt16, + _outputModule.CorLibTypeFactory.UInt16, + _outputModule.CorLibTypeFactory.Byte, + _outputModule.CorLibTypeFactory.Byte, + _outputModule.CorLibTypeFactory.Byte, + _outputModule.CorLibTypeFactory.Byte, + _outputModule.CorLibTypeFactory.Byte, + _outputModule.CorLibTypeFactory.Byte, + _outputModule.CorLibTypeFactory.Byte, + _outputModule.CorLibTypeFactory.Byte])); + + CustomAttributeSignature sig = new(); + sig.FixedArguments.Add(new CustomAttributeArgument(_outputModule.CorLibTypeFactory.UInt32, BitConverter.ToUInt32(guidBytes, 0))); + sig.FixedArguments.Add(new CustomAttributeArgument(_outputModule.CorLibTypeFactory.UInt16, BitConverter.ToUInt16(guidBytes, 4))); + sig.FixedArguments.Add(new CustomAttributeArgument(_outputModule.CorLibTypeFactory.UInt16, BitConverter.ToUInt16(guidBytes, 6))); + sig.FixedArguments.Add(new CustomAttributeArgument(_outputModule.CorLibTypeFactory.Byte, guidBytes[8])); + sig.FixedArguments.Add(new CustomAttributeArgument(_outputModule.CorLibTypeFactory.Byte, guidBytes[9])); + sig.FixedArguments.Add(new CustomAttributeArgument(_outputModule.CorLibTypeFactory.Byte, guidBytes[10])); + sig.FixedArguments.Add(new CustomAttributeArgument(_outputModule.CorLibTypeFactory.Byte, guidBytes[11])); + sig.FixedArguments.Add(new CustomAttributeArgument(_outputModule.CorLibTypeFactory.Byte, guidBytes[12])); + sig.FixedArguments.Add(new CustomAttributeArgument(_outputModule.CorLibTypeFactory.Byte, guidBytes[13])); + sig.FixedArguments.Add(new CustomAttributeArgument(_outputModule.CorLibTypeFactory.Byte, guidBytes[14])); + sig.FixedArguments.Add(new CustomAttributeArgument(_outputModule.CorLibTypeFactory.Byte, guidBytes[15])); + + outputType.CustomAttributes.Add(new CustomAttribute(guidCtor, sig)); + } + + private void AddVersionAttribute(TypeDefinition outputType, int version) + { + TypeReference versionAttrType = GetOrCreateTypeReference( + "Windows.Foundation.Metadata", "VersionAttribute", "Windows.Foundation.FoundationContract"); + + MemberReference versionCtor = new(versionAttrType, ".ctor", + MethodSignature.CreateInstance( + _outputModule.CorLibTypeFactory.Void, + [_outputModule.CorLibTypeFactory.UInt32])); + + CustomAttributeSignature sig = new(); + sig.FixedArguments.Add(new CustomAttributeArgument(_outputModule.CorLibTypeFactory.UInt32, (uint)version)); + + outputType.CustomAttributes.Add(new CustomAttribute(versionCtor, sig)); + } + + private void AddActivatableAttribute(TypeDefinition outputType, uint version, string? factoryInterface) + { + TypeReference activatableAttrType = GetOrCreateTypeReference( + "Windows.Foundation.Metadata", "ActivatableAttribute", "Windows.Foundation.FoundationContract"); + + if (factoryInterface != null) + { + // Constructor: ActivatableAttribute(Type, uint) + TypeReference systemType = GetOrCreateTypeReference("System", "Type", "mscorlib"); + MemberReference ctor = new(activatableAttrType, ".ctor", + MethodSignature.CreateInstance( + _outputModule.CorLibTypeFactory.Void, + [systemType.ToTypeSignature(false), + _outputModule.CorLibTypeFactory.UInt32])); + + CustomAttributeSignature sig = new(); + sig.FixedArguments.Add(new CustomAttributeArgument(systemType.ToTypeSignature(false), ResolveTypeNameToSignature(factoryInterface))); + sig.FixedArguments.Add(new CustomAttributeArgument(_outputModule.CorLibTypeFactory.UInt32, version)); + + outputType.CustomAttributes.Add(new CustomAttribute(ctor, sig)); + } + else + { + // Constructor: ActivatableAttribute(uint) + MemberReference ctor = new(activatableAttrType, ".ctor", + MethodSignature.CreateInstance( + _outputModule.CorLibTypeFactory.Void, + [_outputModule.CorLibTypeFactory.UInt32])); + + CustomAttributeSignature sig = new(); + sig.FixedArguments.Add(new CustomAttributeArgument(_outputModule.CorLibTypeFactory.UInt32, version)); + + outputType.CustomAttributes.Add(new CustomAttribute(ctor, sig)); + } + } + + private void AddStaticAttribute(TypeDefinition classOutputType, uint version, string staticInterfaceName) + { + TypeReference staticAttrType = GetOrCreateTypeReference( + "Windows.Foundation.Metadata", "StaticAttribute", "Windows.Foundation.FoundationContract"); + + TypeReference systemType = GetOrCreateTypeReference("System", "Type", "mscorlib"); + MemberReference ctor = new(staticAttrType, ".ctor", + MethodSignature.CreateInstance( + _outputModule.CorLibTypeFactory.Void, + [systemType.ToTypeSignature(false), + _outputModule.CorLibTypeFactory.UInt32])); + + CustomAttributeSignature sig = new(); + sig.FixedArguments.Add(new CustomAttributeArgument(systemType.ToTypeSignature(false), ResolveTypeNameToSignature(staticInterfaceName))); + sig.FixedArguments.Add(new CustomAttributeArgument(_outputModule.CorLibTypeFactory.UInt32, version)); + + classOutputType.CustomAttributes.Add(new CustomAttribute(ctor, sig)); + } + + private void AddExclusiveToAttribute(TypeDefinition interfaceType, string className) + { + TypeReference exclusiveToAttrType = GetOrCreateTypeReference( + "Windows.Foundation.Metadata", "ExclusiveToAttribute", "Windows.Foundation.FoundationContract"); + + TypeReference systemType = GetOrCreateTypeReference("System", "Type", "mscorlib"); + MemberReference ctor = new(exclusiveToAttrType, ".ctor", + MethodSignature.CreateInstance( + _outputModule.CorLibTypeFactory.Void, + [systemType.ToTypeSignature(false)])); + + CustomAttributeSignature sig = new(); + sig.FixedArguments.Add(new CustomAttributeArgument(systemType.ToTypeSignature(false), ResolveTypeNameToSignature(className))); + + interfaceType.CustomAttributes.Add(new CustomAttribute(ctor, sig)); + } + + private void AddDefaultAttribute(InterfaceImplementation interfaceImpl) + { + TypeReference defaultAttrType = GetOrCreateTypeReference( + "Windows.Foundation.Metadata", "DefaultAttribute", "Windows.Foundation.FoundationContract"); + + MemberReference ctor = new(defaultAttrType, ".ctor", + MethodSignature.CreateInstance(_outputModule.CorLibTypeFactory.Void)); + + interfaceImpl.CustomAttributes.Add(new CustomAttribute(ctor, new CustomAttributeSignature())); + } + + private static bool HasVersionAttribute(TypeDefinition type) + { + return type.CustomAttributes.Any( + attr => attr.Constructor?.DeclaringType?.Name?.Value is "VersionAttribute" or "ContractVersionAttribute"); + } + + private int GetVersion(TypeDefinition type) + { + foreach (CustomAttribute attr in type.CustomAttributes) + { + if (attr.Constructor?.DeclaringType?.Name?.Value == "VersionAttribute" && + attr.Signature?.FixedArguments.Count > 0 && + attr.Signature.FixedArguments[0].Element is uint version) + { + return (int)version; + } + } + + return Version.Parse(_version).Major; + } + + /// + /// Copies custom attributes from a source metadata element to a target metadata element, + /// filtering out attributes that are handled separately by the generator or not meaningful for WinMD. + /// + private void CopyCustomAttributes(IHasCustomAttribute source, IHasCustomAttribute target) + { + foreach (CustomAttribute attr in source.CustomAttributes) + { + if (!ShouldCopyAttribute(attr, _runtimeContext)) + { + continue; + } + + MemberReference? importedCtor = ImportAttributeConstructor(attr.Constructor); + if (importedCtor == null) + { + continue; + } + + CustomAttributeSignature clonedSig = CloneAttributeSignature(attr.Signature); + target.CustomAttributes.Add(new CustomAttribute(importedCtor, clonedSig)); + } + } + + /// + /// Determines whether a custom attribute should be copied to the output WinMD. + /// + private static bool ShouldCopyAttribute(CustomAttribute attr, RuntimeContext? runtimeContext) + { + string? attrTypeName = attr.Constructor?.DeclaringType?.FullName; + + if (attrTypeName is null) + { + return false; + } + + // Skip attributes already handled separately by the generator + if (attrTypeName is + "System.Runtime.InteropServices.GuidAttribute" or + "WindowsRuntime.Xaml.GeneratedCustomPropertyProviderAttribute" or + "Windows.Foundation.Metadata.VersionAttribute" or + "System.Reflection.DefaultMemberAttribute") + { + return false; + } + + // Skip compiler-generated attributes not meaningful for WinMD + if (attrTypeName.StartsWith("System.Runtime.CompilerServices.", StringComparison.Ordinal)) + { + return false; + } + + // Skip non-public attribute types (if resolvable) + if (runtimeContext is not null + && attr.Constructor?.DeclaringType is { } attrType + && attrType is not TypeDefinition + && attrType.Resolve(runtimeContext, out TypeDefinition? attrTypeDef) == ResolutionStatus.Success + && !attrTypeDef!.IsPublic && !attrTypeDef.IsNestedPublic) + { + return false; + } + + // Skip attributes with unreadable signatures + return attr.Signature != null; + } + + /// + /// Imports an attribute constructor reference into the output module. + /// + private MemberReference? ImportAttributeConstructor(ICustomAttributeType? ctor) + { + if (ctor?.DeclaringType == null || ctor.Signature is not MethodSignature methodSig) + { + return null; + } + + ITypeDefOrRef importedType = ImportTypeReference(ctor.DeclaringType); + + // Attribute constructor parameters must use CLR types (System.Type, System.String, etc.) + // not WinRT projected types (TypeName, HString), because the custom attribute blob + // serializer only supports primitives, System.Type, System.String, and enum types. + TypeSignature[] importedParams = [.. methodSig.ParameterTypes + .Select(ImportTypeSignatureForAttribute)]; + + MethodSignature importedSig = MethodSignature.CreateInstance( + _outputModule.CorLibTypeFactory.Void, + importedParams); + + return new MemberReference(importedType, ".ctor", importedSig); + } + + /// + /// Clones a custom attribute signature, remapping type references to the output module. + /// + private CustomAttributeSignature CloneAttributeSignature(CustomAttributeSignature? inputSig) + { + if (inputSig == null) + { + return new CustomAttributeSignature(); + } + + CustomAttributeSignature outputSig = new(); + + foreach (CustomAttributeArgument arg in inputSig.FixedArguments) + { + outputSig.FixedArguments.Add(CloneAttributeArgument(arg)); + } + + foreach (CustomAttributeNamedArgument namedArg in inputSig.NamedArguments) + { + TypeSignature mappedArgType = MapTypeSignatureToOutput(namedArg.Argument.ArgumentType); + CustomAttributeArgument clonedInnerArg = CloneAttributeArgument(namedArg.Argument); + + outputSig.NamedArguments.Add(new CustomAttributeNamedArgument( + namedArg.MemberType, + namedArg.MemberName, + mappedArgType, + clonedInnerArg)); + } + + return outputSig; + } + + /// + /// Imports a type signature for use in attribute constructor parameters. + /// Unlike , this preserves CLR types + /// (System.Type, System.String) that custom attribute blobs require. + /// + private TypeSignature ImportTypeSignatureForAttribute(TypeSignature sig) + { + if (sig is CorLibTypeSignature) + { + return sig; + } + + if (sig is TypeDefOrRefSignature typeDefOrRefSig) + { + string? fullName = typeDefOrRefSig.Type?.FullName; + + // System.Type must stay as System.Type for attribute blob encoding + if (fullName == "System.Type") + { + return GetOrCreateTypeReference("System", "Type", "mscorlib").ToTypeSignature(false); + } + + // Enum types: import the reference so the blob encoder can resolve the underlying type + TypeDefinition? resolved = SafeResolve(typeDefOrRefSig.Type); + if (resolved != null && resolved.IsEnum) + { + return ImportTypeReference(typeDefOrRefSig.Type!).ToTypeSignature(typeDefOrRefSig.IsValueType); + } + + // For other types, import directly without WinRT mapping + return ImportTypeReference(typeDefOrRefSig.Type!).ToTypeSignature(typeDefOrRefSig.IsValueType); + } + + // Fallback: use the standard mapping + return MapTypeSignatureToOutput(sig); + } + + /// + /// Clones a single custom attribute argument, remapping type references. + /// Uses attribute-safe type imports to preserve CLR types required by the blob encoder. + /// + private CustomAttributeArgument CloneAttributeArgument(CustomAttributeArgument arg) + { + TypeSignature mappedType = ImportTypeSignatureForAttribute(arg.ArgumentType); + CustomAttributeArgument clonedArg = new(mappedType); + + if (arg.IsNullArray) + { + clonedArg.IsNullArray = true; + } + else + { + foreach (object? element in arg.Elements) + { + // Type-valued elements are stored as TypeSignature and need remapping + clonedArg.Elements.Add(element is TypeSignature typeSig + ? MapTypeSignatureToOutput(typeSig) + : element); + } + } + + return clonedArg; + } + + private static bool IsPubliclyAccessible(ITypeDefOrRef type) + { + if (type is TypeDefinition typeDef) + { + return typeDef.IsPublic || typeDef.IsNestedPublic; + } + + // For type references and specs, assume accessible + return true; + } + + /// + /// Encodes a GUID from a SHA1 hash (UUID v5 format). + /// + private static Guid EncodeGuid(byte[] hash) + { + byte[] guidBytes = new byte[16]; + Array.Copy(hash, guidBytes, 16); + + if (BitConverter.IsLittleEndian) + { + // Swap bytes of int a (bytes 0-3) + (guidBytes[0], guidBytes[3]) = (guidBytes[3], guidBytes[0]); + (guidBytes[1], guidBytes[2]) = (guidBytes[2], guidBytes[1]); + + // Swap bytes of short b (bytes 4-5) + (guidBytes[4], guidBytes[5]) = (guidBytes[5], guidBytes[4]); + + // Swap bytes of short c (bytes 6-7) and encode version + byte t = guidBytes[6]; + guidBytes[6] = guidBytes[7]; + guidBytes[7] = (byte)((t & 0x0F) | 0x50); // version 5 + } + else + { + // Set version to 5 (SHA1) + guidBytes[7] = (byte)((guidBytes[7] & 0x0F) | 0x50); + } + + // Set variant to RFC 4122 + guidBytes[8] = (byte)((guidBytes[8] & 0x3F) | 0x80); + + return new Guid(guidBytes); + } + + /// + /// Resolves a fully-qualified type name to a for use in custom attribute arguments. + /// Uses the output TypeDefinition directly to avoid creating self-referencing TypeReferences. + /// + private TypeSignature ResolveTypeNameToSignature(string qualifiedTypeName) + { + return _typeDefinitionMapping.TryGetValue(qualifiedTypeName, out TypeDeclaration? declaration) && declaration.OutputType != null + ? declaration.OutputType.ToTypeSignature() + : throw new InvalidOperationException($"Type '{qualifiedTypeName}' not found in the output type definitions."); + } +} \ No newline at end of file diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.CustomMappedMembers.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.CustomMappedMembers.cs new file mode 100644 index 0000000000..5cd0013c70 --- /dev/null +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.CustomMappedMembers.cs @@ -0,0 +1,788 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using WindowsRuntime.WinMDGenerator.Discovery; +using WindowsRuntime.WinMDGenerator.Models; +using MethodAttributes = AsmResolver.PE.DotNet.Metadata.Tables.MethodAttributes; +using MethodImplAttributes = AsmResolver.PE.DotNet.Metadata.Tables.MethodImplAttributes; +using MethodSemanticsAttributes = AsmResolver.PE.DotNet.Metadata.Tables.MethodSemanticsAttributes; +using ParameterAttributes = AsmResolver.PE.DotNet.Metadata.Tables.ParameterAttributes; + +namespace WindowsRuntime.WinMDGenerator.Generation; + +internal sealed partial class WinmdWriter +{ + /// + /// Processes custom mapped interfaces for a class type. This maps .NET collection interfaces, + /// IDisposable, INotifyPropertyChanged, etc. to their WinRT equivalents and adds the + /// required explicit implementation methods and MethodImpl records. + /// + private void ProcessCustomMappedInterfaces(TypeDefinition inputType, TypeDefinition outputType) + { + // Gather all interfaces from the type and its base types (matching old generator's GetInterfaces) + List allInterfaces = GatherAllInterfaces(inputType); + + // Collect all mapped interfaces and determine if they are publicly or explicitly implemented + List<(InterfaceImplementation impl, string interfaceName, MappedType mapping, bool isPublic)> mappedInterfaces = []; + + foreach (InterfaceImplementation impl in allInterfaces) + { + if (impl.Interface == null) + { + continue; + } + + string interfaceName = GetInterfaceQualifiedName(impl.Interface); + + if (!_mapper.HasMappingForType(interfaceName)) + { + continue; + } + + MappedType mapping = _mapper.GetMappedType(interfaceName); + + // Determine if the interface is publicly implemented. + // Check if the class has public methods that match the .NET interface members. + // For mapped interfaces, the .NET method names differ from WinRT names + // (e.g., Add vs Append), so we check the .NET interface's members. + bool isPublic = IsInterfacePubliclyImplemented(classType: inputType, impl, _runtimeContext); + + mappedInterfaces.Add((impl, interfaceName, mapping, isPublic)); + } + + // If generic IEnumerable (IIterable) is present, skip non-generic IEnumerable (IBindableIterable) + bool hasGenericEnumerable = allInterfaces.Any(i => + i.Interface != null && GetInterfaceQualifiedName(i.Interface) == "System.Collections.Generic.IEnumerable`1"); + + foreach ((InterfaceImplementation impl, string interfaceName, MappedType mapping, bool isPublic) in mappedInterfaces) + { + (string mappedNs, string mappedName, string mappedAssembly, _, _) = mapping.GetMapping(); + + // Skip non-generic IEnumerable when generic IEnumerable is also implemented + if (hasGenericEnumerable && interfaceName == "System.Collections.IEnumerable") + { + continue; + } + + // Add the mapped interface as an implementation on the output type + TypeReference mappedInterfaceRef = GetOrCreateTypeReference(mappedNs, mappedName, mappedAssembly); + + // Check if the output type already implements this mapped interface + // (e.g., IObservableVector already brings IVector) + string mappedFullName = string.IsNullOrEmpty(mappedNs) ? mappedName : $"{mappedNs}.{mappedName}"; + bool alreadyImplemented = outputType.Interfaces.Any(i => + { + string? existingName = i.Interface is TypeSpecification existingTs && existingTs.Signature is GenericInstanceTypeSignature existingGits + ? existingGits.GenericType.FullName + : i.Interface?.FullName; + return existingName == mappedFullName; + }); + + if (alreadyImplemented) + { + continue; + } + + ITypeDefOrRef mappedInterfaceTypeRef; + + // For generic interfaces, handle type arguments (mapping KeyValuePair -> IKeyValuePair, etc.) + if (impl.Interface is TypeSpecification typeSpec && typeSpec.Signature is GenericInstanceTypeSignature genericInst) + { + TypeSignature[] mappedArgs = [.. genericInst.TypeArguments + .Select(MapCustomMappedTypeArgument)]; + TypeSpecification mappedSpec = new(new GenericInstanceTypeSignature(mappedInterfaceRef, false, mappedArgs)); + outputType.Interfaces.Add(new InterfaceImplementation(mappedSpec)); + mappedInterfaceTypeRef = mappedSpec; + } + else + { + outputType.Interfaces.Add(new InterfaceImplementation(mappedInterfaceRef)); + mappedInterfaceTypeRef = mappedInterfaceRef; + } + + // Add explicit implementation methods for the mapped interface + AddCustomMappedTypeMembers(outputType, mappedName, mappedInterfaceTypeRef, isPublic); + } + } + + /// + /// Maps a type argument for custom mapped interfaces, transforming mapped types + /// like KeyValuePair to IKeyValuePair. + /// + private TypeSignature MapCustomMappedTypeArgument(TypeSignature arg) + { + TypeSignature mapped = MapTypeSignatureToOutput(arg); + + // Check if the mapped type itself has a WinRT mapping (e.g. KeyValuePair -> IKeyValuePair) + if (mapped is TypeDefOrRefSignature tdrs) + { + string typeName = AssemblyAnalyzer.GetQualifiedName(tdrs.Type); + if (_mapper.HasMappingForType(typeName)) + { + MappedType innerMapping = _mapper.GetMappedType(typeName); + (string ns, string name, string asm, _, _) = innerMapping.GetMapping(); + TypeReference innerRef = GetOrCreateTypeReference(ns, name, asm); + return innerRef.ToTypeSignature(tdrs.IsValueType); + } + } + + // For generic instances, recursively map type arguments + if (mapped is GenericInstanceTypeSignature gits) + { + string typeName = AssemblyAnalyzer.GetQualifiedName(gits.GenericType); + if (_mapper.HasMappingForType(typeName)) + { + MappedType innerMapping = _mapper.GetMappedType(typeName); + (string ns, string name, string asm, _, _) = innerMapping.GetMapping(); + TypeReference innerRef = GetOrCreateTypeReference(ns, name, asm); + TypeSignature[] innerArgs = [.. gits.TypeArguments.Select(MapCustomMappedTypeArgument)]; + return new GenericInstanceTypeSignature(innerRef, false, innerArgs); + } + } + + return mapped; + } + + /// + /// Adds the explicit implementation methods for a specific mapped WinRT interface. + /// + private void AddCustomMappedTypeMembers( + TypeDefinition outputType, + string mappedTypeName, + ITypeDefOrRef mappedInterfaceRef, + bool isPublic) + { + // Build the qualified prefix for explicit implementation method names. + // For generic types, use short type names (e.g. "IMap`2" not "IMap`2") + string qualifiedPrefix = FormatQualifiedInterfaceName(mappedInterfaceRef); + + // Store parent interface generic type arguments for MethodImpl signature conversion. + // MethodImpl declarations on generic interfaces should reference methods using !0, !1 etc. (not resolved types). + TypeSignature[]? parentGenericArgs = null; + if (mappedInterfaceRef is TypeSpecification mts2 && mts2.Signature is GenericInstanceTypeSignature mgits) + { + parentGenericArgs = [.. mgits.TypeArguments]; + } + + // Look up a type signature in the parent interface's generic arguments by identity. + // Returns the corresponding GenericParameterSignature (!0, !1) or null if not found. + GenericParameterSignature? FindParentGenericParam(TypeSignature sig) + { + if (parentGenericArgs == null) + { + return null; + } + + string sigFullName = sig.FullName; + for (int i = 0; i < parentGenericArgs.Length; i++) + { + if (parentGenericArgs[i].FullName == sigFullName) + { + return new GenericParameterSignature(_outputModule, GenericParameterType.Type, i); + } + } + + return null; + } + + // Convert a resolved type signature to use generic parameters (!0, !1) for MethodImpl declarations. + // For parent interface generic args, substitutes resolved types back to !0, !1. + // For all GenericInstanceTypeSignature in signatures, converts to open form + // (e.g., EventHandler`1 -> EventHandler`1) matching WinRT metadata conventions. + TypeSignature ToGenericParam(TypeSignature sig) + { + if (parentGenericArgs != null) + { + GenericParameterSignature? gps = FindParentGenericParam(sig); + if (gps != null) + { + return gps; + } + } + + return sig switch + { + GenericInstanceTypeSignature innerGits => ToOpenGenericForm(innerGits), + SzArrayTypeSignature szArray => new SzArrayTypeSignature(ToGenericParam(szArray.BaseType)), + ByReferenceTypeSignature byRef => new ByReferenceTypeSignature(ToGenericParam(byRef.BaseType)), + _ => sig + }; + } + + // Convert a GenericInstanceTypeSignature to its open form for MethodImpl declarations. + // E.g., KeyValuePair -> KeyValuePair when those are parent interface args. + GenericInstanceTypeSignature ToOpenGenericForm(GenericInstanceTypeSignature gits) + { + TypeSignature[] openArgs = new TypeSignature[gits.TypeArguments.Count]; + for (int i = 0; i < gits.TypeArguments.Count; i++) + { + TypeSignature arg = gits.TypeArguments[i]; + // Try parent interface generic arg substitution (first match wins for duplicate args) + GenericParameterSignature? parentGps = FindParentGenericParam(arg); + if (parentGps != null) + { + openArgs[i] = parentGps; + } + else if (arg is GenericInstanceTypeSignature nestedGits) + { + openArgs[i] = ToOpenGenericForm(nestedGits); + } + else + { + // Use the generic type's own parameter + openArgs[i] = new GenericParameterSignature(_outputModule, GenericParameterType.Type, i); + } + } + + return new GenericInstanceTypeSignature(gits.GenericType, gits.IsValueType, openArgs); + } + + void AddMappedMethod(string name, (string name, TypeSignature type, ParameterAttributes attrs)[]? parameters, TypeSignature? returnType) + { + string methodName = isPublic ? name : $"{qualifiedPrefix}.{name}"; + + MethodAttributes attrs = isPublic + ? (MethodAttributes.Public | MethodAttributes.Final | MethodAttributes.Virtual | MethodAttributes.HideBySig | MethodAttributes.NewSlot) + : (MethodAttributes.Private | MethodAttributes.Final | MethodAttributes.Virtual | MethodAttributes.HideBySig | MethodAttributes.NewSlot); + + TypeSignature[] paramTypes = parameters?.Select(p => p.type).ToArray() ?? []; + MethodSignature sig = MethodSignature.CreateInstance(returnType ?? _outputModule.CorLibTypeFactory.Void, paramTypes); + + MethodDefinition method = new(methodName, attrs, sig) + { + ImplAttributes = MethodImplAttributes.Runtime | MethodImplAttributes.Managed + }; + + if (parameters != null) + { + int idx = 1; + foreach ((string name, TypeSignature type, ParameterAttributes attrs) p in parameters) + { + method.ParameterDefinitions.Add(new ParameterDefinition((ushort)idx++, p.name, p.attrs)); + } + } + + outputType.Methods.Add(method); + + // Add MethodImpl pointing to the mapped interface method (use generic params !0, !1 for declaration signature) + TypeSignature[] implParamTypes = parameters?.Select(p => ToGenericParam(p.type)).ToArray() ?? []; + TypeSignature implReturnType = ToGenericParam(returnType ?? _outputModule.CorLibTypeFactory.Void); + MemberReference interfaceMethodRef = new(mappedInterfaceRef, name, MethodSignature.CreateInstance(implReturnType, implParamTypes)); + outputType.MethodImplementations.Add(new MethodImplementation(interfaceMethodRef, method)); + } + + void AddMappedProperty(string name, TypeSignature propertyType, bool hasSetter) + { + string propName = isPublic ? name : $"{qualifiedPrefix}.{name}"; + string getMethodName = isPublic ? $"get_{name}" : $"{qualifiedPrefix}.get_{name}"; + + // Getter + MethodAttributes getAttrs = isPublic + ? (MethodAttributes.Public | MethodAttributes.Final | MethodAttributes.Virtual | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.SpecialName) + : (MethodAttributes.Private | MethodAttributes.Final | MethodAttributes.Virtual | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.SpecialName); + + MethodDefinition getter = new(getMethodName, getAttrs, MethodSignature.CreateInstance(propertyType)) + { + ImplAttributes = MethodImplAttributes.Runtime | MethodImplAttributes.Managed + }; + outputType.Methods.Add(getter); + + // Property + PropertyDefinition prop = new(propName, 0, PropertySignature.CreateInstance(propertyType)); + prop.Semantics.Add(new MethodSemantics(getter, MethodSemanticsAttributes.Getter)); + + // MethodImpl for getter (use generic params for declaration signature) + TypeSignature implPropertyType = ToGenericParam(propertyType); + MemberReference getterRef = new(mappedInterfaceRef, $"get_{name}", MethodSignature.CreateInstance(implPropertyType, [])); + outputType.MethodImplementations.Add(new MethodImplementation(getterRef, getter)); + + if (hasSetter) + { + string putMethodName = isPublic ? $"put_{name}" : $"{qualifiedPrefix}.put_{name}"; + MethodDefinition setter = new(putMethodName, getAttrs, MethodSignature.CreateInstance(_outputModule.CorLibTypeFactory.Void, [propertyType])) + { + ImplAttributes = MethodImplAttributes.Runtime | MethodImplAttributes.Managed + }; + setter.ParameterDefinitions.Add(new ParameterDefinition(1, "value", ParameterAttributes.In)); + outputType.Methods.Add(setter); + prop.Semantics.Add(new MethodSemantics(setter, MethodSemanticsAttributes.Setter)); + + MemberReference setterRef = new(mappedInterfaceRef, $"put_{name}", MethodSignature.CreateInstance(_outputModule.CorLibTypeFactory.Void, [implPropertyType])); + outputType.MethodImplementations.Add(new MethodImplementation(setterRef, setter)); + } + + outputType.Properties.Add(prop); + } + + // Helper to get generic type arguments from the mapped interface + TypeSignature GetGenericArg(int index) + { + return mappedInterfaceRef is TypeSpecification ts && ts.Signature is GenericInstanceTypeSignature gits && index < gits.TypeArguments.Count + ? gits.TypeArguments[index] + : _outputModule.CorLibTypeFactory.Object; + } + + TypeSignature uint32Sig = _outputModule.CorLibTypeFactory.UInt32; + TypeSignature boolSig = _outputModule.CorLibTypeFactory.Boolean; + TypeSignature objectSig = _outputModule.CorLibTypeFactory.Object; + TypeSignature stringSig = _outputModule.CorLibTypeFactory.String; + + TypeSignature GetTypeRef(string ns, string name, string asm, bool isValueType = false) => + GetOrCreateTypeReference(ns, name, asm).ToTypeSignature(isValueType); + + TypeSignature GetGenericTypeRef(string ns, string name, string asm, params TypeSignature[] args) => + new GenericInstanceTypeSignature(GetOrCreateTypeReference(ns, name, asm), false, args); + + // Generate members for each known mapped type + switch (mappedTypeName) + { + case "IClosable": + AddMappedMethod("Close", null, null); + break; + + case "IIterable`1": + AddMappedMethod("First", null, + GetGenericTypeRef("Windows.Foundation.Collections", "IIterator`1", "Windows.Foundation.FoundationContract", GetGenericArg(0))); + break; + + case "IMap`2": + AddMappedMethod("Clear", null, null); + AddMappedMethod("GetView", null, + GetGenericTypeRef("Windows.Foundation.Collections", "IMapView`2", "Windows.Foundation.FoundationContract", GetGenericArg(0), GetGenericArg(1))); + AddMappedMethod("HasKey", + [("key", GetGenericArg(0), ParameterAttributes.In)], boolSig); + AddMappedMethod("Insert", + [("key", GetGenericArg(0), ParameterAttributes.In), ("value", GetGenericArg(1), ParameterAttributes.In)], boolSig); + AddMappedMethod("Lookup", + [("key", GetGenericArg(0), ParameterAttributes.In)], GetGenericArg(1)); + AddMappedMethod("Remove", + [("key", GetGenericArg(0), ParameterAttributes.In)], null); + AddMappedProperty("Size", uint32Sig, false); + break; + + case "IMapView`2": + AddMappedMethod("HasKey", + [("key", GetGenericArg(0), ParameterAttributes.In)], boolSig); + AddMappedMethod("Lookup", + [("key", GetGenericArg(0), ParameterAttributes.In)], GetGenericArg(1)); + AddMappedMethod("Split", + [("first", new ByReferenceTypeSignature(GetGenericTypeRef("Windows.Foundation.Collections", "IMapView`2", "Windows.Foundation.FoundationContract", GetGenericArg(0), GetGenericArg(1))), ParameterAttributes.Out), + ("second", new ByReferenceTypeSignature(GetGenericTypeRef("Windows.Foundation.Collections", "IMapView`2", "Windows.Foundation.FoundationContract", GetGenericArg(0), GetGenericArg(1))), ParameterAttributes.Out)], null); + AddMappedProperty("Size", uint32Sig, false); + break; + + case "IVector`1": + AddMappedMethod("Append", + [("value", GetGenericArg(0), ParameterAttributes.In)], null); + AddMappedMethod("Clear", null, null); + AddMappedMethod("GetAt", + [("index", uint32Sig, ParameterAttributes.In)], GetGenericArg(0)); + AddMappedMethod("GetMany", + [("startIndex", uint32Sig, ParameterAttributes.In), ("items", new SzArrayTypeSignature(GetGenericArg(0)), ParameterAttributes.In)], uint32Sig); + AddMappedMethod("GetView", null, + GetGenericTypeRef("Windows.Foundation.Collections", "IVectorView`1", "Windows.Foundation.FoundationContract", GetGenericArg(0))); + AddMappedMethod("IndexOf", + [("value", GetGenericArg(0), ParameterAttributes.In), ("index", new ByReferenceTypeSignature(uint32Sig), ParameterAttributes.Out)], boolSig); + AddMappedMethod("InsertAt", + [("index", uint32Sig, ParameterAttributes.In), ("value", GetGenericArg(0), ParameterAttributes.In)], null); + AddMappedMethod("RemoveAt", + [("index", uint32Sig, ParameterAttributes.In)], null); + AddMappedMethod("RemoveAtEnd", null, null); + AddMappedMethod("ReplaceAll", + [("items", new SzArrayTypeSignature(GetGenericArg(0)), ParameterAttributes.In)], null); + AddMappedMethod("SetAt", + [("index", uint32Sig, ParameterAttributes.In), ("value", GetGenericArg(0), ParameterAttributes.In)], null); + AddMappedProperty("Size", uint32Sig, false); + break; + + case "IVectorView`1": + AddMappedMethod("GetAt", + [("index", uint32Sig, ParameterAttributes.In)], GetGenericArg(0)); + AddMappedMethod("GetMany", + [("startIndex", uint32Sig, ParameterAttributes.In), ("items", new SzArrayTypeSignature(GetGenericArg(0)), ParameterAttributes.In)], uint32Sig); + AddMappedMethod("IndexOf", + [("value", GetGenericArg(0), ParameterAttributes.In), ("index", new ByReferenceTypeSignature(uint32Sig), ParameterAttributes.Out)], boolSig); + AddMappedProperty("Size", uint32Sig, false); + break; + + case "IBindableIterable": + AddMappedMethod("First", null, + GetTypeRef("Microsoft.UI.Xaml.Interop", "IBindableIterator", "Microsoft.UI")); + break; + + case "IBindableVector": + AddMappedMethod("Append", + [("value", objectSig, ParameterAttributes.In)], null); + AddMappedMethod("Clear", null, null); + AddMappedMethod("GetAt", + [("index", uint32Sig, ParameterAttributes.In)], objectSig); + AddMappedMethod("GetView", null, + GetTypeRef("Microsoft.UI.Xaml.Interop", "IBindableVectorView", "Microsoft.UI")); + AddMappedMethod("IndexOf", + [("value", objectSig, ParameterAttributes.In), ("index", new ByReferenceTypeSignature(uint32Sig), ParameterAttributes.Out)], boolSig); + AddMappedMethod("InsertAt", + [("index", uint32Sig, ParameterAttributes.In), ("value", objectSig, ParameterAttributes.In)], null); + AddMappedMethod("RemoveAt", + [("index", uint32Sig, ParameterAttributes.In)], null); + AddMappedMethod("RemoveAtEnd", null, null); + AddMappedMethod("SetAt", + [("index", uint32Sig, ParameterAttributes.In), ("value", objectSig, ParameterAttributes.In)], null); + AddMappedProperty("Size", uint32Sig, false); + break; + + case "INotifyPropertyChanged": + // Event: PropertyChanged + AddMappedEvent(outputType, "PropertyChanged", + GetTypeRef("Microsoft.UI.Xaml.Data", "PropertyChangedEventHandler", "Microsoft.UI"), + mappedInterfaceRef, isPublic); + break; + + case "ICommand": + AddMappedEvent(outputType, "CanExecuteChanged", + GetGenericTypeRef("Windows.Foundation", "EventHandler`1", "Windows.Foundation.FoundationContract", objectSig), + mappedInterfaceRef, isPublic); + AddMappedMethod("CanExecute", + [("parameter", objectSig, ParameterAttributes.In)], boolSig); + AddMappedMethod("Execute", + [("parameter", objectSig, ParameterAttributes.In)], null); + break; + + case "INotifyCollectionChanged": + AddMappedEvent(outputType, "CollectionChanged", + GetTypeRef("Microsoft.UI.Xaml.Interop", "NotifyCollectionChangedEventHandler", "Microsoft.UI"), + mappedInterfaceRef, isPublic); + break; + + case "INotifyDataErrorInfo": + AddMappedProperty("HasErrors", boolSig, false); + AddMappedEvent(outputType, "ErrorsChanged", + GetGenericTypeRef("Windows.Foundation", "EventHandler`1", "Windows.Foundation.FoundationContract", + GetTypeRef("Microsoft.UI.Xaml.Data", "DataErrorsChangedEventArgs", "Microsoft.UI")), + mappedInterfaceRef, isPublic); + AddMappedMethod("GetErrors", + [("propertyName", stringSig, ParameterAttributes.In)], + GetGenericTypeRef("Windows.Foundation.Collections", "IIterable`1", "Windows.Foundation.FoundationContract", objectSig)); + break; + + case "IXamlServiceProvider": + AddMappedMethod("GetService", + [("type", GetTypeRef("Windows.UI.Xaml.Interop", "TypeName", "Windows.Foundation.UniversalApiContract", isValueType: true), ParameterAttributes.In)], + objectSig); + break; + + default: + break; + } + } + + /// + /// Adds a mapped event with add/remove methods and MethodImpl records. + /// + private void AddMappedEvent( + TypeDefinition outputType, + string eventName, + TypeSignature handlerType, + ITypeDefOrRef mappedInterfaceRef, + bool isPublic) + { + string qualifiedPrefix = mappedInterfaceRef.FullName ?? ""; + TypeReference tokenType = GetOrCreateTypeReference("Windows.Foundation", "EventRegistrationToken", "Windows.Foundation.FoundationContract"); + TypeSignature tokenSig = tokenType.ToTypeSignature(true); + + ITypeDefOrRef handlerTypeRef = handlerType is TypeDefOrRefSignature tdrs ? tdrs.Type : (handlerType is GenericInstanceTypeSignature gits ? new TypeSpecification(gits) : tokenType); + + string addName = isPublic ? $"add_{eventName}" : $"{qualifiedPrefix}.add_{eventName}"; + string removeName = isPublic ? $"remove_{eventName}" : $"{qualifiedPrefix}.remove_{eventName}"; + + MethodAttributes attrs = isPublic + ? (MethodAttributes.Public | MethodAttributes.Final | MethodAttributes.Virtual | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.SpecialName) + : (MethodAttributes.Private | MethodAttributes.Final | MethodAttributes.Virtual | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.SpecialName); + + // Add method + MethodDefinition adder = new(addName, attrs, MethodSignature.CreateInstance(tokenSig, [handlerType])) + { + ImplAttributes = MethodImplAttributes.Runtime | MethodImplAttributes.Managed + }; + adder.ParameterDefinitions.Add(new ParameterDefinition(1, "handler", ParameterAttributes.In)); + outputType.Methods.Add(adder); + + // Remove method + MethodDefinition remover = new(removeName, attrs, MethodSignature.CreateInstance(_outputModule.CorLibTypeFactory.Void, [tokenSig])) + { + ImplAttributes = MethodImplAttributes.Runtime | MethodImplAttributes.Managed + }; + remover.ParameterDefinitions.Add(new ParameterDefinition(1, "token", ParameterAttributes.In)); + outputType.Methods.Add(remover); + + // Event + EventDefinition evt = new(isPublic ? eventName : $"{qualifiedPrefix}.{eventName}", 0, handlerTypeRef); + evt.Semantics.Add(new MethodSemantics(adder, MethodSemanticsAttributes.AddOn)); + evt.Semantics.Add(new MethodSemantics(remover, MethodSemanticsAttributes.RemoveOn)); + outputType.Events.Add(evt); + + // MethodImpls — use open generic form for handler type in declaration signatures + // to match WinRT metadata conventions (e.g., EventHandler`1 not EventHandler`1) + TypeSignature implHandlerType = handlerType is GenericInstanceTypeSignature handlerGits + ? ToOpenGenericFormStatic(handlerGits, _outputModule) + : handlerType; + MemberReference addRef = new(mappedInterfaceRef, $"add_{eventName}", MethodSignature.CreateInstance(tokenSig, [implHandlerType])); + outputType.MethodImplementations.Add(new MethodImplementation(addRef, adder)); + MemberReference removeRef = new(mappedInterfaceRef, $"remove_{eventName}", MethodSignature.CreateInstance(_outputModule.CorLibTypeFactory.Void, [tokenSig])); + outputType.MethodImplementations.Add(new MethodImplementation(removeRef, remover)); + } + + /// + /// Gathers all interfaces from a type and its base type chain, including interfaces + /// inherited from interfaces. Resolves generic type parameters through the base class chain. + /// + private List GatherAllInterfaces(TypeDefinition type) + { + HashSet seen = new(StringComparer.Ordinal); + List result = []; + + void CollectFromType(TypeDefinition typeDef, TypeSignature[]? genericArgs) + { + foreach (InterfaceImplementation impl in typeDef.Interfaces) + { + if (impl.Interface == null) + { + continue; + } + + // If we have generic args, substitute them in the interface reference + ITypeDefOrRef resolvedInterface = impl.Interface; + if (genericArgs != null && impl.Interface is TypeSpecification ts && + ts.Signature is GenericInstanceTypeSignature gits) + { + // Resolve generic parameters in type arguments (recursively for nested generics) + TypeSignature[] resolvedArgs = [.. gits.TypeArguments.Select(arg => ResolveGenericArg(arg, genericArgs))]; + resolvedInterface = new TypeSpecification(new GenericInstanceTypeSignature(gits.GenericType, gits.IsValueType, resolvedArgs)); + } + + string name = resolvedInterface.FullName ?? ""; + if (!seen.Add(name)) + { + continue; + } + + if (IsPubliclyAccessible(resolvedInterface)) + { + result.Add(new InterfaceImplementation(resolvedInterface)); + } + + // Also collect interfaces inherited by this interface + TypeDefinition? interfaceDef = resolvedInterface is TypeSpecification ts2 + ? SafeResolve((ts2.Signature as GenericInstanceTypeSignature)?.GenericType) + : SafeResolve(resolvedInterface); + + if (interfaceDef != null) + { + // Pass the resolved interface's generic args down + TypeSignature[]? innerArgs = resolvedInterface is TypeSpecification ts3 && + ts3.Signature is GenericInstanceTypeSignature innerGits + ? [.. innerGits.TypeArguments] + : null; + CollectFromType(interfaceDef, innerArgs); + } + } + } + + // Collect from the type itself + CollectFromType(type, null); + + // Walk base types, resolving generic arguments + ITypeDefOrRef? baseTypeRef = type.BaseType; + while (baseTypeRef != null) + { + TypeDefinition? baseDef = SafeResolve(baseTypeRef); + if (baseDef == null) + { + break; + } + + // Get generic arguments from the base type reference + TypeSignature[]? baseGenericArgs = baseTypeRef is TypeSpecification baseTs && + baseTs.Signature is GenericInstanceTypeSignature baseGits + ? [.. baseGits.TypeArguments] + : null; + + CollectFromType(baseDef, baseGenericArgs); + baseTypeRef = baseDef.BaseType; + } + + return result; + } + + /// + /// Recursively resolves generic parameters in a type signature using the provided generic arguments. + /// Handles nested generic instances like KeyValuePair<!0, !1>. + /// + private static TypeSignature ResolveGenericArg(TypeSignature arg, TypeSignature[] genericArgs) + { + if (arg is GenericParameterSignature gps && gps.Index < genericArgs.Length) + { + return genericArgs[gps.Index]; + } + + if (arg is GenericInstanceTypeSignature nestedGits) + { + TypeSignature[] resolvedInnerArgs = [.. nestedGits.TypeArguments.Select(a => ResolveGenericArg(a, genericArgs))]; + return new GenericInstanceTypeSignature(nestedGits.GenericType, nestedGits.IsValueType, resolvedInnerArgs); + } + + return arg; + } + + /// + /// Converts a GenericInstanceTypeSignature to its open form for MethodImpl declarations. + /// E.g., EventHandler`1<Object> → EventHandler`1<!0> + /// + private static GenericInstanceTypeSignature ToOpenGenericFormStatic(GenericInstanceTypeSignature gits, ModuleDefinition module) + { + TypeSignature[] openArgs = new TypeSignature[gits.TypeArguments.Count]; + for (int i = 0; i < gits.TypeArguments.Count; i++) + { + openArgs[i] = new GenericParameterSignature(module, GenericParameterType.Type, i); + } + + return new GenericInstanceTypeSignature(gits.GenericType, gits.IsValueType, openArgs); + } + + /// + /// Determines if a mapped interface is publicly implemented on the class. + /// Checks if the class declares public methods whose names match the .NET interface members. + /// For inherited interfaces, walks up the class hierarchy. + /// + private static bool IsInterfacePubliclyImplemented(TypeDefinition classType, InterfaceImplementation impl, RuntimeContext? runtimeContext) + { + TypeDefinition? interfaceDef = impl.Interface is TypeSpecification ts + ? (ts.Signature as GenericInstanceTypeSignature)?.GenericType.Resolve(runtimeContext) + : impl.Interface?.Resolve(runtimeContext); + + if (interfaceDef == null) + { + return false; + } + + // Walk the class hierarchy to find public implementations + TypeDefinition? current = classType; + while (current != null) + { + foreach (MethodDefinition interfaceMethod in interfaceDef.Methods) + { + string methodName = interfaceMethod.Name?.Value ?? ""; + if (current.Methods.Any(m => m.IsPublic && m.Name?.Value == methodName)) + { + return true; + } + } + + current = current.BaseType?.Resolve(runtimeContext); + } + + return false; + } + + /// + /// Formats a qualified interface name for use in explicit method names. + /// Uses short type names for generic arguments (e.g., "String" not "System.String"). + /// + private static string FormatQualifiedInterfaceName(ITypeDefOrRef typeRef) + { + if (typeRef is TypeSpecification spec && spec.Signature is GenericInstanceTypeSignature gits) + { + string baseName = gits.GenericType.FullName ?? ""; + string args = string.Join(", ", gits.TypeArguments.Select(FormatShortTypeName)); + return $"{baseName}<{args}>"; + } + + return typeRef.FullName ?? ""; + } + + /// + /// Formats a type signature using short names (e.g., "String" instead of "System.String"). + /// + private static string FormatShortTypeName(TypeSignature sig) + { + if (sig is GenericInstanceTypeSignature gits) + { + string baseName = gits.GenericType.FullName ?? ""; + string args = string.Join(", ", gits.TypeArguments.Select(FormatShortTypeName)); + return $"{baseName}<{args}>"; + } + + return sig is CorLibTypeSignature corLib + ? corLib.Type.Name?.Value ?? sig.FullName + : sig.FullName; + } + + /// + /// Collects the names of members that belong to custom mapped or unmapped .NET interfaces. + /// These members should be excluded from the WinMD class definition since they're replaced + /// by the WinRT mapped interface members. + /// + private HashSet CollectCustomMappedMemberNames(TypeDefinition inputType) + { + HashSet memberNames = new(StringComparer.Ordinal); + + List allInterfaces = GatherAllInterfaces(inputType); + + foreach (InterfaceImplementation impl in allInterfaces) + { + if (impl.Interface == null) + { + continue; + } + + string interfaceName = GetInterfaceQualifiedName(impl.Interface); + + // Include members from both mapped interfaces and unmapped interfaces + if (!_mapper.HasMappingForType(interfaceName) && + !TypeMapper.ImplementedInterfacesWithoutMapping.Contains(interfaceName)) + { + continue; + } + + TypeDefinition? interfaceDef = impl.Interface is TypeSpecification ts + ? SafeResolve((ts.Signature as GenericInstanceTypeSignature)?.GenericType) + : SafeResolve(impl.Interface); + + if (interfaceDef == null) + { + continue; + } + + // Add all method names from this interface + foreach (MethodDefinition method in interfaceDef.Methods) + { + string methodName = method.Name?.Value ?? ""; + _ = memberNames.Add(methodName); + + // For property accessors, also add the property name + if (methodName.StartsWith("get_", StringComparison.Ordinal) || methodName.StartsWith("set_", StringComparison.Ordinal)) + { + _ = memberNames.Add(methodName[4..]); + } + } + + // Add property names + foreach (PropertyDefinition prop in interfaceDef.Properties) + { + _ = memberNames.Add(prop.Name?.Value ?? ""); + } + + // Add event names + foreach (EventDefinition evt in interfaceDef.Events) + { + _ = memberNames.Add(evt.Name?.Value ?? ""); + } + } + + return memberNames; + } +} \ No newline at end of file diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Members.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Members.cs new file mode 100644 index 0000000000..fc9dfc0682 --- /dev/null +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Members.cs @@ -0,0 +1,324 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Linq; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using AsmResolver.PE.DotNet.Metadata.Tables; +using WindowsRuntime.WinMDGenerator.Discovery; +using MethodAttributes = AsmResolver.PE.DotNet.Metadata.Tables.MethodAttributes; +using MethodImplAttributes = AsmResolver.PE.DotNet.Metadata.Tables.MethodImplAttributes; +using MethodSemanticsAttributes = AsmResolver.PE.DotNet.Metadata.Tables.MethodSemanticsAttributes; +using ParameterAttributes = AsmResolver.PE.DotNet.Metadata.Tables.ParameterAttributes; + +namespace WindowsRuntime.WinMDGenerator.Generation; + +internal sealed partial class WinmdWriter +{ + private void AddMethodToInterface(TypeDefinition outputType, MethodDefinition inputMethod) + { + TypeSignature returnType = inputMethod.Signature!.ReturnType is CorLibTypeSignature { ElementType: ElementType.Void } + ? _outputModule.CorLibTypeFactory.Void + : MapTypeSignatureToOutput(inputMethod.Signature.ReturnType); + + TypeSignature[] parameterTypes = [.. inputMethod.Signature.ParameterTypes + .Select(MapTypeSignatureToOutput)]; + + MethodAttributes attrs = + MethodAttributes.Public | + MethodAttributes.HideBySig | + MethodAttributes.Abstract | + MethodAttributes.Virtual | + MethodAttributes.NewSlot; + + if (inputMethod.IsSpecialName) + { + attrs |= MethodAttributes.SpecialName; + } + + MethodDefinition outputMethod = new( + inputMethod.Name!.Value, + attrs, + MethodSignature.CreateInstance(returnType, parameterTypes)); + + // Add parameter definitions with correct attributes for WinRT array conventions + AddParameterDefinitions(outputMethod, inputMethod); + + outputType.Methods.Add(outputMethod); + + // Copy custom attributes from the input method + CopyCustomAttributes(inputMethod, outputMethod); + } + + private void AddMethodToClass(TypeDefinition outputType, MethodDefinition inputMethod) + { + TypeSignature returnType = inputMethod.Signature!.ReturnType is CorLibTypeSignature { ElementType: ElementType.Void } + ? _outputModule.CorLibTypeFactory.Void + : MapTypeSignatureToOutput(inputMethod.Signature.ReturnType); + + TypeSignature[] parameterTypes = [.. inputMethod.Signature.ParameterTypes + .Select(MapTypeSignatureToOutput)]; + + bool isConstructor = inputMethod.IsConstructor; + MethodAttributes attrs = MethodAttributes.Public | MethodAttributes.HideBySig; + + if (isConstructor) + { + attrs |= MethodAttributes.SpecialName | MethodAttributes.RuntimeSpecialName; + } + else if (inputMethod.IsStatic) + { + attrs |= MethodAttributes.Static; + } + else + { + attrs |= MethodAttributes.Virtual | MethodAttributes.NewSlot | MethodAttributes.Final; + } + + if (inputMethod.IsSpecialName && !isConstructor) + { + attrs |= MethodAttributes.SpecialName; + } + + MethodSignature signature = isConstructor || !inputMethod.IsStatic + ? MethodSignature.CreateInstance(returnType, parameterTypes) + : MethodSignature.CreateStatic(returnType, parameterTypes); + + MethodDefinition outputMethod = new( + inputMethod.Name!.Value, + attrs, + signature) + { + ImplAttributes = MethodImplAttributes.Runtime | MethodImplAttributes.Managed + }; + + // Add parameter definitions with correct attributes for WinRT array conventions + AddParameterDefinitions(outputMethod, inputMethod); + + outputType.Methods.Add(outputMethod); + + // Copy custom attributes from the input method + CopyCustomAttributes(inputMethod, outputMethod); + } + + /// + /// Adds parameter definitions to an output method with correct WinRT attributes. + /// Handles Span/ReadOnlySpan → array parameter attribute mapping: + /// - ReadOnlySpan<T> → [in] T[] (PassArray) + /// - Span<T> → [out] T[] without BYREF (FillArray) + /// - out T[] → [out] T[] with BYREF (ReceiveArray) + /// - All other params → [in] + /// + private static void AddParameterDefinitions(MethodDefinition outputMethod, MethodDefinition inputMethod) + { + int paramIndex = 1; + IList inputParamTypes = inputMethod.Signature!.ParameterTypes; + + foreach (ParameterDefinition inputParam in inputMethod.ParameterDefinitions) + { + int sigIndex = paramIndex - 1; + ParameterAttributes paramAttrs = ParameterAttributes.In; + + if (sigIndex < inputParamTypes.Count) + { + paramAttrs = GetWinRTParameterAttributes(inputParamTypes[sigIndex]); + } + + outputMethod.ParameterDefinitions.Add(new ParameterDefinition( + (ushort)paramIndex++, + inputParam.Name!.Value, + paramAttrs)); + } + } + + /// + /// Determines the WinRT parameter attributes based on the input parameter type. + /// + private static ParameterAttributes GetWinRTParameterAttributes(TypeSignature inputParamType) + { + // out parameters (ByRef) stay as Out + if (inputParamType is ByReferenceTypeSignature) + { + return ParameterAttributes.Out; + } + + // Span → FillArray pattern: [out] without BYREF + if (inputParamType is GenericInstanceTypeSignature gits) + { + string typeName = AssemblyAnalyzer.GetQualifiedName(gits.GenericType); + if (typeName == "System.Span`1") + { + return ParameterAttributes.Out; + } + } + + // ReadOnlySpan and everything else → [in] + return ParameterAttributes.In; + } + + private void AddPropertyToType(TypeDefinition outputType, PropertyDefinition inputProperty, bool isInterfaceParent) + { + TypeSignature propertyType = MapTypeSignatureToOutput(inputProperty.Signature!.ReturnType); + + // For interface parents (synthesized interfaces), always use instance signatures + // even when the original property was static — interface methods are always instance + bool isStatic = !isInterfaceParent && (inputProperty.GetMethod?.IsStatic == true || inputProperty.SetMethod?.IsStatic == true); + + PropertyDefinition outputProperty = new( + inputProperty.Name!.Value, + 0, + isStatic ? PropertySignature.CreateStatic(propertyType) : PropertySignature.CreateInstance(propertyType)); + + // Add getter + if (inputProperty.GetMethod != null) + { + MethodAttributes attrs = MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName; + if (isInterfaceParent) + { + attrs |= MethodAttributes.Abstract | MethodAttributes.Virtual | MethodAttributes.NewSlot; + } + else if (isStatic) + { + attrs |= MethodAttributes.Static; + } + else + { + attrs |= MethodAttributes.Virtual | MethodAttributes.NewSlot | MethodAttributes.Final; + } + + MethodSignature getSignature = isStatic + ? MethodSignature.CreateStatic(propertyType) + : MethodSignature.CreateInstance(propertyType); + + MethodDefinition getter = new("get_" + inputProperty.Name.Value, attrs, getSignature); + if (!isInterfaceParent) + { + getter.ImplAttributes = MethodImplAttributes.Runtime | MethodImplAttributes.Managed; + } + outputType.Methods.Add(getter); + outputProperty.Semantics.Add(new MethodSemantics(getter, MethodSemanticsAttributes.Getter)); + } + + // Add setter (WinRT uses "put_" prefix) + if (inputProperty.SetMethod != null && inputProperty.SetMethod.IsPublic) + { + MethodAttributes attrs = MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName; + if (isInterfaceParent) + { + attrs |= MethodAttributes.Abstract | MethodAttributes.Virtual | MethodAttributes.NewSlot; + } + else if (isStatic) + { + attrs |= MethodAttributes.Static; + } + else + { + attrs |= MethodAttributes.Virtual | MethodAttributes.NewSlot | MethodAttributes.Final; + } + + MethodSignature setSignature = isStatic + ? MethodSignature.CreateStatic(_outputModule.CorLibTypeFactory.Void, [propertyType]) + : MethodSignature.CreateInstance(_outputModule.CorLibTypeFactory.Void, [propertyType]); + + MethodDefinition setter = new("put_" + inputProperty.Name.Value, attrs, setSignature); + if (!isInterfaceParent) + { + setter.ImplAttributes = MethodImplAttributes.Runtime | MethodImplAttributes.Managed; + } + + // Add parameter + setter.ParameterDefinitions.Add(new ParameterDefinition(1, "value", ParameterAttributes.In)); + + outputType.Methods.Add(setter); + outputProperty.Semantics.Add(new MethodSemantics(setter, MethodSemanticsAttributes.Setter)); + } + + outputType.Properties.Add(outputProperty); + + // Copy custom attributes from the input property + CopyCustomAttributes(inputProperty, outputProperty); + } + + private void AddEventToType(TypeDefinition outputType, EventDefinition inputEvent, bool isInterfaceParent) + { + ITypeDefOrRef eventType = ImportTypeReference(inputEvent.EventType!); + TypeReference eventRegistrationTokenType = GetOrCreateTypeReference( + "Windows.Foundation", "EventRegistrationToken", "Windows.Foundation.FoundationContract"); + + EventDefinition outputEvent = new(inputEvent.Name!.Value, 0, eventType); + + // For interface parents (synthesized interfaces), always use instance signatures + bool isStatic = !isInterfaceParent && inputEvent.AddMethod?.IsStatic == true; + + // Add method + { + MethodAttributes attrs = MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName; + if (isInterfaceParent) + { + attrs |= MethodAttributes.Abstract | MethodAttributes.Virtual | MethodAttributes.NewSlot; + } + else if (isStatic) + { + attrs |= MethodAttributes.Static; + } + else + { + attrs |= MethodAttributes.Virtual | MethodAttributes.NewSlot | MethodAttributes.Final; + } + + TypeSignature handlerSig = eventType.ToTypeSignature(false); + TypeSignature tokenSig = eventRegistrationTokenType.ToTypeSignature(true); + + MethodSignature addSignature = isStatic + ? MethodSignature.CreateStatic(tokenSig, [handlerSig]) + : MethodSignature.CreateInstance(tokenSig, [handlerSig]); + + MethodDefinition adder = new("add_" + inputEvent.Name.Value, attrs, addSignature); + if (!isInterfaceParent) + { + adder.ImplAttributes = MethodImplAttributes.Runtime | MethodImplAttributes.Managed; + } + adder.ParameterDefinitions.Add(new ParameterDefinition(1, "handler", ParameterAttributes.In)); + outputType.Methods.Add(adder); + outputEvent.Semantics.Add(new MethodSemantics(adder, MethodSemanticsAttributes.AddOn)); + } + + // Remove method + { + MethodAttributes attrs = MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName; + if (isInterfaceParent) + { + attrs |= MethodAttributes.Abstract | MethodAttributes.Virtual | MethodAttributes.NewSlot; + } + else if (isStatic) + { + attrs |= MethodAttributes.Static; + } + else + { + attrs |= MethodAttributes.Virtual | MethodAttributes.NewSlot | MethodAttributes.Final; + } + + TypeSignature tokenSig = eventRegistrationTokenType.ToTypeSignature(true); + + MethodSignature removeSignature = isStatic + ? MethodSignature.CreateStatic(_outputModule.CorLibTypeFactory.Void, [tokenSig]) + : MethodSignature.CreateInstance(_outputModule.CorLibTypeFactory.Void, [tokenSig]); + + MethodDefinition remover = new("remove_" + inputEvent.Name.Value, attrs, removeSignature); + if (!isInterfaceParent) + { + remover.ImplAttributes = MethodImplAttributes.Runtime | MethodImplAttributes.Managed; + } + remover.ParameterDefinitions.Add(new ParameterDefinition(1, "token", ParameterAttributes.In)); + outputType.Methods.Add(remover); + outputEvent.Semantics.Add(new MethodSemantics(remover, MethodSemanticsAttributes.RemoveOn)); + } + + outputType.Events.Add(outputEvent); + + // Copy custom attributes from the input event + CopyCustomAttributes(inputEvent, outputEvent); + } +} \ No newline at end of file diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.SynthesizedInterfaces.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.SynthesizedInterfaces.cs new file mode 100644 index 0000000000..6a12c05960 --- /dev/null +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.SynthesizedInterfaces.cs @@ -0,0 +1,289 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Linq; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using WindowsRuntime.WinMDGenerator.Discovery; +using MethodAttributes = AsmResolver.PE.DotNet.Metadata.Tables.MethodAttributes; +using TypeAttributes = AsmResolver.PE.DotNet.Metadata.Tables.TypeAttributes; + +namespace WindowsRuntime.WinMDGenerator.Generation; + +internal sealed partial class WinmdWriter +{ + private enum SynthesizedInterfaceType + { + Static, + Factory, + Default + } + + private static string GetSynthesizedInterfaceName(string className, SynthesizedInterfaceType type) + { + return "I" + className + type switch + { + SynthesizedInterfaceType.Default => "Class", + SynthesizedInterfaceType.Factory => "Factory", + SynthesizedInterfaceType.Static => "Static", + _ => "", + }; + } + + private void AddSynthesizedInterfaces(TypeDefinition inputType, TypeDefinition classOutputType, TypeDeclaration classDeclaration) + { + // Static vs non-static member filtering is handled below per-member + + // Collect members that come from interface implementations + HashSet membersFromInterfaces = []; + + // Use all interfaces including inherited ones from the input type + List allInterfaces = GatherAllInterfaces(inputType); + foreach (InterfaceImplementation impl in allInterfaces) + { + TypeDefinition? interfaceDef = impl.Interface is TypeSpecification ts + ? SafeResolve((ts.Signature as GenericInstanceTypeSignature)?.GenericType) + : SafeResolve(impl.Interface); + + if (interfaceDef != null) + { + foreach (MethodDefinition interfaceMethod in interfaceDef.Methods) + { + _ = membersFromInterfaces.Add(interfaceMethod.Name?.Value ?? ""); + } + + foreach (PropertyDefinition prop in interfaceDef.Properties) + { + _ = membersFromInterfaces.Add(prop.Name?.Value ?? ""); + } + + foreach (EventDefinition evt in interfaceDef.Events) + { + _ = membersFromInterfaces.Add(evt.Name?.Value ?? ""); + } + } + } + + // Also include members from custom mapped interfaces (already excluded from the class) + HashSet customMappedNames = CollectCustomMappedMemberNames(inputType); + membersFromInterfaces.UnionWith(customMappedNames); + + // Also detect explicit interface implementations from the compiled IL + // (private methods with dots in their names like "AuthoringTest.IDouble.GetDouble") + foreach (MethodDefinition method in inputType.Methods) + { + if (!method.IsPublic && method.Name?.Value?.Contains('.') == true) + { + // Extract the method name after the last dot + string fullName = method.Name.Value; + int lastDot = fullName.LastIndexOf('.'); + if (lastDot > 0) + { + _ = membersFromInterfaces.Add(fullName[(lastDot + 1)..]); + } + } + } + + // Also use MethodImplementations from the input type's IL to detect implicit interface + // implementations. This handles cases where a public class method implicitly implements + // an external interface method (e.g., IWwwFormUrlDecoderEntry.get_Name) — the compiler + // generates MethodImpl entries that tell us which methods come from interfaces. + foreach (MethodImplementation methodImpl in inputType.MethodImplementations) + { + if (methodImpl.Body is MethodDefinition bodyMethod && bodyMethod.IsPublic) + { + _ = membersFromInterfaces.Add(bodyMethod.Name?.Value ?? ""); + } + } + + AddSynthesizedInterface(inputType, classOutputType, classDeclaration, SynthesizedInterfaceType.Static, membersFromInterfaces); + AddSynthesizedInterface(inputType, classOutputType, classDeclaration, SynthesizedInterfaceType.Factory, membersFromInterfaces); + AddSynthesizedInterface(inputType, classOutputType, classDeclaration, SynthesizedInterfaceType.Default, membersFromInterfaces); + } + + private void AddSynthesizedInterface( + TypeDefinition inputType, + TypeDefinition classOutputType, + TypeDeclaration classDeclaration, + SynthesizedInterfaceType interfaceType, + HashSet membersFromInterfaces) + { + bool hasMembers = false; + string ns = AssemblyAnalyzer.GetEffectiveNamespace(inputType) ?? ""; + string className = inputType.Name!.Value; + string interfaceName = GetSynthesizedInterfaceName(className, interfaceType); + + TypeAttributes typeAttributes = + TypeAttributes.NotPublic | + (TypeAttributes)0x4000 | // WindowsRuntime + TypeAttributes.AutoLayout | + TypeAttributes.AnsiClass | + TypeAttributes.Interface | + TypeAttributes.Abstract; + + TypeDefinition synthesizedInterface = new(ns, interfaceName, typeAttributes); + + // Add members to the synthesized interface + foreach (MethodDefinition method in inputType.Methods) + { + if (!method.IsPublic) + { + continue; + } + + if (interfaceType == SynthesizedInterfaceType.Factory && + method.IsConstructor && + method.Parameters.Count > 0) + { + // Factory methods: parameterized constructors become Create methods + hasMembers = true; + AddFactoryMethod(synthesizedInterface, inputType, method); + } + else if (interfaceType == SynthesizedInterfaceType.Static && method.IsStatic && !method.IsConstructor && !method.IsSpecialName) + { + hasMembers = true; + AddMethodToInterface(synthesizedInterface, method); + } + else if (interfaceType == SynthesizedInterfaceType.Default && !method.IsStatic && !method.IsConstructor && !method.IsSpecialName) + { + // Only include members not already from an interface + if (!membersFromInterfaces.Contains(method.Name?.Value ?? "")) + { + hasMembers = true; + AddMethodToInterface(synthesizedInterface, method); + } + } + } + + // Add properties + foreach (PropertyDefinition property in inputType.Properties) + { + bool isStatic = property.GetMethod?.IsStatic == true || property.SetMethod?.IsStatic == true; + bool isPublic = property.GetMethod?.IsPublic == true || property.SetMethod?.IsPublic == true; + + if (!isPublic) + { + continue; + } + + if ((interfaceType == SynthesizedInterfaceType.Static && isStatic) || + (interfaceType == SynthesizedInterfaceType.Default && !isStatic)) + { + // For default interface, skip properties already provided by an implemented interface + if (interfaceType == SynthesizedInterfaceType.Default) + { + string getterName = "get_" + property.Name!.Value; + if (membersFromInterfaces.Contains(getterName)) + { + continue; + } + } + + hasMembers = true; + AddPropertyToType(synthesizedInterface, property, isInterfaceParent: true); + } + } + + // Add events + foreach (EventDefinition evt in inputType.Events) + { + bool isStatic = evt.AddMethod?.IsStatic == true; + bool isPublic = evt.AddMethod?.IsPublic == true || evt.RemoveMethod?.IsPublic == true; + + if (!isPublic) + { + continue; + } + + if ((interfaceType == SynthesizedInterfaceType.Static && isStatic) || + (interfaceType == SynthesizedInterfaceType.Default && !isStatic)) + { + // For default interface, skip events already provided by an implemented interface + if (interfaceType == SynthesizedInterfaceType.Default) + { + string adderName = "add_" + evt.Name!.Value; + if (membersFromInterfaces.Contains(adderName)) + { + continue; + } + } + + hasMembers = true; + AddEventToType(synthesizedInterface, evt, isInterfaceParent: true); + } + } + + // Only emit the interface if it has members, or if it's the default and the class has no other interfaces + if (hasMembers || (interfaceType == SynthesizedInterfaceType.Default && inputType.Interfaces.Count == 0)) + { + _outputModule.TopLevelTypes.Add(synthesizedInterface); + + string qualifiedInterfaceName = string.IsNullOrEmpty(ns) ? interfaceName : $"{ns}.{interfaceName}"; + + TypeDeclaration interfaceDeclaration = new(null, synthesizedInterface, isComponentType: false); + _typeDefinitionMapping[qualifiedInterfaceName] = interfaceDeclaration; + + int version = GetVersion(inputType); + + if (interfaceType == SynthesizedInterfaceType.Default) + { + classDeclaration.DefaultInterface = qualifiedInterfaceName; + + // Add interface implementation on the class (use TypeRef per WinMD convention) + InterfaceImplementation interfaceImpl = new(EnsureTypeReference(synthesizedInterface)); + classOutputType.Interfaces.Add(interfaceImpl); + + // Add DefaultAttribute on the interface implementation + AddDefaultAttribute(interfaceImpl); + } + + // Add version attribute + AddVersionAttribute(synthesizedInterface, version); + + // Add GUID attribute + AddGuidAttributeFromName(synthesizedInterface, interfaceName); + + // Add ExclusiveTo attribute + AddExclusiveToAttribute(synthesizedInterface, AssemblyAnalyzer.GetQualifiedName(inputType)); + + if (interfaceType == SynthesizedInterfaceType.Factory) + { + AddActivatableAttribute(classOutputType, (uint)version, qualifiedInterfaceName); + } + else if (interfaceType == SynthesizedInterfaceType.Static) + { + classDeclaration.StaticInterface = qualifiedInterfaceName; + AddStaticAttribute(classOutputType, (uint)version, qualifiedInterfaceName); + } + } + } + + private void AddFactoryMethod(TypeDefinition synthesizedInterface, TypeDefinition classType, MethodDefinition constructor) + { + // Look up the output class TypeDefinition to use as the return type + string classQualifiedName = AssemblyAnalyzer.GetQualifiedName(classType); + TypeDefinition outputClassType = _typeDefinitionMapping[classQualifiedName].OutputType!; + TypeSignature returnType = new TypeDefOrRefSignature(outputClassType, isValueType: false); + + TypeSignature[] parameterTypes = [.. constructor.Signature!.ParameterTypes + .Select(MapTypeSignatureToOutput)]; + + MethodDefinition factoryMethod = new( + "Create" + classType.Name!.Value, + MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.Abstract | MethodAttributes.Virtual | MethodAttributes.NewSlot, + MethodSignature.CreateInstance(returnType, parameterTypes)); + + // Add parameter definitions with correct WinRT attributes + AddParameterDefinitions(factoryMethod, constructor); + + synthesizedInterface.Methods.Add(factoryMethod); + } + + private static string GetInterfaceQualifiedName(ITypeDefOrRef type) + { + return type is TypeSpecification typeSpec && typeSpec.Signature is GenericInstanceTypeSignature genericInst + ? AssemblyAnalyzer.GetQualifiedName(genericInst.GenericType) + : AssemblyAnalyzer.GetQualifiedName(type); + } +} \ No newline at end of file diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.TypeMapping.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.TypeMapping.cs new file mode 100644 index 0000000000..90ecc687cc --- /dev/null +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.TypeMapping.cs @@ -0,0 +1,256 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Linq; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using AsmResolver.PE.DotNet.Metadata.Tables; +using WindowsRuntime.WinMDGenerator.Discovery; +using WindowsRuntime.WinMDGenerator.Models; +using AssemblyAttributes = AsmResolver.PE.DotNet.Metadata.Tables.AssemblyAttributes; + +namespace WindowsRuntime.WinMDGenerator.Generation; + +internal sealed partial class WinmdWriter +{ + /// + /// Maps a type signature from the input module to the output module. + /// + private TypeSignature MapTypeSignatureToOutput(TypeSignature inputSig) + { + // Handle CorLib types + if (inputSig is CorLibTypeSignature corLib) + { +#pragma warning disable IDE0072 // Switch already has default case handling all other element types + return corLib.ElementType switch + { + ElementType.Boolean => _outputModule.CorLibTypeFactory.Boolean, + ElementType.Char => _outputModule.CorLibTypeFactory.Char, + ElementType.I1 => _outputModule.CorLibTypeFactory.SByte, + ElementType.U1 => _outputModule.CorLibTypeFactory.Byte, + ElementType.I2 => _outputModule.CorLibTypeFactory.Int16, + ElementType.U2 => _outputModule.CorLibTypeFactory.UInt16, + ElementType.I4 => _outputModule.CorLibTypeFactory.Int32, + ElementType.U4 => _outputModule.CorLibTypeFactory.UInt32, + ElementType.I8 => _outputModule.CorLibTypeFactory.Int64, + ElementType.U8 => _outputModule.CorLibTypeFactory.UInt64, + ElementType.R4 => _outputModule.CorLibTypeFactory.Single, + ElementType.R8 => _outputModule.CorLibTypeFactory.Double, + ElementType.String => _outputModule.CorLibTypeFactory.String, + ElementType.Object => _outputModule.CorLibTypeFactory.Object, + ElementType.I => _outputModule.CorLibTypeFactory.IntPtr, + ElementType.U => _outputModule.CorLibTypeFactory.UIntPtr, + ElementType.Void => _outputModule.CorLibTypeFactory.Void, + _ => _outputModule.CorLibTypeFactory.Object + }; +#pragma warning restore IDE0072 + } + + // Handle SZArray (single-dimensional zero-based arrays) + if (inputSig is SzArrayTypeSignature szArray) + { + return new SzArrayTypeSignature(MapTypeSignatureToOutput(szArray.BaseType)); + } + + // Handle generic instance types + if (inputSig is GenericInstanceTypeSignature genericInst) + { + string genericTypeName = AssemblyAnalyzer.GetQualifiedName(genericInst.GenericType); + + // Map Span and ReadOnlySpan to T[] (SzArray) for WinRT + // ReadOnlySpan → PassArray (in), Span → FillArray (out without BYREF) + if (genericTypeName is "System.Span`1" or "System.ReadOnlySpan`1" + && genericInst.TypeArguments.Count == 1) + { + return new SzArrayTypeSignature(MapTypeSignatureToOutput(genericInst.TypeArguments[0])); + } + + // Check if the generic type itself has a WinRT mapping (e.g., IList`1 -> IVector`1) + if (_mapper.HasMappingForType(genericTypeName)) + { + MappedType mapping = _mapper.GetMappedType(genericTypeName); + (string ns, string name, string asm, _, bool isValueType) = mapping.GetMapping(); + ITypeDefOrRef mappedType = GetOrCreateTypeReference(ns, name, asm); + TypeSignature[] mappedArgs = [.. genericInst.TypeArguments.Select(MapTypeSignatureToOutput)]; + return new GenericInstanceTypeSignature(mappedType, isValueType, mappedArgs); + } + + ITypeDefOrRef importedType = ImportTypeReference(genericInst.GenericType); + TypeSignature[] importedArgs = [.. genericInst.TypeArguments.Select(MapTypeSignatureToOutput)]; + return new GenericInstanceTypeSignature(importedType, genericInst.IsValueType, importedArgs); + } + + // Handle generic method/type parameters + if (inputSig is GenericParameterSignature genericParam) + { + return new GenericParameterSignature(_outputModule, genericParam.ParameterType, genericParam.Index); + } + + // Handle ByRef + if (inputSig is ByReferenceTypeSignature byRef) + { + return new ByReferenceTypeSignature(MapTypeSignatureToOutput(byRef.BaseType)); + } + + // Handle TypeDefOrRefSignature + if (inputSig is TypeDefOrRefSignature typeDefOrRef) + { + string typeName = AssemblyAnalyzer.GetQualifiedName(typeDefOrRef.Type); + + // Check if the type has a WinRT mapping (e.g., IDisposable -> IClosable, Type -> TypeName) + if (_mapper.HasMappingForType(typeName)) + { + MappedType mapping = _mapper.GetMappedType(typeName); + (string ns, string name, string asm, _, bool isValueType) = mapping.GetMapping(); + ITypeDefOrRef mappedType = GetOrCreateTypeReference(ns, name, asm); + return new TypeDefOrRefSignature(mappedType, isValueType); + } + + ITypeDefOrRef importedRef = ImportTypeReference(typeDefOrRef.Type); + return new TypeDefOrRefSignature(importedRef, typeDefOrRef.IsValueType); + } + + // Fallback: import the type + return _outputModule.CorLibTypeFactory.Object; + } + + /// + /// Imports a type reference from the input module to the output module. + /// + private ITypeDefOrRef ImportTypeReference(ITypeDefOrRef type) + { + if (type is TypeDefinition typeDef) + { + // Check if we've already processed this type into the output module + string qualifiedName = AssemblyAnalyzer.GetQualifiedName(typeDef); + if (_typeDefinitionMapping.TryGetValue(qualifiedName, out TypeDeclaration? declaration) && declaration.OutputType != null) + { + return declaration.OutputType; + } + + // If this is a public type from the input module, process it on demand + if (typeDef.IsPublic || typeDef.IsNestedPublic) + { + ProcessType(typeDef); + if (_typeDefinitionMapping.TryGetValue(qualifiedName, out declaration) && declaration.OutputType != null) + { + return declaration.OutputType; + } + } + + // External type or non-WinRT type — create a type reference + return GetOrCreateTypeReference( + AssemblyAnalyzer.GetEffectiveNamespace(typeDef) ?? "", + typeDef.Name!.Value, + _inputModule.Assembly?.Name?.Value ?? "mscorlib"); + } + + if (type is TypeReference typeRef) + { + string ns = typeRef.Namespace?.Value ?? ""; + string name = typeRef.Name!.Value; + string fullName = string.IsNullOrEmpty(ns) ? name : $"{ns}.{name}"; + + // Check if this type is in the output module (same-assembly reference) + if (_typeDefinitionMapping.TryGetValue(fullName, out TypeDeclaration? declaration) && declaration.OutputType != null) + { + return declaration.OutputType; + } + + // For WinRT types from projection assemblies, use the WinRT contract assembly name + // from WindowsRuntimeMetadataAttribute instead of the projection assembly name. + // E.g., StackPanel from Microsoft.WinUI → Microsoft.UI.Xaml in the WinMD. + string assembly = GetAssemblyNameFromScope(typeRef.Scope); + TypeDefinition? resolvedType = SafeResolve(typeRef); + if (resolvedType != null) + { + string? winrtAssembly = AssemblyAnalyzer.GetAssemblyForWinRTType(resolvedType); + if (winrtAssembly != null) + { + assembly = winrtAssembly; + } + } + + return GetOrCreateTypeReference(ns, name, assembly); + } + + if (type is TypeSpecification typeSpec) + { + // For type specs, we need to create a new TypeSpecification in the output + TypeSignature mappedSig = MapTypeSignatureToOutput(typeSpec.Signature!); + return new TypeSpecification(mappedSig); + } + + return GetOrCreateTypeReference("System", "Object", "mscorlib"); + } + + private static string GetAssemblyNameFromScope(IResolutionScope? scope) + { + return scope switch + { + AssemblyReference asmRef => asmRef.Name?.Value ?? "mscorlib", + ModuleDefinition mod => mod.Assembly?.Name?.Value ?? "mscorlib", + _ => "mscorlib" + }; + } + + /// + /// Ensures an ITypeDefOrRef is a TypeReference, not a TypeDefinition. + /// WinMD convention: interface implementations should use TypeRef even for same-module types + /// (TypeDef redirection per the WinMD spec). + /// + private ITypeDefOrRef EnsureTypeReference(ITypeDefOrRef type) + { + if (type is TypeDefinition typeDef) + { + string ns = AssemblyAnalyzer.GetEffectiveNamespace(typeDef) ?? ""; + string name = typeDef.Name!.Value; + string assembly = _outputModule.Assembly?.Name?.Value ?? "mscorlib"; + return GetOrCreateTypeReference(ns, name, assembly); + } + + return type; + } + + private TypeReference GetOrCreateTypeReference(string @namespace, string name, string assemblyName) + { + string fullName = string.IsNullOrEmpty(@namespace) ? name : $"{@namespace}.{name}"; + + if (_typeReferenceCache.TryGetValue(fullName, out TypeReference? cached)) + { + return cached; + } + + AssemblyReference assemblyRef = GetOrCreateAssemblyReference(assemblyName); + TypeReference typeRef = new(_outputModule, assemblyRef, @namespace, name); + _typeReferenceCache[fullName] = typeRef; + return typeRef; + } + + private AssemblyReference GetOrCreateAssemblyReference(string assemblyName) + { + if (_assemblyReferenceCache.TryGetValue(assemblyName, out AssemblyReference? cached)) + { + return cached; + } + + AssemblyAttributes flags = string.CompareOrdinal(assemblyName, "mscorlib") == 0 + ? 0 + : AssemblyAttributes.ContentWindowsRuntime; + + AssemblyReference asmRef = new(assemblyName, new Version(0xFF, 0xFF, 0xFF, 0xFF)) + { + Attributes = flags, + }; + + if (string.CompareOrdinal(assemblyName, "mscorlib") == 0) + { + asmRef.PublicKeyOrToken = [0xb7, 0x7a, 0x5c, 0x56, 0x19, 0x34, 0xe0, 0x89]; + } + + _outputModule.AssemblyReferences.Add(asmRef); + _assemblyReferenceCache[assemblyName] = asmRef; + return asmRef; + } +} \ No newline at end of file diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs new file mode 100644 index 0000000000..55e69cd6ad --- /dev/null +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs @@ -0,0 +1,681 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using AsmResolver.PE.DotNet.Metadata.Tables; +using WindowsRuntime.WinMDGenerator.Discovery; +using WindowsRuntime.WinMDGenerator.Models; +using FieldAttributes = AsmResolver.PE.DotNet.Metadata.Tables.FieldAttributes; +using MethodAttributes = AsmResolver.PE.DotNet.Metadata.Tables.MethodAttributes; +using MethodImplAttributes = AsmResolver.PE.DotNet.Metadata.Tables.MethodImplAttributes; +using MethodSemanticsAttributes = AsmResolver.PE.DotNet.Metadata.Tables.MethodSemanticsAttributes; +using ParameterAttributes = AsmResolver.PE.DotNet.Metadata.Tables.ParameterAttributes; +using TypeAttributes = AsmResolver.PE.DotNet.Metadata.Tables.TypeAttributes; + +namespace WindowsRuntime.WinMDGenerator.Generation; + +internal sealed partial class WinmdWriter +{ + /// + /// Checks if an enum type represents a WinRT API contract (has [ApiContract] attribute). + /// + private static bool IsApiContract(TypeDefinition type) + { + return type.CustomAttributes.Any( + attr => attr.Constructor?.DeclaringType?.Name?.Value == "ApiContractAttribute"); + } + + /// + /// Emits an API contract type as an empty struct in the WinMD. + /// In C#, API contracts are projected as enums with [ApiContract], but in WinMD + /// metadata they are represented as empty structs per the WinRT type system spec. + /// + private void AddApiContractType(TypeDefinition inputType) + { + string qualifiedName = AssemblyAnalyzer.GetQualifiedName(inputType); + + TypeAttributes typeAttributes = + TypeAttributes.Public | + (TypeAttributes)0x4000 | // WindowsRuntime + TypeAttributes.SequentialLayout | + TypeAttributes.AnsiClass | + TypeAttributes.Sealed; + + TypeReference baseType = GetOrCreateTypeReference("System", "ValueType", "mscorlib"); + + TypeDefinition outputType = new( + AssemblyAnalyzer.GetEffectiveNamespace(inputType), + inputType.Name!.Value, + typeAttributes, + baseType); + + _outputModule.TopLevelTypes.Add(outputType); + TypeDeclaration declaration = new(inputType, outputType, isComponentType: true); + _typeDefinitionMapping[qualifiedName] = declaration; + } + + private void AddEnumType(TypeDefinition inputType) + { + // API contract types are projected as enums in C# but emitted as empty structs in WinMD + if (IsApiContract(inputType)) + { + AddApiContractType(inputType); + return; + } + + string qualifiedName = AssemblyAnalyzer.GetQualifiedName(inputType); + + TypeAttributes typeAttributes = + TypeAttributes.Public | + (TypeAttributes)0x4000 | // WindowsRuntime + TypeAttributes.AutoLayout | + TypeAttributes.AnsiClass | + TypeAttributes.Sealed; + + TypeReference baseType = GetOrCreateTypeReference("System", "Enum", "mscorlib"); + + TypeDefinition outputType = new( + AssemblyAnalyzer.GetEffectiveNamespace(inputType), + inputType.Name!.Value, + typeAttributes, + baseType); + + // Add the value__ field + TypeSignature underlyingType = GetEnumUnderlyingType(inputType); + FieldDefinition valueField = new( + "value__", + FieldAttributes.Private | FieldAttributes.SpecialName | FieldAttributes.RuntimeSpecialName, + new FieldSignature(underlyingType)); + outputType.Fields.Add(valueField); + + _outputModule.TopLevelTypes.Add(outputType); + + TypeDeclaration declaration = new(inputType, outputType, isComponentType: true); + _typeDefinitionMapping[qualifiedName] = declaration; + + // Enum literal fields use the enum type itself (not the underlying type) + TypeSignature enumTypeSignature = new TypeDefOrRefSignature(outputType, isValueType: true); + + // Add enum members + foreach (FieldDefinition field in inputType.Fields) + { + if (field.IsSpecialName) + { + continue; // Skip value__ + } + + if (!field.IsPublic) + { + continue; + } + + FieldDefinition outputField = new( + field.Name!.Value, + FieldAttributes.Public | FieldAttributes.Static | FieldAttributes.Literal | FieldAttributes.HasDefault, + new FieldSignature(enumTypeSignature)); + + if (field.Constant != null) + { + outputField.Constant = new Constant(field.Constant.Type, new DataBlobSignature(field.Constant.Value!.Data)); + } + + outputType.Fields.Add(outputField); + } + } + + private TypeSignature GetEnumUnderlyingType(TypeDefinition enumType) + { + foreach (FieldDefinition field in enumType.Fields) + { + if (field.IsSpecialName && field.Name?.Value == "value__") + { + return MapTypeSignatureToOutput(field.Signature!.FieldType); + } + } + + // Default to Int32 + return _outputModule.CorLibTypeFactory.Int32; + } + + private void AddDelegateType(TypeDefinition inputType) + { + string qualifiedName = AssemblyAnalyzer.GetQualifiedName(inputType); + + TypeAttributes typeAttributes = + TypeAttributes.Public | + (TypeAttributes)0x4000 | // WindowsRuntime + TypeAttributes.AutoLayout | + TypeAttributes.AnsiClass | + TypeAttributes.Sealed; + + TypeReference baseType = GetOrCreateTypeReference("System", "MulticastDelegate", "mscorlib"); + + TypeDefinition outputType = new( + AssemblyAnalyzer.GetEffectiveNamespace(inputType), + inputType.Name!.Value, + typeAttributes, + baseType); + + // Register early so self-referencing signatures can find this type + _outputModule.TopLevelTypes.Add(outputType); + TypeDeclaration declaration = new(inputType, outputType, isComponentType: true); + _typeDefinitionMapping[qualifiedName] = declaration; + + // Add .ctor(object, IntPtr) — private per WinRT delegate convention + MethodDefinition ctor = new( + ".ctor", + MethodAttributes.Private | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RuntimeSpecialName, + MethodSignature.CreateInstance( + _outputModule.CorLibTypeFactory.Void, + [_outputModule.CorLibTypeFactory.Object, + _outputModule.CorLibTypeFactory.IntPtr])) + { + ImplAttributes = MethodImplAttributes.Runtime | MethodImplAttributes.Managed + }; + ctor.ParameterDefinitions.Add(new ParameterDefinition(1, "object", 0)); + ctor.ParameterDefinitions.Add(new ParameterDefinition(2, "method", 0)); + outputType.Methods.Add(ctor); + + // Add Invoke method + MethodDefinition? inputInvoke = inputType.Methods.FirstOrDefault(m => m.Name?.Value == "Invoke"); + if (inputInvoke != null) + { + TypeSignature returnType = inputInvoke.Signature!.ReturnType is CorLibTypeSignature { ElementType: ElementType.Void } + ? _outputModule.CorLibTypeFactory.Void + : MapTypeSignatureToOutput(inputInvoke.Signature.ReturnType); + + TypeSignature[] parameterTypes = [.. inputInvoke.Signature.ParameterTypes + .Select(MapTypeSignatureToOutput)]; + + MethodDefinition invoke = new( + "Invoke", + MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.Virtual | MethodAttributes.NewSlot, + MethodSignature.CreateInstance(returnType, parameterTypes)) + { + ImplAttributes = MethodImplAttributes.Runtime | MethodImplAttributes.Managed + }; + + // Add parameter names with [In] attribute + int paramIndex = 1; + foreach (ParameterDefinition inputParam in inputInvoke.ParameterDefinitions) + { + invoke.ParameterDefinitions.Add(new ParameterDefinition( + (ushort)paramIndex++, + inputParam.Name!.Value, + ParameterAttributes.In)); + } + + outputType.Methods.Add(invoke); + } + + // Add GUID attribute + AddGuidAttribute(outputType, inputType); + } + + private void AddInterfaceType(TypeDefinition inputType) + { + string qualifiedName = AssemblyAnalyzer.GetQualifiedName(inputType); + + TypeAttributes typeAttributes = + TypeAttributes.Public | + (TypeAttributes)0x4000 | // WindowsRuntime + TypeAttributes.AutoLayout | + TypeAttributes.AnsiClass | + TypeAttributes.Interface | + TypeAttributes.Abstract; + + TypeDefinition outputType = new( + AssemblyAnalyzer.GetEffectiveNamespace(inputType), + inputType.Name!.Value, + typeAttributes); + + // Register early so self-referencing signatures can find this type + _outputModule.TopLevelTypes.Add(outputType); + TypeDeclaration declaration = new(inputType, outputType, isComponentType: true); + _typeDefinitionMapping[qualifiedName] = declaration; + + // Add methods (skip property/event accessors — they're added by property/event processing below) + foreach (MethodDefinition method in inputType.Methods) + { + if (!method.IsPublic || method.IsSpecialName) + { + continue; + } + + AddMethodToInterface(outputType, method); + } + + // Add properties + foreach (PropertyDefinition property in inputType.Properties) + { + AddPropertyToType(outputType, property, isInterfaceParent: true); + } + + // Add events + foreach (EventDefinition evt in inputType.Events) + { + AddEventToType(outputType, evt, isInterfaceParent: true); + } + + // Add interface implementations + foreach (InterfaceImplementation impl in inputType.Interfaces) + { + if (impl.Interface != null) + { + ITypeDefOrRef outputInterfaceRef = EnsureTypeReference(ImportTypeReference(impl.Interface)); + outputType.Interfaces.Add(new InterfaceImplementation(outputInterfaceRef)); + } + } + + // Add GUID attribute + AddGuidAttribute(outputType, inputType); + } + + private void AddStructType(TypeDefinition inputType) + { + string qualifiedName = AssemblyAnalyzer.GetQualifiedName(inputType); + + TypeAttributes typeAttributes = + TypeAttributes.Public | + (TypeAttributes)0x4000 | // WindowsRuntime + TypeAttributes.SequentialLayout | + TypeAttributes.AnsiClass | + TypeAttributes.Sealed; + + TypeReference baseType = GetOrCreateTypeReference("System", "ValueType", "mscorlib"); + + TypeDefinition outputType = new( + AssemblyAnalyzer.GetEffectiveNamespace(inputType), + inputType.Name!.Value, + typeAttributes, + baseType); + + // Register early so self-referencing signatures can find this type + _outputModule.TopLevelTypes.Add(outputType); + TypeDeclaration declaration = new(inputType, outputType, isComponentType: true); + _typeDefinitionMapping[qualifiedName] = declaration; + + // Add public fields + foreach (FieldDefinition field in inputType.Fields) + { + if (!field.IsPublic || field.IsStatic) + { + continue; + } + + FieldDefinition outputField = new( + field.Name!.Value, + FieldAttributes.Public, + new FieldSignature(MapTypeSignatureToOutput(field.Signature!.FieldType))); + outputType.Fields.Add(outputField); + } + } + + private void AddClassType(TypeDefinition inputType) + { + string qualifiedName = AssemblyAnalyzer.GetQualifiedName(inputType); + + TypeAttributes typeAttributes = + TypeAttributes.Public | + (TypeAttributes)0x4000 | // WindowsRuntime + TypeAttributes.AutoLayout | + TypeAttributes.AnsiClass | + TypeAttributes.Class | + TypeAttributes.BeforeFieldInit; + + // Sealed for: sealed classes and static classes (abstract+sealed in metadata) + // WinRT doesn't support abstract base classes, so non-static abstract classes + // are treated as regular unsealed runtime classes + if (inputType.IsSealed) + { + typeAttributes |= TypeAttributes.Sealed; + } + + // In C#, static classes are both abstract and sealed in metadata + if (inputType.IsAbstract && inputType.IsSealed) + { + typeAttributes |= TypeAttributes.Abstract; + } + + // Determine base type + ITypeDefOrRef? baseType; + if (inputType.BaseType != null && inputType.BaseType.FullName != "System.Object") + { + // Check if the base type is abstract; WinRT doesn't support projecting abstract classes + TypeDefinition? baseTypeDef = SafeResolve(inputType.BaseType); + baseType = baseTypeDef != null && baseTypeDef.IsAbstract + ? GetOrCreateTypeReference("System", "Object", "mscorlib") + : ImportTypeReference(inputType.BaseType); + } + else + { + baseType = GetOrCreateTypeReference("System", "Object", "mscorlib"); + } + + TypeDefinition outputType = new( + AssemblyAnalyzer.GetEffectiveNamespace(inputType), + inputType.Name!.Value, + typeAttributes, + baseType); + + // Register in the mapping early so self-referencing method signatures can find it + _outputModule.TopLevelTypes.Add(outputType); + TypeDeclaration declaration = new(inputType, outputType, isComponentType: true); + _typeDefinitionMapping[qualifiedName] = declaration; + + bool hasConstructor = false; + bool hasDefaultConstructor = false; + bool hasAtLeastOneNonPublicConstructor = false; + bool isStaticClass = inputType.IsAbstract && inputType.IsSealed; + + // Collect members from custom mapped interfaces and unmapped interfaces to exclude from the class + HashSet customMappedMembers = CollectCustomMappedMemberNames(inputType); + + // Add methods (non-property/event accessors) + foreach (MethodDefinition method in inputType.Methods) + { + if (method.IsConstructor) + { + if (!method.IsPublic) + { + hasAtLeastOneNonPublicConstructor = true; + continue; + } + + hasConstructor = true; + hasDefaultConstructor |= method.Parameters.Count == 0; + AddMethodToClass(outputType, method); + } + else if (method.IsPublic && !method.IsSpecialName) + { + // Skip methods that belong to custom mapped or unmapped interfaces + if (customMappedMembers.Contains(method.Name?.Value ?? "")) + { + continue; + } + + AddMethodToClass(outputType, method); + } + } + + // Add properties + foreach (PropertyDefinition property in inputType.Properties) + { + // Skip properties that belong to custom mapped or unmapped interfaces + if (customMappedMembers.Contains(property.Name?.Value ?? "")) + { + continue; + } + + // Only add if at least one accessor is public + bool hasPublicGetter = property.GetMethod?.IsPublic == true; + bool hasPublicSetter = property.SetMethod?.IsPublic == true; + if (hasPublicGetter || hasPublicSetter) + { + AddPropertyToType(outputType, property, isInterfaceParent: false); + } + } + + // Add events + foreach (EventDefinition evt in inputType.Events) + { + // Skip events that belong to custom mapped or unmapped interfaces + if (customMappedMembers.Contains(evt.Name?.Value ?? "")) + { + continue; + } + + bool hasPublicAdder = evt.AddMethod?.IsPublic == true; + bool hasPublicRemover = evt.RemoveMethod?.IsPublic == true; + if (hasPublicAdder || hasPublicRemover) + { + AddEventToType(outputType, evt, isInterfaceParent: false); + } + } + + // Implicit constructor if none defined + if (!hasConstructor && !hasAtLeastOneNonPublicConstructor && !isStaticClass) + { + MethodDefinition defaultCtor = new( + ".ctor", + MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RuntimeSpecialName, + MethodSignature.CreateInstance(_outputModule.CorLibTypeFactory.Void)) + { + ImplAttributes = MethodImplAttributes.Runtime | MethodImplAttributes.Managed + }; + outputType.Methods.Add(defaultCtor); + hasDefaultConstructor = true; + } + + // Add interface implementations (excluding mapped and unmappable interfaces) + foreach (InterfaceImplementation impl in inputType.Interfaces) + { + if (impl.Interface == null || !IsPubliclyAccessible(impl.Interface)) + { + continue; + } + + string interfaceName = GetInterfaceQualifiedName(impl.Interface); + + // Skip interfaces that have a WinRT mapping — they'll be added as their + // mapped equivalents by ProcessCustomMappedInterfaces below + if (_mapper.HasMappingForType(interfaceName)) + { + continue; + } + + // Skip .NET interfaces that have no WinRT equivalent + if (TypeMapper.ImplementedInterfacesWithoutMapping.Contains(interfaceName)) + { + continue; + } + + ITypeDefOrRef outputInterfaceRef = EnsureTypeReference(ImportTypeReference(impl.Interface)); + outputType.Interfaces.Add(new InterfaceImplementation(outputInterfaceRef)); + } + + // Add activatable attribute if it has a default constructor + if (hasDefaultConstructor) + { + int version = GetVersion(inputType); + AddActivatableAttribute(outputType, (uint)version, null); + } + + // Process custom mapped interfaces (IList -> IVector, IDisposable -> IClosable, etc.) + ProcessCustomMappedInterfaces(inputType, outputType); + + // Add synthesized interfaces (IFooClass, IFooFactory, IFooStatic) + AddSynthesizedInterfaces(inputType, outputType, declaration); + + // Add explicit interface implementation methods (private methods with qualified names) + AddExplicitInterfaceImplementations(inputType, outputType); + + // If no default synthesized interface was created but the class implements + // user interfaces, mark the first interface implementation as [Default]. + // The [Default] goes on the WinRT equivalent of the first user-declared .NET interface. + if (declaration.DefaultInterface == null && outputType.Interfaces.Count > 0) + { + InterfaceImplementation? defaultImpl = FindDefaultInterface(inputType, outputType); + AddDefaultAttribute(defaultImpl ?? outputType.Interfaces[0]); + } + } + + /// + /// Finds the output interface that should receive [Default] — the WinRT equivalent + /// of the first user-declared .NET interface on the input type. + /// + private InterfaceImplementation? FindDefaultInterface(TypeDefinition inputType, TypeDefinition outputType) + { + if (inputType.Interfaces.Count == 0) + { + return null; + } + + // Get the first user-declared interface + InterfaceImplementation firstInputImpl = inputType.Interfaces[0]; + string firstIfaceName = GetInterfaceQualifiedName(firstInputImpl.Interface!); + + // Check if it's a mapped type (e.g., IList -> IVector) + string targetName = _mapper.HasMappingForType(firstIfaceName) + ? _mapper.GetMappedType(firstIfaceName).GetMapping() is var (ns, name, _, _, _) + ? string.IsNullOrEmpty(ns) ? name : $"{ns}.{name}" + : firstIfaceName + : firstIfaceName; + + // Find the matching interface on the output type + foreach (InterfaceImplementation outputImpl in outputType.Interfaces) + { + if (outputImpl.Interface != null && GetInterfaceQualifiedName(outputImpl.Interface) == targetName) + { + return outputImpl; + } + } + + return null; + } + + /// + /// Adds explicit interface implementation methods from the input class. + /// Applies WinRT conventions: set_ to put_, event add returns EventRegistrationToken, + /// event remove takes EventRegistrationToken. + /// + private void AddExplicitInterfaceImplementations(TypeDefinition inputType, TypeDefinition outputType) + { + TypeReference eventRegistrationTokenType = GetOrCreateTypeReference( + "Windows.Foundation", "EventRegistrationToken", "Windows.Foundation.FoundationContract"); + TypeSignature tokenSig = eventRegistrationTokenType.ToTypeSignature(true); + + foreach (MethodDefinition method in inputType.Methods) + { + if (method.IsPublic || method.Name?.Value?.Contains('.') != true) + { + continue; + } + + string fullMethodName = method.Name.Value; + int lastDot = fullMethodName.LastIndexOf('.'); + if (lastDot <= 0) + { + continue; + } + + string interfaceQualName = fullMethodName[..lastDot]; + string shortMethodName = fullMethodName[(lastDot + 1)..]; + + if (!_typeDefinitionMapping.ContainsKey(interfaceQualName)) + { + continue; + } + + // Apply WinRT naming: set_ to put_ + string winrtShortName = shortMethodName; + if (winrtShortName.StartsWith("set_", StringComparison.Ordinal)) + { + winrtShortName = "put_" + winrtShortName[4..]; + } + + string winrtFullName = $"{interfaceQualName}.{winrtShortName}"; + + TypeSignature returnType; + TypeSignature[] parameterTypes; + string[] paramNames; + + if (winrtShortName.StartsWith("add_", StringComparison.Ordinal)) + { + // Event add: returns EventRegistrationToken, param is handler type named "handler" + returnType = tokenSig; + parameterTypes = [.. method.Signature!.ParameterTypes.Select(MapTypeSignatureToOutput)]; + paramNames = ["handler"]; + } + else if (winrtShortName.StartsWith("remove_", StringComparison.Ordinal)) + { + // Event remove: takes EventRegistrationToken named "token", returns void + returnType = _outputModule.CorLibTypeFactory.Void; + parameterTypes = [tokenSig]; + paramNames = ["token"]; + } + else + { + returnType = method.Signature!.ReturnType is CorLibTypeSignature { ElementType: ElementType.Void } + ? _outputModule.CorLibTypeFactory.Void + : MapTypeSignatureToOutput(method.Signature.ReturnType); + parameterTypes = [.. method.Signature.ParameterTypes.Select(MapTypeSignatureToOutput)]; + paramNames = [.. method.ParameterDefinitions.Select(p => p.Name?.Value ?? "value")]; + } + + MethodAttributes attrs = + MethodAttributes.Private | MethodAttributes.Final | MethodAttributes.Virtual | + MethodAttributes.HideBySig | MethodAttributes.NewSlot; + + if (method.IsSpecialName) + { + attrs |= MethodAttributes.SpecialName; + } + + MethodDefinition outputMethod = new(winrtFullName, attrs, + MethodSignature.CreateInstance(returnType, parameterTypes)) + { + ImplAttributes = MethodImplAttributes.Runtime | MethodImplAttributes.Managed + }; + + for (int i = 0; i < paramNames.Length; i++) + { + ParameterAttributes paramAttr = i < parameterTypes.Length + ? GetWinRTParameterAttributes(method.Signature!.ParameterTypes[i]) + : ParameterAttributes.In; + outputMethod.ParameterDefinitions.Add(new ParameterDefinition( + (ushort)(i + 1), paramNames[i], paramAttr)); + } + + outputType.Methods.Add(outputMethod); + + if (winrtShortName.StartsWith("get_", StringComparison.Ordinal) || winrtShortName.StartsWith("put_", StringComparison.Ordinal)) + { + string propName = $"{interfaceQualName}.{winrtShortName[4..]}"; + PropertyDefinition? existingProp = outputType.Properties.FirstOrDefault(p => p.Name?.Value == propName); + if (existingProp == null) + { + TypeSignature propType = winrtShortName.StartsWith("get_", StringComparison.Ordinal) ? returnType : parameterTypes[0]; + PropertyDefinition prop = new(propName, 0, PropertySignature.CreateInstance(propType)); + prop.Semantics.Add(new MethodSemantics(outputMethod, + winrtShortName.StartsWith("get_", StringComparison.Ordinal) ? MethodSemanticsAttributes.Getter : MethodSemanticsAttributes.Setter)); + outputType.Properties.Add(prop); + } + else + { + existingProp.Semantics.Add(new MethodSemantics(outputMethod, MethodSemanticsAttributes.Setter)); + } + } + else if (winrtShortName.StartsWith("add_", StringComparison.Ordinal)) + { + string evtName = $"{interfaceQualName}.{winrtShortName[4..]}"; + ITypeDefOrRef eventType = parameterTypes.Length > 0 && parameterTypes[0] is TypeDefOrRefSignature tdrs + ? tdrs.Type + : parameterTypes.Length > 0 && parameterTypes[0] is GenericInstanceTypeSignature gits + ? new TypeSpecification(gits) + : GetOrCreateTypeReference("Windows.Foundation", "EventHandler`1", "Windows.Foundation.FoundationContract"); + EventDefinition evt = new(evtName, 0, eventType); + evt.Semantics.Add(new MethodSemantics(outputMethod, MethodSemanticsAttributes.AddOn)); + outputType.Events.Add(evt); + } + else if (winrtShortName.StartsWith("remove_", StringComparison.Ordinal)) + { + string evtName = $"{interfaceQualName}.{winrtShortName[7..]}"; + EventDefinition? existingEvt = outputType.Events.FirstOrDefault(e => e.Name?.Value == evtName); + existingEvt?.Semantics.Add(new MethodSemantics(outputMethod, MethodSemanticsAttributes.RemoveOn)); + } + + TypeDeclaration interfaceDecl = _typeDefinitionMapping[interfaceQualName]; + if (interfaceDecl.OutputType != null) + { + MemberReference interfaceMethodRef = new(interfaceDecl.OutputType, winrtShortName, + MethodSignature.CreateInstance(returnType, parameterTypes)); + outputType.MethodImplementations.Add(new MethodImplementation(interfaceMethodRef, outputMethod)); + } + } + } +} diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs new file mode 100644 index 0000000000..3a5090e8d2 --- /dev/null +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs @@ -0,0 +1,483 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using WindowsRuntime.WinMDGenerator.Discovery; +using WindowsRuntime.WinMDGenerator.Errors; +using WindowsRuntime.WinMDGenerator.Models; +using AssemblyAttributes = AsmResolver.PE.DotNet.Metadata.Tables.AssemblyAttributes; + +namespace WindowsRuntime.WinMDGenerator.Generation; + +/// +/// Writes a WinMD file from analyzed assembly types using AsmResolver. +/// +internal sealed partial class WinmdWriter +{ + private readonly string _version; + private readonly TypeMapper _mapper; + private readonly ModuleDefinition _inputModule; + private readonly RuntimeContext? _runtimeContext; + + // Output WinMD module and assembly + private readonly ModuleDefinition _outputModule; + + // Tracking for type definitions in the output WinMD + private readonly Dictionary _typeDefinitionMapping = new(StringComparer.Ordinal); + private readonly Dictionary _assemblyReferenceCache = new(StringComparer.Ordinal); + private readonly Dictionary _typeReferenceCache = new(StringComparer.Ordinal); + + /// + /// Creates a new instance. + /// + public WinmdWriter( + string assemblyName, + string version, + TypeMapper mapper, + ModuleDefinition inputModule) + { + _version = version; + _mapper = mapper; + _inputModule = inputModule; + _runtimeContext = inputModule.RuntimeContext; + + // Create the output WinMD module + _outputModule = new ModuleDefinition(assemblyName + ".winmd") + { + RuntimeVersion = "WindowsRuntime 1.4" + }; + + // Replace the default mscorlib reference with the WinMD-style one (v255.255.255.255 with PKT) + AssemblyReference defaultCorLib = (AssemblyReference)_outputModule.CorLibTypeFactory.CorLibScope; + defaultCorLib.Version = new Version(0xFF, 0xFF, 0xFF, 0xFF); + defaultCorLib.PublicKeyOrToken = [0xb7, 0x7a, 0x5c, 0x56, 0x19, 0x34, 0xe0, 0x89]; + _assemblyReferenceCache["mscorlib"] = defaultCorLib; + + // Create the output assembly with WindowsRuntime flag (keep reference alive via module) + _ = new AssemblyDefinition(assemblyName, new Version(version)) + { + Modules = { _outputModule }, + Attributes = AssemblyAttributes.ContentWindowsRuntime, + HashAlgorithm = AsmResolver.PE.DotNet.Metadata.Tables.AssemblyHashAlgorithm.Sha1 + }; + } + + /// + /// Safely resolves a type reference, returning null instead of throwing if the type + /// cannot be found (e.g., output-only synthesized types not in the input assembly). + /// + private TypeDefinition? SafeResolve(ITypeDefOrRef? typeRef) + { + return typeRef is TypeDefinition td + ? td + : typeRef is not null + && _runtimeContext is not null + && typeRef.Resolve(_runtimeContext, out TypeDefinition? resolved) == ResolutionStatus.Success + ? resolved + : null; + } + + /// + /// Processes a public type from the input assembly and adds it to the WinMD. + /// + public void ProcessType(TypeDefinition inputType) + { + string qualifiedName = AssemblyAnalyzer.GetQualifiedName(inputType); + + if (_typeDefinitionMapping.ContainsKey(qualifiedName)) + { + return; + } + + if (inputType.IsEnum) + { + AddEnumType(inputType); + } + else if (inputType.IsDelegate) + { + AddDelegateType(inputType); + } + else if (inputType.IsInterface) + { + AddInterfaceType(inputType); + } + else if (inputType.IsValueType) + { + AddStructType(inputType); + } + else if (inputType.IsClass) + { + AddClassType(inputType); + } + } + + /// + /// Finalizes the WinMD generation by adding MethodImpls, version attributes, and custom attributes. + /// + public void FinalizeGeneration() + { + // Phase 1: Add MethodImpl fixups for classes + // Snapshot the mapping to avoid modification during iteration (ProcessType may add entries via MapTypeSignatureToOutput) + List> typeDeclarations = [.. _typeDefinitionMapping]; + foreach ((string qualifiedName, TypeDeclaration declaration) in typeDeclarations) + { + if (declaration.OutputType == null || declaration.InputType == null || !declaration.IsComponentType) + { + continue; + } + + TypeDefinition classOutputType = declaration.OutputType; + TypeDefinition classInputType = declaration.InputType; + + // Add MethodImpls for implemented interfaces (excluding the default synthesized interface, handled below) + // Snapshot the interfaces list to avoid modification during iteration + List outputInterfaces = [.. classOutputType.Interfaces]; + foreach (InterfaceImplementation classInterfaceImpl in outputInterfaces) + { + // Resolve the interface — handle TypeSpecification (generic instances) by resolving the GenericType + bool resolvedFromInput = false; + TypeSignature[]? interfaceGenericArgs = null; + TypeDefinition? interfaceDef = null; + + if (classInterfaceImpl.Interface is TypeSpecification ts + && ts.Signature is GenericInstanceTypeSignature gits) + { + interfaceDef = SafeResolve(gits.GenericType); + interfaceGenericArgs = [.. gits.TypeArguments]; + } + else + { + interfaceDef = SafeResolve(classInterfaceImpl.Interface); + + // For same-module TypeRefs (created by EnsureTypeReference), Resolve() may fail + // since the output module isn't in the resolver. Look up in our type mapping instead. + if (interfaceDef == null && classInterfaceImpl.Interface != null) + { + string ifaceFullName = classInterfaceImpl.Interface.FullName ?? ""; + if (_typeDefinitionMapping.TryGetValue(ifaceFullName, out TypeDeclaration? ifaceDecl) && ifaceDecl.OutputType != null) + { + interfaceDef = ifaceDecl.OutputType; + } + } + } + + // If the output interface can't be resolved (WinRT contract assemblies), + // find the matching interface from the INPUT type which points to resolvable projection assemblies + if (interfaceDef == null) + { + string outputIfaceName = GetInterfaceQualifiedName(classInterfaceImpl.Interface!); + foreach (InterfaceImplementation inputImpl in classInputType.Interfaces) + { + if (inputImpl.Interface != null && GetInterfaceQualifiedName(inputImpl.Interface) == outputIfaceName) + { + interfaceDef = inputImpl.Interface is TypeSpecification its + && its.Signature is GenericInstanceTypeSignature igits + ? SafeResolve(igits.GenericType) + : SafeResolve(inputImpl.Interface); + resolvedFromInput = interfaceDef != null; + break; + } + } + } + + if (interfaceDef == null) + { + // Still unresolvable — MethodImpls for mapped interfaces are already + // created by AddCustomMappedTypeMembers, so this is expected for those. + continue; + } + + // Skip the default synthesized interface — it's handled separately below + string interfaceQualName = AssemblyAnalyzer.GetQualifiedName(interfaceDef); + if (interfaceQualName == declaration.DefaultInterface) + { + continue; + } + + List interfaceMethods = [.. interfaceDef.Methods]; + foreach (MethodDefinition interfaceMethod in interfaceMethods) + { + // Check if an explicit implementation already exists for this interface method. + // If so, prefer it — don't create a MethodImpl for the public method. + string explicitName = $"{interfaceQualName}.{interfaceMethod.Name?.Value}"; + int paramCount = interfaceMethod.Signature?.ParameterTypes.Count ?? 0; + bool hasExplicitImpl = classOutputType.Methods.Any(m => + m.Name?.Value == explicitName && + (m.Signature?.ParameterTypes.Count ?? 0) == paramCount); + + MethodDefinition? classMethod; + + if (hasExplicitImpl) + { + // Match by explicit name only + classMethod = classOutputType.Methods.FirstOrDefault(m => + { + if (m.Name?.Value != explicitName) + { + return false; + } + + if ((m.Signature?.ParameterTypes.Count ?? 0) != paramCount) + { + return false; + } + + for (int i = 0; i < paramCount; i++) + { + string classParamName = m.Signature!.ParameterTypes[i].FullName; + string ifaceParamName = interfaceMethod.Signature!.ParameterTypes[i].FullName; + + if (classParamName != ifaceParamName && + !(resolvedFromInput && IsProjectionEquivalent(ifaceParamName, classParamName))) + { + return false; + } + } + + return true; + }); + } + else + { + // Find the corresponding method on the class by name. + // When resolved from input ref assemblies, map .NET projection types to WinRT equivalents. + classMethod = FindMatchingMethod(classOutputType, interfaceMethod, resolvedFromInput, interfaceGenericArgs); + + // Fallback for event methods from ref assemblies: CsWinRT projections change + // event accessor signatures (e.g., remove_ takes delegate instead of EventRegistrationToken). + // Match by name only since WinRT event accessors are unique by name. + if (classMethod == null && resolvedFromInput && interfaceMethod.IsSpecialName) + { + string methodName = interfaceMethod.Name?.Value ?? ""; + classMethod = classOutputType.Methods.FirstOrDefault(m => m.Name?.Value == methodName); + } + } + + if (classMethod != null) + { + // Use the class method's signature for the MethodImpl declaration when resolved + // from input ref assemblies — the ref assembly uses .NET projection types + // (e.g., System.Type) but the WinMD needs WinRT types (e.g., TypeName) + MethodSignature implSignature = resolvedFromInput ? classMethod.Signature! : interfaceMethod.Signature!; + MemberReference interfaceMethodRef = new(classInterfaceImpl.Interface, interfaceMethod.Name!.Value, implSignature); + classOutputType.MethodImplementations.Add(new MethodImplementation(interfaceMethodRef, classMethod)); + } + } + } + + // Add MethodImpls for default synthesized interface + if (declaration.DefaultInterface != null && + _typeDefinitionMapping.TryGetValue(declaration.DefaultInterface, out TypeDeclaration? defaultInterfaceDecl) && + defaultInterfaceDecl.OutputType != null) + { + TypeDefinition defaultInterface = defaultInterfaceDecl.OutputType; + + foreach (MethodDefinition interfaceMethod in defaultInterface.Methods) + { + MethodDefinition? classMethod = FindMatchingMethod(classOutputType, interfaceMethod); + if (classMethod != null) + { + MemberReference interfaceMethodRef = new(defaultInterface, interfaceMethod.Name!.Value, interfaceMethod.Signature); + classOutputType.MethodImplementations.Add(new MethodImplementation(interfaceMethodRef, classMethod)); + } + } + } + } + + // Phase 2: Add default version attributes for types that don't have one + int defaultVersion = Version.Parse(_version).Major; + + foreach ((string _, TypeDeclaration declaration) in typeDeclarations) + { + if (declaration.OutputType == null) + { + continue; + } + + if (!HasVersionAttribute(declaration.OutputType)) + { + // Skip adding VersionAttribute if the input type has ContractVersionAttribute + // (it will be copied in Phase 3 via CopyCustomAttributes) + bool hasContractVersion = declaration.InputType?.CustomAttributes.Any( + attr => attr.Constructor?.DeclaringType?.Name?.Value == "ContractVersionAttribute") == true; + + if (!hasContractVersion) + { + // Use the version from the input type if available, otherwise use the default + int version = declaration.InputType != null ? GetVersion(declaration.InputType) : defaultVersion; + AddVersionAttribute(declaration.OutputType, version); + } + } + } + + // Phase 3: Add custom attributes from input types to output types + foreach ((string _, TypeDeclaration declaration) in typeDeclarations) + { + if (declaration.OutputType == null || declaration.InputType == null || !declaration.IsComponentType) + { + continue; + } + + CopyCustomAttributes(declaration.InputType, declaration.OutputType); + } + + // Phase 4: Add overload attributes for methods with the same name + foreach ((string _, TypeDeclaration declaration) in typeDeclarations) + { + if (declaration.OutputType == null) + { + continue; + } + + AddOverloadAttributesForType(declaration.OutputType); + } + } + + private MethodDefinition? FindMatchingMethod( + TypeDefinition classType, + MethodDefinition interfaceMethod, + bool mapInterfaceTypes = false, + TypeSignature[]? interfaceGenericArgs = null) + { + string methodName = interfaceMethod.Name?.Value ?? ""; + + foreach (MethodDefinition classMethod in classType.Methods) + { + if (classMethod.Name?.Value != methodName) + { + continue; + } + + // Match parameter count + if (classMethod.Signature?.ParameterTypes.Count != interfaceMethod.Signature?.ParameterTypes.Count) + { + continue; + } + + // Match parameter types + bool parametersMatch = true; + for (int i = 0; i < (classMethod.Signature?.ParameterTypes.Count ?? 0); i++) + { + string classParamName = classMethod.Signature!.ParameterTypes[i].FullName; + TypeSignature ifaceParamType = interfaceMethod.Signature!.ParameterTypes[i]; + + // Resolve generic parameters (!0, !1) using the interface's generic arguments + if (interfaceGenericArgs != null) + { + ifaceParamType = ResolveGenericArg(ifaceParamType, interfaceGenericArgs); + } + + string ifaceParamName = ifaceParamType.FullName; + + if (classParamName != ifaceParamName) + { + // When comparing against externally-resolved interface methods (from ref assemblies), + // check if the .NET projection type maps to the WinRT type via TypeMapper + if (!mapInterfaceTypes || !IsProjectionEquivalent(ifaceParamName, classParamName)) + { + parametersMatch = false; + break; + } + } + } + + if (!parametersMatch) + { + continue; + } + + return classMethod; + } + + return null; + } + + /// + /// Checks if a .NET projection type name maps to a WinRT type name via the TypeMapper. + /// + private bool IsProjectionEquivalent(string dotNetTypeName, string winrtTypeName) + { + // Strip generic type arguments for mapper lookup. + // E.g., "System.Collections.Generic.IEnumerable`1" → "System.Collections.Generic.IEnumerable`1" + // The mapper uses open generic names as keys. + string lookupName = dotNetTypeName; + int angleBracket = dotNetTypeName.IndexOf('<'); + if (angleBracket > 0) + { + lookupName = dotNetTypeName[..angleBracket]; + } + + if (_mapper.HasMappingForType(lookupName)) + { + (string ns, string name, _, _, _) = _mapper.GetMappedType(lookupName).GetMapping(); + string mappedName = string.IsNullOrEmpty(ns) ? name : $"{ns}.{name}"; + + // For generic types, compare the open generic name portion of both + if (angleBracket > 0) + { + int winrtAngle = winrtTypeName.IndexOf('<'); + string winrtOpenName = winrtAngle > 0 ? winrtTypeName[..winrtAngle] : winrtTypeName; + return mappedName == winrtOpenName; + } + + return mappedName == winrtTypeName; + } + + return false; + } + + 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); + + foreach (IGrouping group in methodGroups) + { + int overloadIndex = 1; + foreach (MethodDefinition method in group.Skip(1)) + { + overloadIndex++; + string overloadName = $"{group.Key}{overloadIndex}"; + AddOverloadAttribute(method, overloadName); + } + } + } + + private void AddOverloadAttribute(MethodDefinition method, string overloadName) + { + TypeReference overloadAttrType = GetOrCreateTypeReference( + "Windows.Foundation.Metadata", "OverloadAttribute", "Windows.Foundation.FoundationContract"); + + MemberReference ctor = new(overloadAttrType, ".ctor", + MethodSignature.CreateInstance( + _outputModule.CorLibTypeFactory.Void, + [_outputModule.CorLibTypeFactory.String])); + + CustomAttributeSignature sig = new(); + sig.FixedArguments.Add(new CustomAttributeArgument(_outputModule.CorLibTypeFactory.String, overloadName)); + + method.CustomAttributes.Add(new CustomAttribute(ctor, sig)); + } + + /// + /// Writes the WinMD to the specified path. + /// + public void Write(string outputPath) + { + try + { + _outputModule.Write(outputPath); + } + catch (Exception e) + { + throw WellKnownWinMDExceptions.WinMDWriteError(e); + } + } +} \ No newline at end of file diff --git a/src/WinRT.WinMD.Generator/Models/TypeDeclaration.cs b/src/WinRT.WinMD.Generator/Models/TypeDeclaration.cs new file mode 100644 index 0000000000..1c4675edfa --- /dev/null +++ b/src/WinRT.WinMD.Generator/Models/TypeDeclaration.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AsmResolver.DotNet; + +namespace WindowsRuntime.WinMDGenerator.Generation; + +/// +/// Tracks the input and output type definitions for a given type. +/// +internal sealed class TypeDeclaration +{ + /// The source type from the input assembly. + public TypeDefinition? InputType { get; } + + /// The generated type in the output WinMD. + public TypeDefinition? OutputType { get; } + + /// Whether this type is a component type (authored by the user). + public bool IsComponentType { get; } + + /// The name of the default synthesized interface. + public string? DefaultInterface { get; set; } + + /// The name of the static synthesized interface. + public string? StaticInterface { get; set; } + + public TypeDeclaration(TypeDefinition? inputType, TypeDefinition? outputType, bool isComponentType = false) + { + InputType = inputType; + OutputType = outputType; + IsComponentType = isComponentType; + } +} \ No newline at end of file diff --git a/src/WinRT.WinMD.Generator/Models/TypeMapper.cs b/src/WinRT.WinMD.Generator/Models/TypeMapper.cs new file mode 100644 index 0000000000..a2b09ee838 --- /dev/null +++ b/src/WinRT.WinMD.Generator/Models/TypeMapper.cs @@ -0,0 +1,223 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using AsmResolver.DotNet; + +namespace WindowsRuntime.WinMDGenerator.Models; + +/// +/// A mapped type from a .NET type to a WinRT type. +/// +internal readonly struct MappedType +{ + private readonly string? _namespace; + private readonly string? _name; + private readonly string? _assembly; + private readonly bool _isSystemType; + private readonly bool _isValueType; + private readonly bool _isBlittable; + private readonly Func? _multipleMappingFunc; + + /// + /// Creates a new with a fixed mapping. + /// + public MappedType(string @namespace, string name, string assembly, bool isValueType = false, bool isBlittable = false) + { + _namespace = @namespace; + _name = name; + _assembly = assembly; + _isSystemType = string.CompareOrdinal(assembly, "mscorlib") == 0; + _isValueType = isValueType; + _isBlittable = isBlittable; + _multipleMappingFunc = null; + } + + /// + /// Creates a new with a context-dependent mapping. + /// + public MappedType(Func multipleMappingFunc) + { + _namespace = null; + _name = null; + _assembly = null; + _isSystemType = false; + _isValueType = false; + _isBlittable = false; + _multipleMappingFunc = multipleMappingFunc; + } + + /// + /// Gets the mapping tuple (namespace, name, assembly, isSystemType, isValueType). + /// + /// The optional containing type for context-dependent mappings. + public (string Namespace, string Name, string Assembly, bool IsSystemType, bool IsValueType) GetMapping(TypeDefinition? containingType = null) + { + return _multipleMappingFunc != null + ? _multipleMappingFunc(containingType) + : (_namespace!, _name!, _assembly!, _isSystemType, _isValueType); + } + + /// + /// Gets whether the mapped type is blittable. + /// + public bool IsBlittable() + { + return _isValueType && _isBlittable; + } +} + +/// +/// Maps .NET types to their WinRT equivalents. +/// +internal sealed class TypeMapper +{ + private readonly Dictionary _typeMapping; + + // Based on whether System.Type is used in an attribute declaration or elsewhere, + // we need to choose the correct custom mapping as attributes don't use the TypeName mapping. + private static (string, string, string, bool, bool) GetSystemTypeCustomMapping(TypeDefinition? containingType) + { + bool isDefinedInAttribute = + containingType != null && + string.CompareOrdinal(containingType.BaseType?.FullName, "System.Attribute") == 0; + return isDefinedInAttribute + ? ("System", "Type", "mscorlib", true, false) + : ("Windows.UI.Xaml.Interop", "TypeName", "Windows.Foundation.UniversalApiContract", false, true); + } + + /// + /// Creates a new instance. + /// + /// Whether to use Windows.UI.Xaml projections. + public TypeMapper(bool useWindowsUIXamlProjections) + { +#pragma warning disable IDE0045 // Keep if-else for readability with large dictionary initializations + // This should be in sync with the reverse mapping from WinRT.Runtime/Projections.cs and cswinrt/helpers.h. + if (useWindowsUIXamlProjections) + { + _typeMapping = new(StringComparer.Ordinal) + { + { "System.DateTimeOffset", new MappedType("Windows.Foundation", "DateTime", "Windows.Foundation.FoundationContract", true, false) }, + { "System.Exception", new MappedType("Windows.Foundation", "HResult", "Windows.Foundation.FoundationContract", true, false) }, + { "System.EventHandler`1", new MappedType("Windows.Foundation", "EventHandler`1", "Windows.Foundation.FoundationContract") }, + { "System.EventHandler`2", new MappedType("Windows.Foundation", "TypedEventHandler`2", "Windows.Foundation.FoundationContract") }, + { "System.FlagsAttribute", new MappedType("System", "FlagsAttribute", "mscorlib") }, + { "System.IDisposable", new MappedType("Windows.Foundation", "IClosable", "Windows.Foundation.FoundationContract") }, + { "System.Nullable`1", new MappedType("Windows.Foundation", "IReference`1", "Windows.Foundation.FoundationContract") }, + { "System.Object", new MappedType("System", "Object", "mscorlib") }, + { "System.TimeSpan", new MappedType("Windows.Foundation", "TimeSpan", "Windows.Foundation.FoundationContract", true, false) }, + { "System.Uri", new MappedType("Windows.Foundation", "Uri", "Windows.Foundation.FoundationContract") }, + { "System.ComponentModel.INotifyPropertyChanged", new MappedType("Windows.UI.Xaml.Data", "INotifyPropertyChanged", "Windows.UI.Xaml") }, + { "System.ComponentModel.PropertyChangedEventArgs", new MappedType("Windows.UI.Xaml.Data", "PropertyChangedEventArgs", "Windows.UI.Xaml") }, + { "System.ComponentModel.PropertyChangedEventHandler", new MappedType("Windows.UI.Xaml.Data", "PropertyChangedEventHandler", "Windows.UI.Xaml") }, + { "System.Windows.Input.ICommand", new MappedType("Windows.UI.Xaml.Input", "ICommand", "Windows.UI.Xaml") }, + { "System.Collections.IEnumerable", new MappedType("Windows.UI.Xaml.Interop", "IBindableIterable", "Windows.UI.Xaml") }, + { "System.Collections.IList", new MappedType("Windows.UI.Xaml.Interop", "IBindableVector", "Windows.UI.Xaml") }, + { "System.Collections.Specialized.INotifyCollectionChanged", new MappedType("Windows.UI.Xaml.Interop", "INotifyCollectionChanged", "Windows.UI.Xaml") }, + { "System.Collections.Specialized.NotifyCollectionChangedAction", new MappedType("Windows.UI.Xaml.Interop", "NotifyCollectionChangedAction", "Windows.UI.Xaml", true, true) }, + { "System.Collections.Specialized.NotifyCollectionChangedEventArgs", new MappedType("Windows.UI.Xaml.Interop", "NotifyCollectionChangedEventArgs", "Windows.UI.Xaml") }, + { "System.Collections.Specialized.NotifyCollectionChangedEventHandler", new MappedType("Windows.UI.Xaml.Interop", "NotifyCollectionChangedEventHandler", "Windows.UI.Xaml") }, + { "WindowsRuntime.InteropServices.EventRegistrationToken", new MappedType("Windows.Foundation", "EventRegistrationToken", "Windows.Foundation.FoundationContract", true, true) }, + { "System.AttributeTargets", new MappedType("Windows.Foundation.Metadata", "AttributeTargets", "Windows.Foundation.FoundationContract", true, true) }, + { "System.AttributeUsageAttribute", new MappedType("Windows.Foundation.Metadata", "AttributeUsageAttribute", "Windows.Foundation.FoundationContract") }, + { "System.Numerics.Matrix3x2", new MappedType("Windows.Foundation.Numerics", "Matrix3x2", "Windows.Foundation.FoundationContract", true, true) }, + { "System.Numerics.Matrix4x4", new MappedType("Windows.Foundation.Numerics", "Matrix4x4", "Windows.Foundation.FoundationContract", true, true) }, + { "System.Numerics.Plane", new MappedType("Windows.Foundation.Numerics", "Plane", "Windows.Foundation.FoundationContract", true, true) }, + { "System.Numerics.Quaternion", new MappedType("Windows.Foundation.Numerics", "Quaternion", "Windows.Foundation.FoundationContract", true, true) }, + { "System.Numerics.Vector2", new MappedType("Windows.Foundation.Numerics", "Vector2", "Windows.Foundation.FoundationContract", true, true) }, + { "System.Numerics.Vector3", new MappedType("Windows.Foundation.Numerics", "Vector3", "Windows.Foundation.FoundationContract", true, true) }, + { "System.Numerics.Vector4", new MappedType("Windows.Foundation.Numerics", "Vector4", "Windows.Foundation.FoundationContract", true, true) }, + { "System.Type", new MappedType(GetSystemTypeCustomMapping) }, + { "System.Collections.Generic.IEnumerable`1", new MappedType("Windows.Foundation.Collections", "IIterable`1", "Windows.Foundation.FoundationContract") }, + { "System.Collections.Generic.IEnumerator`1", new MappedType("Windows.Foundation.Collections", "IIterator`1", "Windows.Foundation.FoundationContract") }, + { "System.Collections.Generic.KeyValuePair`2", new MappedType("Windows.Foundation.Collections", "IKeyValuePair`2", "Windows.Foundation.FoundationContract") }, + { "System.Collections.Generic.IReadOnlyDictionary`2", new MappedType("Windows.Foundation.Collections", "IMapView`2", "Windows.Foundation.FoundationContract") }, + { "System.Collections.Generic.IDictionary`2", new MappedType("Windows.Foundation.Collections", "IMap`2", "Windows.Foundation.FoundationContract") }, + { "System.Collections.Generic.IReadOnlyList`1", new MappedType("Windows.Foundation.Collections", "IVectorView`1", "Windows.Foundation.FoundationContract") }, + { "System.Collections.Generic.IList`1", new MappedType("Windows.Foundation.Collections", "IVector`1", "Windows.Foundation.FoundationContract") }, + { "Windows.UI.Color", new MappedType("Windows.UI", "Color", "Windows.Foundation.UniversalApiContract", true, true) }, + }; + } + else + { + _typeMapping = new(StringComparer.Ordinal) + { + { "System.DateTimeOffset", new MappedType("Windows.Foundation", "DateTime", "Windows.Foundation.FoundationContract", true, false) }, + { "System.Exception", new MappedType("Windows.Foundation", "HResult", "Windows.Foundation.FoundationContract", true, false) }, + { "System.EventHandler`1", new MappedType("Windows.Foundation", "EventHandler`1", "Windows.Foundation.FoundationContract") }, + { "System.EventHandler`2", new MappedType("Windows.Foundation", "TypedEventHandler`2", "Windows.Foundation.FoundationContract") }, + { "System.FlagsAttribute", new MappedType("System", "FlagsAttribute", "mscorlib") }, + { "System.IDisposable", new MappedType("Windows.Foundation", "IClosable", "Windows.Foundation.FoundationContract") }, + { "System.IServiceProvider", new MappedType("Microsoft.UI.Xaml", "IXamlServiceProvider", "Microsoft.UI.Xaml") }, + { "System.Nullable`1", new MappedType("Windows.Foundation", "IReference`1", "Windows.Foundation.FoundationContract") }, + { "System.Object", new MappedType("System", "Object", "mscorlib") }, + { "System.TimeSpan", new MappedType("Windows.Foundation", "TimeSpan", "Windows.Foundation.FoundationContract", true, false) }, + { "System.Uri", new MappedType("Windows.Foundation", "Uri", "Windows.Foundation.FoundationContract") }, + { "System.ComponentModel.DataErrorsChangedEventArgs", new MappedType("Microsoft.UI.Xaml.Data", "DataErrorsChangedEventArgs", "Microsoft.UI.Xaml") }, + { "System.ComponentModel.INotifyDataErrorInfo", new MappedType("Microsoft.UI.Xaml.Data", "INotifyDataErrorInfo", "Microsoft.UI.Xaml") }, + { "System.ComponentModel.INotifyPropertyChanged", new MappedType("Microsoft.UI.Xaml.Data", "INotifyPropertyChanged", "Microsoft.UI.Xaml") }, + { "System.ComponentModel.PropertyChangedEventArgs", new MappedType("Microsoft.UI.Xaml.Data", "PropertyChangedEventArgs", "Microsoft.UI.Xaml") }, + { "System.ComponentModel.PropertyChangedEventHandler", new MappedType("Microsoft.UI.Xaml.Data", "PropertyChangedEventHandler", "Microsoft.UI.Xaml") }, + { "System.Windows.Input.ICommand", new MappedType("Microsoft.UI.Xaml.Input", "ICommand", "Microsoft.UI.Xaml") }, + { "System.Collections.IEnumerable", new MappedType("Microsoft.UI.Xaml.Interop", "IBindableIterable", "Microsoft.UI.Xaml") }, + { "System.Collections.IList", new MappedType("Microsoft.UI.Xaml.Interop", "IBindableVector", "Microsoft.UI.Xaml") }, + { "System.Collections.Specialized.INotifyCollectionChanged", new MappedType("Microsoft.UI.Xaml.Interop", "INotifyCollectionChanged", "Microsoft.UI.Xaml") }, + { "System.Collections.Specialized.NotifyCollectionChangedAction", new MappedType("Microsoft.UI.Xaml.Interop", "NotifyCollectionChangedAction", "Microsoft.UI.Xaml", true, true) }, + { "System.Collections.Specialized.NotifyCollectionChangedEventArgs", new MappedType("Microsoft.UI.Xaml.Interop", "NotifyCollectionChangedEventArgs", "Microsoft.UI.Xaml") }, + { "System.Collections.Specialized.NotifyCollectionChangedEventHandler", new MappedType("Microsoft.UI.Xaml.Interop", "NotifyCollectionChangedEventHandler", "Microsoft.UI.Xaml") }, + { "WindowsRuntime.InteropServices.EventRegistrationToken", new MappedType("Windows.Foundation", "EventRegistrationToken", "Windows.Foundation.FoundationContract", true, true) }, + { "System.AttributeTargets", new MappedType("Windows.Foundation.Metadata", "AttributeTargets", "Windows.Foundation.FoundationContract", true, true) }, + { "System.AttributeUsageAttribute", new MappedType("Windows.Foundation.Metadata", "AttributeUsageAttribute", "Windows.Foundation.FoundationContract") }, + { "System.Numerics.Matrix3x2", new MappedType("Windows.Foundation.Numerics", "Matrix3x2", "Windows.Foundation.FoundationContract", true, true) }, + { "System.Numerics.Matrix4x4", new MappedType("Windows.Foundation.Numerics", "Matrix4x4", "Windows.Foundation.FoundationContract", true, true) }, + { "System.Numerics.Plane", new MappedType("Windows.Foundation.Numerics", "Plane", "Windows.Foundation.FoundationContract", true, true) }, + { "System.Numerics.Quaternion", new MappedType("Windows.Foundation.Numerics", "Quaternion", "Windows.Foundation.FoundationContract", true, true) }, + { "System.Numerics.Vector2", new MappedType("Windows.Foundation.Numerics", "Vector2", "Windows.Foundation.FoundationContract", true, true) }, + { "System.Numerics.Vector3", new MappedType("Windows.Foundation.Numerics", "Vector3", "Windows.Foundation.FoundationContract", true, true) }, + { "System.Numerics.Vector4", new MappedType("Windows.Foundation.Numerics", "Vector4", "Windows.Foundation.FoundationContract", true, true) }, + { "System.Type", new MappedType(GetSystemTypeCustomMapping) }, + { "System.Collections.Generic.IEnumerable`1", new MappedType("Windows.Foundation.Collections", "IIterable`1", "Windows.Foundation.FoundationContract") }, + { "System.Collections.Generic.IEnumerator`1", new MappedType("Windows.Foundation.Collections", "IIterator`1", "Windows.Foundation.FoundationContract") }, + { "System.Collections.Generic.KeyValuePair`2", new MappedType("Windows.Foundation.Collections", "IKeyValuePair`2", "Windows.Foundation.FoundationContract") }, + { "System.Collections.Generic.IReadOnlyDictionary`2", new MappedType("Windows.Foundation.Collections", "IMapView`2", "Windows.Foundation.FoundationContract") }, + { "System.Collections.Generic.IDictionary`2", new MappedType("Windows.Foundation.Collections", "IMap`2", "Windows.Foundation.FoundationContract") }, + { "System.Collections.Generic.IReadOnlyList`1", new MappedType("Windows.Foundation.Collections", "IVectorView`1", "Windows.Foundation.FoundationContract") }, + { "System.Collections.Generic.IList`1", new MappedType("Windows.Foundation.Collections", "IVector`1", "Windows.Foundation.FoundationContract") }, + { "Windows.UI.Color", new MappedType("Windows.UI", "Color", "Windows.Foundation.UniversalApiContract", true, true) }, + }; + } +#pragma warning restore IDE0045 + } + + /// + /// Checks whether a mapping exists for the given fully-qualified type name. + /// + public bool HasMappingForType(string typeName) + { + return _typeMapping.ContainsKey(typeName); + } + + /// + /// Gets the mapped type for the given fully-qualified type name. + /// + public MappedType GetMappedType(string typeName) + { + return _typeMapping[typeName]; + } + + /// + /// The list of interfaces that are implemented by C# types but should not be mapped to WinRT interfaces. + /// + internal static readonly HashSet ImplementedInterfacesWithoutMapping = new(StringComparer.Ordinal) + { + "System.Collections.Generic.ICollection`1", + "System.Collections.Generic.IReadOnlyCollection`1", + "System.Collections.ICollection", + "System.Collections.IEnumerator", + "System.IEquatable`1", + "System.Runtime.InteropServices.ICustomQueryInterface", + "System.Runtime.InteropServices.IDynamicInterfaceCastable", + }; +} \ No newline at end of file diff --git a/src/WinRT.WinMD.Generator/Program.cs b/src/WinRT.WinMD.Generator/Program.cs new file mode 100644 index 0000000000..84fa701fda --- /dev/null +++ b/src/WinRT.WinMD.Generator/Program.cs @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using ConsoleAppFramework; +using WindowsRuntime.WinMDGenerator.Generation; + +// Run the WinMD generator with all parsed arguments +ConsoleApp.Run(args, WinMDGenerator.Run); \ No newline at end of file diff --git a/src/WinRT.WinMD.Generator/WinRT.WinMD.Generator.csproj b/src/WinRT.WinMD.Generator/WinRT.WinMD.Generator.csproj new file mode 100644 index 0000000000..bd46487da0 --- /dev/null +++ b/src/WinRT.WinMD.Generator/WinRT.WinMD.Generator.csproj @@ -0,0 +1,57 @@ + + + Exe + net10.0 + preview + enable + true + true + true + + + C#/WinRT WinMD Generator v$(VersionString) + C#/WinRT WinMD Generator v$(VersionString) + Copyright (c) Microsoft Corporation. All rights reserved. + $(AssemblyVersionNumber) + $(VersionNumber) + + + true + + + WindowsRuntime.WinMDGenerator + + + cswinrtwinmdgen + + + true + true + latest + latest-all + true + strict + true + + + true + win-$(BuildToolArch) + true + + + + + true + Speed + false + true + Guard + false + false + + + + + + + diff --git a/src/cswinrt.slnx b/src/cswinrt.slnx index f6abd71bfc..2d293329a4 100644 --- a/src/cswinrt.slnx +++ b/src/cswinrt.slnx @@ -165,7 +165,8 @@ - + + @@ -428,4 +429,5 @@ +