From 5ad0bafb7485bae7ea4bcf69abc48f592bb00d5a Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Mon, 23 Mar 2026 18:50:56 -0700 Subject: [PATCH 01/50] Initial WinMD generator port --- ...icrosoft.Windows.CsWinMD.Generator.targets | 95 + .../RunCsWinRTWinMDGenerator.cs | 159 ++ .../CommandLineArgumentNameAttribute.cs | 19 + .../Discovery/AssemblyAnalyzer.cs | 130 ++ .../Errors/WellKnownWinMDExceptions.cs | 52 + .../Generation/WinMDGenerator.cs | 108 ++ .../Generation/WinMDGeneratorArgs.Parsing.cs | 158 ++ .../Generation/WinMDGeneratorArgs.cs | 36 + .../Generation/WinmdWriter.cs | 1653 +++++++++++++++++ .../Models/TypeMapper.cs | 222 +++ src/WinRT.WinMD.Generator/Program.cs | 8 + .../Resolvers/PathAssemblyResolver.cs | 97 + .../WinRT.WinMD.Generator.csproj | 37 + src/cswinrt.slnx | 1 + 14 files changed, 2775 insertions(+) create mode 100644 nuget/Microsoft.Windows.CsWinMD.Generator.targets create mode 100644 src/WinRT.Generator.Tasks/RunCsWinRTWinMDGenerator.cs create mode 100644 src/WinRT.WinMD.Generator/Attributes/CommandLineArgumentNameAttribute.cs create mode 100644 src/WinRT.WinMD.Generator/Discovery/AssemblyAnalyzer.cs create mode 100644 src/WinRT.WinMD.Generator/Errors/WellKnownWinMDExceptions.cs create mode 100644 src/WinRT.WinMD.Generator/Generation/WinMDGenerator.cs create mode 100644 src/WinRT.WinMD.Generator/Generation/WinMDGeneratorArgs.Parsing.cs create mode 100644 src/WinRT.WinMD.Generator/Generation/WinMDGeneratorArgs.cs create mode 100644 src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs create mode 100644 src/WinRT.WinMD.Generator/Models/TypeMapper.cs create mode 100644 src/WinRT.WinMD.Generator/Program.cs create mode 100644 src/WinRT.WinMD.Generator/Resolvers/PathAssemblyResolver.cs create mode 100644 src/WinRT.WinMD.Generator/WinRT.WinMD.Generator.csproj diff --git a/nuget/Microsoft.Windows.CsWinMD.Generator.targets b/nuget/Microsoft.Windows.CsWinMD.Generator.targets new file mode 100644 index 000000000..e1eaeaa25 --- /dev/null +++ b/nuget/Microsoft.Windows.CsWinMD.Generator.targets @@ -0,0 +1,95 @@ + + + + + $(MSBuildThisFileDirectory)..\tools\WinRT.Generator.Tasks.dll + $(SolutionDir)WinRT.WinMD.Generator\bin\$(Configuration)\net10.0\ + + + false + + + $(IntermediateOutputPath) + <_CsWinRTWinMDOutputPath>$(CsWinRTWinMDOutputDirectory)$(AssemblyName).winmd + + + High + High + true + + + <_RunCsWinRTWinMDGeneratorPropertyInputsCachePath Condition="'$(_RunCsWinRTWinMDGeneratorPropertyInputsCachePath)' == ''">$(IntermediateOutputPath)$(MSBuildProjectName).cswinrtwinmdgen.cache + <_RunCsWinRTWinMDGeneratorPropertyInputsCachePath>$([MSBuild]::NormalizePath('$(MSBuildProjectDirectory)', '$(_RunCsWinRTWinMDGeneratorPropertyInputsCachePath)')) + + + + + + + <_RunCsWinRTWinMDGeneratorInputsCacheToHash Include="$(CsWinRTWinMDGenToolsDirectory)" /> + <_RunCsWinRTWinMDGeneratorInputsCacheToHash Include="$(CsWinRTUseWindowsUIXamlProjections)" /> + <_RunCsWinRTWinMDGeneratorInputsCacheToHash Include="$(AssemblyVersion)" /> + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/WinRT.Generator.Tasks/RunCsWinRTWinMDGenerator.cs b/src/WinRT.Generator.Tasks/RunCsWinRTWinMDGenerator.cs new file mode 100644 index 000000000..b76fc7f1c --- /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 000000000..5fe2632af --- /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; +} diff --git a/src/WinRT.WinMD.Generator/Discovery/AssemblyAnalyzer.cs b/src/WinRT.WinMD.Generator/Discovery/AssemblyAnalyzer.cs new file mode 100644 index 000000000..627092499 --- /dev/null +++ b/src/WinRT.WinMD.Generator/Discovery/AssemblyAnalyzer.cs @@ -0,0 +1,130 @@ +// 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) + { + if (!IsPublicType(type)) + { + continue; + } + + // We include classes, interfaces, structs, enums, and delegates + if (type.IsClass || type.IsInterface || type.IsValueType || type.IsEnum || IsDelegate(type)) + { + publicTypes.Add(type); + } + } + + return 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 delegate (inherits from System.MulticastDelegate). + /// + internal static bool IsDelegate(TypeDefinition type) + { + return type.BaseType?.FullName == "System.MulticastDelegate"; + } + + /// + /// Checks whether a type is a WinRT type (has the WindowsRuntimeTypeAttribute). + /// + internal static bool IsWinRTType(TypeDefinition type) + { + return type.CustomAttributes.Any( + attr => attr.Constructor?.DeclaringType?.Name?.Value == "WindowsRuntimeTypeAttribute"); + } + + /// + /// Gets the assembly name from a WindowsRuntimeTypeAttribute on a type, if present. + /// + internal static string? GetAssemblyForWinRTType(TypeDefinition type) + { + foreach (CustomAttribute attr in type.CustomAttributes) + { + if (attr.Constructor?.DeclaringType?.Name?.Value == "WindowsRuntimeTypeAttribute" && + attr.Signature?.FixedArguments.Count > 0) + { + return attr.Signature.FixedArguments[0].Element as string; + } + } + + return null; + } + + /// + /// Gets the full qualified name of a type, including generic arity. + /// + internal static string GetQualifiedName(TypeDefinition type) + { + string name = type.Name!.Value; + if (type.GenericParameters.Count > 0) + { + name += $"`{type.GenericParameters.Count}"; + } + + return type.Namespace is { Value: { Length: > 0 } ns } + ? $"{ns}.{name}" + : name; + } + + /// + /// Gets the full qualified name for an . + /// + internal static string GetQualifiedName(ITypeDefOrRef type) + { + string name = type.Name!.Value; + if (type is TypeDefinition td && td.GenericParameters.Count > 0) + { + name += $"`{td.GenericParameters.Count}"; + } + + return type.Namespace is { Value: { Length: > 0 } ns } + ? $"{ns}.{name}" + : name; + } +} diff --git a/src/WinRT.WinMD.Generator/Errors/WellKnownWinMDExceptions.cs b/src/WinRT.WinMD.Generator/Errors/WellKnownWinMDExceptions.cs new file mode 100644 index 000000000..c69b95473 --- /dev/null +++ b/src/WinRT.WinMD.Generator/Errors/WellKnownWinMDExceptions.cs @@ -0,0 +1,52 @@ +// 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 +{ + /// + /// Creates an exception for a response file read error. + /// + public static Exception ResponseFileReadError(Exception inner) + { + return new InvalidOperationException("Failed to read the response file.", inner); + } + + /// + /// Creates an exception for a malformed response file. + /// + public static Exception MalformedResponseFile() + { + return new InvalidOperationException("The response file is malformed."); + } + + /// + /// Creates an exception for a response file argument parsing error. + /// + public static Exception ResponseFileArgumentParsingError(string propertyName, Exception? inner = null) + { + return new InvalidOperationException($"Failed to parse the '{propertyName}' argument from the response file.", inner); + } + + /// + /// Creates an exception for a WinMD generation error. + /// + public static Exception WinMDGenerationError(Exception inner) + { + return new InvalidOperationException("Failed to generate the WinMD file.", inner); + } + + /// + /// Creates an exception for a WinMD write error. + /// + public static Exception WinMDWriteError(Exception inner) + { + return new InvalidOperationException("Failed to write the WinMD file.", inner); + } +} diff --git a/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.cs b/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.cs new file mode 100644 index 000000000..eab9bd40a --- /dev/null +++ b/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.cs @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using AsmResolver.DotNet; +using ConsoleAppFramework; +using WindowsRuntime.WinMDGenerator.Discovery; +using WindowsRuntime.WinMDGenerator.Errors; +using WindowsRuntime.WinMDGenerator.Models; +using WindowsRuntime.WinMDGenerator.Resolvers; + +namespace WindowsRuntime.WinMDGenerator.Generation; + +/// +/// The implementation of the CsWinRT WinMD generator. +/// +internal static 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; + + // Parse the actual arguments from the response file + try + { + args = WinMDGeneratorArgs.ParseFromResponseFile(inputFilePath, token); + } + catch (Exception e) + { + ConsoleApp.Log($"Error parsing response file: {e.Message}"); + throw; + } + + token.ThrowIfCancellationRequested(); + + ConsoleApp.Log($"Generating WinMD for assembly: {Path.GetFileName(args.InputAssemblyPath)}"); + ConsoleApp.Log($"Output: {args.OutputWinmdPath}"); + + // Phase 1: Load the input assembly using AsmResolver + ModuleDefinition inputModule; + + try + { + string[] allReferencePaths = [args.InputAssemblyPath, .. args.ReferenceAssemblyPaths]; + PathAssemblyResolver assemblyResolver = new(allReferencePaths); + inputModule = ModuleDefinition.FromFile(args.InputAssemblyPath, assemblyResolver.ReaderParameters); + } + catch (Exception e) + { + ConsoleApp.Log($"Error loading input assembly: {e.Message}"); + throw WellKnownWinMDExceptions.WinMDGenerationError(e); + } + + token.ThrowIfCancellationRequested(); + + // Phase 2: Discover public types + AssemblyAnalyzer analyzer = new(inputModule); + IReadOnlyList publicTypes = analyzer.DiscoverPublicTypes(); + + ConsoleApp.Log($"Found {publicTypes.Count} public types"); + + token.ThrowIfCancellationRequested(); + + // Phase 3: Generate the WinMD + try + { + string assemblyName = analyzer.AssemblyName; + TypeMapper mapper = new(args.UseWindowsUIXamlProjections); + + WinmdWriter writer = new( + assemblyName, + args.AssemblyVersion, + mapper, + inputModule); + + foreach (TypeDefinition type in 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); + + ConsoleApp.Log($"WinMD generated successfully: {args.OutputWinmdPath}"); + } + catch (Exception e) + { + ConsoleApp.Log($"Error generating WinMD: {e.Message}"); + throw WellKnownWinMDExceptions.WinMDGenerationError(e); + } + } +} 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 000000000..4101754f4 --- /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); + } +} diff --git a/src/WinRT.WinMD.Generator/Generation/WinMDGeneratorArgs.cs b/src/WinRT.WinMD.Generator/Generation/WinMDGeneratorArgs.cs new file mode 100644 index 000000000..1efa197a5 --- /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; } +} diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs new file mode 100644 index 000000000..b638156a4 --- /dev/null +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs @@ -0,0 +1,1653 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using AsmResolver.PE.DotNet.Metadata.Tables; +using WindowsRuntime.WinMDGenerator.Discovery; +using WindowsRuntime.WinMDGenerator.Errors; +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; +using FieldAttributes = AsmResolver.PE.DotNet.Metadata.Tables.FieldAttributes; +using TypeAttributes = AsmResolver.PE.DotNet.Metadata.Tables.TypeAttributes; +using AssemblyAttributes = AsmResolver.PE.DotNet.Metadata.Tables.AssemblyAttributes; + +namespace WindowsRuntime.WinMDGenerator.Generation; + +/// +/// Writes a WinMD file from analyzed assembly types using AsmResolver. +/// +internal sealed class WinmdWriter +{ + private readonly string _assemblyName; + private readonly string _version; + private readonly TypeMapper _mapper; + private readonly ModuleDefinition _inputModule; + + // 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) + { + _assemblyName = assemblyName; + _version = version; + _mapper = mapper; + _inputModule = inputModule; + + // Create the output WinMD module + _outputModule = new ModuleDefinition(assemblyName + ".winmd"); + + // Create the output assembly with WindowsRuntime flag (keep reference alive via module) + _ = new AssemblyDefinition(assemblyName, new Version(version)) + { + Modules = { _outputModule }, + Attributes = AssemblyAttributes.ContentWindowsRuntime // WindowsRuntime + }; + + // Add the type + _outputModule.TopLevelTypes.Add(new TypeDefinition(null, "", 0)); + } + + /// + /// 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 (AssemblyAnalyzer.IsDelegate(inputType)) + { + AddDelegateType(inputType); + } + else if (inputType.IsInterface) + { + AddInterfaceType(inputType); + } + else if (inputType.IsValueType && !inputType.IsEnum) + { + 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 + foreach ((string qualifiedName, TypeDeclaration declaration) in _typeDefinitionMapping) + { + if (declaration.OutputType == null || declaration.InputType == null || !declaration.IsComponentType) + { + continue; + } + + TypeDefinition classOutputType = declaration.OutputType; + TypeDefinition classInputType = declaration.InputType; + + // Add MethodImpls for implemented interfaces + foreach (InterfaceImplementation classInterfaceImpl in classOutputType.Interfaces) + { + if (classInterfaceImpl.Interface == null) + { + continue; + } + + TypeDefinition? interfaceDef = classInterfaceImpl.Interface.Resolve(); + if (interfaceDef == null) + { + continue; + } + + foreach (MethodDefinition interfaceMethod in interfaceDef.Methods) + { + // Find the corresponding method on the class + MethodDefinition? classMethod = FindMatchingMethod(classOutputType, interfaceMethod); + if (classMethod != null) + { + MemberReference interfaceMethodRef = new(classInterfaceImpl.Interface, interfaceMethod.Name!.Value, interfaceMethod.Signature); + classOutputType.MethodImplementations.Add(new MethodImplementation(classMethod, interfaceMethodRef)); + } + } + } + + // Add MethodImpls for default synthesized interface + if (declaration.DefaultInterface != null && + _typeDefinitionMapping.TryGetValue(declaration.DefaultInterface, out TypeDeclaration? defaultInterfaceDecl) && + defaultInterfaceDecl.OutputType != null) + { + TypeDefinition defaultInterface = defaultInterfaceDecl.OutputType; + ITypeDefOrRef interfaceRef = GetOrCreateTypeReference( + defaultInterface.Namespace?.Value ?? "", + defaultInterface.Name!.Value, + _assemblyName); + + foreach (MethodDefinition interfaceMethod in defaultInterface.Methods) + { + MethodDefinition? classMethod = FindMatchingMethod(classOutputType, interfaceMethod); + if (classMethod != null) + { + MemberReference interfaceMethodRef = new(interfaceRef, interfaceMethod.Name!.Value, interfaceMethod.Signature); + classOutputType.MethodImplementations.Add(new MethodImplementation(classMethod, interfaceMethodRef)); + } + } + } + } + + // Phase 2: Add default version attributes for types that don't have one + int defaultVersion = Version.Parse(_version).Major; + + foreach ((string _, TypeDeclaration declaration) in _typeDefinitionMapping) + { + if (declaration.OutputType == null) + { + continue; + } + + if (!HasVersionAttribute(declaration.OutputType)) + { + AddVersionAttribute(declaration.OutputType, defaultVersion); + } + } + + // Phase 3: Add custom attributes from input types to output types + foreach ((string _, TypeDeclaration declaration) in _typeDefinitionMapping) + { + if (declaration.OutputType == null || declaration.InputType == null || !declaration.IsComponentType) + { + continue; + } + + AddCustomAttributesFromInput(declaration.InputType, declaration.OutputType); + } + + // Phase 4: Add overload attributes for methods with the same name + foreach ((string _, TypeDeclaration declaration) in _typeDefinitionMapping) + { + if (declaration.OutputType == null) + { + continue; + } + + AddOverloadAttributesForType(declaration.OutputType); + } + } + + private static MethodDefinition? FindMatchingMethod(TypeDefinition classType, MethodDefinition interfaceMethod) + { + 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; + } + + // Name and parameter count matches, match found. + return classMethod; + } + + return null; + } + + 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); + } + } + + #region Enum Types + + private void AddEnumType(TypeDefinition inputType) + { + 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( + inputType.Namespace?.Value, + 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); + + // 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(underlyingType)); + + if (field.Constant != null) + { + outputField.Constant = new Constant(field.Constant.Type, new DataBlobSignature(field.Constant.Value!.Data)); + } + + outputType.Fields.Add(outputField); + } + + _outputModule.TopLevelTypes.Add(outputType); + + TypeDeclaration declaration = new(inputType, outputType, isComponentType: true); + _typeDefinitionMapping[qualifiedName] = declaration; + } + + 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; + } + + #endregion + + #region Delegate Types + + 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( + inputType.Namespace?.Value, + inputType.Name!.Value, + typeAttributes, + baseType); + + // Add .ctor(object, IntPtr) + MethodDefinition ctor = new( + ".ctor", + MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RuntimeSpecialName, + MethodSignature.CreateInstance( + _outputModule.CorLibTypeFactory.Void, + _outputModule.CorLibTypeFactory.Object, + _outputModule.CorLibTypeFactory.IntPtr)) + { + ImplAttributes = MethodImplAttributes.Runtime | MethodImplAttributes.Managed + }; + 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 + int paramIndex = 1; + foreach (ParameterDefinition inputParam in inputInvoke.ParameterDefinitions) + { + invoke.ParameterDefinitions.Add(new ParameterDefinition( + (ushort)paramIndex++, + inputParam.Name!.Value, + inputParam.Attributes)); + } + + outputType.Methods.Add(invoke); + } + + _outputModule.TopLevelTypes.Add(outputType); + + TypeDeclaration declaration = new(inputType, outputType, isComponentType: true); + _typeDefinitionMapping[qualifiedName] = declaration; + + // Add GUID attribute + AddGuidAttribute(outputType, inputType); + } + + #endregion + + #region Interface Types + + 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( + inputType.Namespace?.Value, + inputType.Name!.Value, + typeAttributes); + + // Add methods + foreach (MethodDefinition method in inputType.Methods) + { + if (!method.IsPublic && method.IsPrivate) + { + 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 = ImportTypeReference(impl.Interface); + outputType.Interfaces.Add(new InterfaceImplementation(outputInterfaceRef)); + } + } + + _outputModule.TopLevelTypes.Add(outputType); + + TypeDeclaration declaration = new(inputType, outputType, isComponentType: true); + _typeDefinitionMapping[qualifiedName] = declaration; + + // Add GUID attribute + AddGuidAttribute(outputType, inputType); + } + + #endregion + + #region Struct Types + + 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( + inputType.Namespace?.Value, + inputType.Name!.Value, + typeAttributes, + baseType); + + // 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); + } + + _outputModule.TopLevelTypes.Add(outputType); + + TypeDeclaration declaration = new(inputType, outputType, isComponentType: true); + _typeDefinitionMapping[qualifiedName] = declaration; + } + + #endregion + + #region Class Types + + 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; + + if (inputType.IsSealed || inputType.IsAbstract) + { + typeAttributes |= TypeAttributes.Sealed; + } + + if (inputType.IsAbstract && !inputType.IsSealed) + { + // Static class in C# is abstract sealed + 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 = inputType.BaseType.Resolve(); + baseType = baseTypeDef != null && baseTypeDef.IsAbstract + ? GetOrCreateTypeReference("System", "Object", "mscorlib") + : ImportTypeReference(inputType.BaseType); + } + else + { + baseType = GetOrCreateTypeReference("System", "Object", "mscorlib"); + } + + TypeDefinition outputType = new( + inputType.Namespace?.Value, + inputType.Name!.Value, + typeAttributes, + baseType); + + bool hasConstructor = false; + bool hasDefaultConstructor = false; + bool hasAtLeastOneNonPublicConstructor = false; + bool isStaticClass = inputType.IsAbstract && inputType.IsSealed; + + // 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) + { + AddMethodToClass(outputType, method); + } + } + + // Add properties + foreach (PropertyDefinition property in inputType.Properties) + { + // 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) + { + 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 + foreach (InterfaceImplementation impl in inputType.Interfaces) + { + if (impl.Interface != null && IsPubliclyAccessible(impl.Interface)) + { + ITypeDefOrRef outputInterfaceRef = ImportTypeReference(impl.Interface); + outputType.Interfaces.Add(new InterfaceImplementation(outputInterfaceRef)); + } + } + + _outputModule.TopLevelTypes.Add(outputType); + + TypeDeclaration declaration = new(inputType, outputType, isComponentType: true); + _typeDefinitionMapping[qualifiedName] = declaration; + + // 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); + } + + #endregion + + #region Synthesized Interfaces + + 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) + { + // TODO: bool isStaticClass = inputType.IsAbstract && inputType.IsSealed; + + // Collect members that come from interface implementations + HashSet membersFromInterfaces = []; + foreach (InterfaceImplementation impl in inputType.Interfaces) + { + TypeDefinition? interfaceDef = impl.Interface?.Resolve(); + if (interfaceDef == null) + { + continue; + } + + foreach (MethodDefinition interfaceMethod in interfaceDef.Methods) + { + // Find the class member that implements this interface member + string methodName = interfaceMethod.Name?.Value ?? ""; + _ = membersFromInterfaces.Add(methodName); + } + } + + 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 = inputType.Namespace?.Value ?? ""; + 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)) + { + 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)) + { + 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 + TypeReference interfaceRef = GetOrCreateTypeReference(ns, interfaceName, _assemblyName); + InterfaceImplementation interfaceImpl = new(interfaceRef); + 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) + { + TypeSignature returnType = ImportTypeReference(classType).ToTypeSignature(); + + 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 names + int paramIndex = 1; + foreach (ParameterDefinition inputParam in constructor.ParameterDefinitions) + { + factoryMethod.ParameterDefinitions.Add(new ParameterDefinition( + (ushort)paramIndex++, + inputParam.Name!.Value, + inputParam.Attributes)); + } + + synthesizedInterface.Methods.Add(factoryMethod); + } + + #endregion + + #region Custom Mapped Interfaces + + /// + /// Processes custom mapped interfaces for a class type. This maps .NET collection interfaces, + /// IDisposable, INotifyPropertyChanged, etc. to their WinRT equivalents. + /// + private void ProcessCustomMappedInterfaces(TypeDefinition inputType, TypeDefinition outputType) + { + foreach (InterfaceImplementation impl in inputType.Interfaces) + { + if (impl.Interface == null) + { + continue; + } + + string interfaceName = GetInterfaceQualifiedName(impl.Interface); + + if (!_mapper.HasMappingForType(interfaceName)) + { + continue; + } + + MappedType mapping = _mapper.GetMappedType(interfaceName); + (string mappedNs, string mappedName, string mappedAssembly, _, _) = mapping.GetMapping(); + + // Add the mapped interface as an implementation on the output type + TypeReference mappedInterfaceRef = GetOrCreateTypeReference(mappedNs, mappedName, mappedAssembly); + + // For generic interfaces, we need to handle type arguments + if (impl.Interface is TypeSpecification typeSpec && typeSpec.Signature is GenericInstanceTypeSignature genericInst) + { + TypeSignature[] mappedArgs = [.. genericInst.TypeArguments + .Select(MapTypeSignatureToOutput)]; + TypeSpecification mappedSpec = new(new GenericInstanceTypeSignature(mappedInterfaceRef, false, mappedArgs)); + outputType.Interfaces.Add(new InterfaceImplementation(mappedSpec)); + } + else + { + outputType.Interfaces.Add(new InterfaceImplementation(mappedInterfaceRef)); + } + } + } + + private static string GetInterfaceQualifiedName(ITypeDefOrRef type) + { + return type is TypeSpecification typeSpec && typeSpec.Signature is GenericInstanceTypeSignature genericInst + ? AssemblyAnalyzer.GetQualifiedName(genericInst.GenericType) + : AssemblyAnalyzer.GetQualifiedName(type); + } + + #endregion + + #region Method/Property/Event Helpers + + 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 + int paramIndex = 1; + foreach (ParameterDefinition inputParam in inputMethod.ParameterDefinitions) + { + outputMethod.ParameterDefinitions.Add(new ParameterDefinition( + (ushort)paramIndex++, + inputParam.Name!.Value, + inputParam.Attributes)); + } + + outputType.Methods.Add(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 + int paramIndex = 1; + foreach (ParameterDefinition inputParam in inputMethod.ParameterDefinitions) + { + outputMethod.ParameterDefinitions.Add(new ParameterDefinition( + (ushort)paramIndex++, + inputParam.Name!.Value, + inputParam.Attributes)); + } + + outputType.Methods.Add(outputMethod); + } + + private void AddPropertyToType(TypeDefinition outputType, PropertyDefinition inputProperty, bool isInterfaceParent) + { + TypeSignature propertyType = MapTypeSignatureToOutput(inputProperty.Signature!.ReturnType); + + PropertyDefinition outputProperty = new( + inputProperty.Name!.Value, + 0, + PropertySignature.CreateInstance(propertyType)); + + bool isStatic = inputProperty.GetMethod?.IsStatic == true || inputProperty.SetMethod?.IsStatic == true; + + // 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); + } + + 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); + + bool isStatic = 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(); + TypeSignature tokenSig = eventRegistrationTokenType.ToTypeSignature(); + + 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(); + + 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); + } + + #endregion + + #region Type Mapping + + /// + /// 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) + { + ITypeDefOrRef importedType = ImportTypeReference(genericInst.GenericType); + TypeSignature[] mappedArgs = [.. genericInst.TypeArguments + .Select(MapTypeSignatureToOutput)]; + return new GenericInstanceTypeSignature(importedType, genericInst.IsValueType, mappedArgs); + } + + // 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) + { + 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; + } + + // Otherwise create a type reference + return GetOrCreateTypeReference( + typeDef.Namespace?.Value ?? "", + typeDef.Name!.Value, + _inputModule.Assembly?.Name?.Value ?? "mscorlib"); + } + + if (type is TypeReference typeRef) + { + string ns = typeRef.Namespace?.Value ?? ""; + string name = typeRef.Name!.Value; + string assembly = GetAssemblyNameFromScope(typeRef.Scope); + 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" + }; + } + + #endregion + + #region Type References + + 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; + } + + #endregion + + #region Attributes + + 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 string guidString && + Guid.TryParse(guidString, 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 + using (SHA1 sha = SHA1.Create()) + { + byte[] hash = sha.ComputeHash(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(), + _outputModule.CorLibTypeFactory.UInt32)); + + CustomAttributeSignature sig = new(); + sig.FixedArguments.Add(new CustomAttributeArgument(systemType.ToTypeSignature(), 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(), + _outputModule.CorLibTypeFactory.UInt32)); + + CustomAttributeSignature sig = new(); + sig.FixedArguments.Add(new CustomAttributeArgument(systemType.ToTypeSignature(), 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())); + + CustomAttributeSignature sig = new(); + sig.FixedArguments.Add(new CustomAttributeArgument(systemType.ToTypeSignature(), 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 == "VersionAttribute"); + } + + 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; + } + + private static void AddCustomAttributesFromInput(TypeDefinition inputType, TypeDefinition outputType) + { + // Currently unused but reserved for future custom attribute encoding + _ = outputType; + + foreach (CustomAttribute attr in inputType.CustomAttributes) + { + string? attrTypeName = attr.Constructor?.DeclaringType?.FullName; + + // Skip attributes that shouldn't be in the WinMD + if (attrTypeName is "System.Runtime.InteropServices.GuidAttribute" or + "WinRT.GeneratedBindableCustomPropertyAttribute" or + "System.Runtime.CompilerServices.NullableAttribute" or + "System.Runtime.CompilerServices.NullableContextAttribute") + { + continue; + } + + // For now, skip complex attributes — they require deeper encoding logic + // TODO: Port full custom attribute encoding in a later phase + } + } + + #endregion + + #region Helpers + + 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); + + // 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); + } + + #endregion +} + +/// +/// 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; + } +} diff --git a/src/WinRT.WinMD.Generator/Models/TypeMapper.cs b/src/WinRT.WinMD.Generator/Models/TypeMapper.cs new file mode 100644 index 000000000..841d0c811 --- /dev/null +++ b/src/WinRT.WinMD.Generator/Models/TypeMapper.cs @@ -0,0 +1,222 @@ +// 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.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") }, + { "WinRT.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.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") }, + { "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") }, + { "System.ComponentModel.INotifyDataErrorInfo", new MappedType("Microsoft.UI.Xaml.Data", "INotifyDataErrorInfo", "Microsoft.UI") }, + { "System.ComponentModel.INotifyPropertyChanged", new MappedType("Microsoft.UI.Xaml.Data", "INotifyPropertyChanged", "Microsoft.UI") }, + { "System.ComponentModel.PropertyChangedEventArgs", new MappedType("Microsoft.UI.Xaml.Data", "PropertyChangedEventArgs", "Microsoft.UI") }, + { "System.ComponentModel.PropertyChangedEventHandler", new MappedType("Microsoft.UI.Xaml.Data", "PropertyChangedEventHandler", "Microsoft.UI") }, + { "System.Windows.Input.ICommand", new MappedType("Microsoft.UI.Xaml.Input", "ICommand", "Microsoft.UI") }, + { "System.Collections.IEnumerable", new MappedType("Microsoft.UI.Xaml.Interop", "IBindableIterable", "Microsoft.UI") }, + { "System.Collections.IList", new MappedType("Microsoft.UI.Xaml.Interop", "IBindableVector", "Microsoft.UI") }, + { "System.Collections.Specialized.INotifyCollectionChanged", new MappedType("Microsoft.UI.Xaml.Interop", "INotifyCollectionChanged", "Microsoft.UI") }, + { "System.Collections.Specialized.NotifyCollectionChangedAction", new MappedType("Microsoft.UI.Xaml.Interop", "NotifyCollectionChangedAction", "Microsoft.UI", true, true) }, + { "System.Collections.Specialized.NotifyCollectionChangedEventArgs", new MappedType("Microsoft.UI.Xaml.Interop", "NotifyCollectionChangedEventArgs", "Microsoft.UI") }, + { "System.Collections.Specialized.NotifyCollectionChangedEventHandler", new MappedType("Microsoft.UI.Xaml.Interop", "NotifyCollectionChangedEventHandler", "Microsoft.UI") }, + { "WinRT.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", + "WinRT.IWinRTObject" + }; +} diff --git a/src/WinRT.WinMD.Generator/Program.cs b/src/WinRT.WinMD.Generator/Program.cs new file mode 100644 index 000000000..04c9b9afb --- /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); diff --git a/src/WinRT.WinMD.Generator/Resolvers/PathAssemblyResolver.cs b/src/WinRT.WinMD.Generator/Resolvers/PathAssemblyResolver.cs new file mode 100644 index 000000000..8adc5a32d --- /dev/null +++ b/src/WinRT.WinMD.Generator/Resolvers/PathAssemblyResolver.cs @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Concurrent; +using System.IO; +using System.Linq; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Serialized; +using AsmResolver.DotNet.Signatures; + +namespace WindowsRuntime.WinMDGenerator.Resolvers; + +/// +/// A custom from a specific set of reference paths. +/// +internal sealed class PathAssemblyResolver : IAssemblyResolver +{ + /// + /// The input .dll paths to load assemblies from. + /// + private readonly string[] _referencePaths; + + /// + /// The cached assemblies. + /// + private readonly ConcurrentDictionary _cache = new(new SignatureComparer()); + + /// + /// Creates a new instance with the specified parameters. + /// + /// The input .dll paths. + public PathAssemblyResolver(string[] referencePaths) + { + _referencePaths = referencePaths; + ReaderParameters = new RuntimeContext(new DotNetRuntimeInfo(".NETCoreApp", new Version(10, 0)), this).DefaultReaderParameters; + } + + /// + /// Gets the instance to use. + /// + public ModuleReaderParameters ReaderParameters { get; } + + /// + public AssemblyDefinition? Resolve(AssemblyDescriptor assembly) + { + // If we already have the assembly in the cache, return it + if (_cache.TryGetValue(assembly, out AssemblyDefinition? cachedDefinition)) + { + return cachedDefinition; + } + + // We can't load an assembly without a name + if (assembly.Name is null) + { + return null; + } + + // Find the first match in our list of reference paths, and load that assembly + foreach (string path in _referencePaths) + { + if (Path.GetFileNameWithoutExtension(path.AsSpan()).SequenceEqual(assembly.Name)) + { + return _cache.GetOrAdd( + key: assembly, + valueFactory: static (_, args) => AssemblyDefinition.FromFile(args.Path, args.Parameters), + factoryArgument: (Path: path, Parameters: ReaderParameters)); + } + } + + return null; + } + + /// + public void AddToCache(AssemblyDescriptor descriptor, AssemblyDefinition definition) + { + _ = _cache.TryAdd(descriptor, definition); + } + + /// + public bool RemoveFromCache(AssemblyDescriptor descriptor) + { + return _cache.TryRemove(descriptor, out _); + } + + /// + public bool HasCached(AssemblyDescriptor descriptor) + { + return _cache.ContainsKey(descriptor); + } + + /// + public void ClearCache() + { + _cache.Clear(); + } +} 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 000000000..8367af499 --- /dev/null +++ b/src/WinRT.WinMD.Generator/WinRT.WinMD.Generator.csproj @@ -0,0 +1,37 @@ + + + Exe + net10.0 + 14.0 + enable + true + true + true + true + Speed + false + + + true + + + WindowsRuntime.WinMDGenerator + + + cswinrtwinmdgen + + + true + true + latest + latest-all + true + strict + true + + + + + + + diff --git a/src/cswinrt.slnx b/src/cswinrt.slnx index f6abd71bf..f020a72c7 100644 --- a/src/cswinrt.slnx +++ b/src/cswinrt.slnx @@ -428,4 +428,5 @@ + From 8ce814f35344984595eee20a089593bf007656c5 Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Mon, 23 Mar 2026 19:13:57 -0700 Subject: [PATCH 02/50] Implement custom attribute copying and fix static class handling in WinMD generator - Fix static class type attributes: abstract-only classes no longer incorrectly get Sealed flag; static classes (abstract+sealed) now correctly get both Sealed and Abstract flags, matching the original source generator behavior - Implement full custom attribute copying from input types/members to WinMD output, replacing the previous stub with CopyCustomAttributes that: - Filters out GuidAttribute, GeneratedBindableCustomProperty, VersionAttribute (handled separately), and System.Runtime.CompilerServices.* attributes - Skips non-public attribute types - Imports constructor references with type remapping - Clones attribute signatures including fixed args, named args, and arrays - Handles Type-valued elements via TypeSignature remapping - Copy custom attributes at all member creation points: methods (class and interface), properties, and events - Remove both TODO comments (custom attribute encoding and static class detection) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generation/WinmdWriter.cs | 170 ++++++++++++++++-- 1 file changed, 152 insertions(+), 18 deletions(-) diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs index b638156a4..c9f198afd 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs @@ -192,7 +192,7 @@ public void FinalizeGeneration() continue; } - AddCustomAttributesFromInput(declaration.InputType, declaration.OutputType); + CopyCustomAttributes(declaration.InputType, declaration.OutputType); } // Phase 4: Add overload attributes for methods with the same name @@ -557,14 +557,17 @@ private void AddClassType(TypeDefinition inputType) TypeAttributes.Class | TypeAttributes.BeforeFieldInit; - if (inputType.IsSealed || inputType.IsAbstract) + // 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; } - if (inputType.IsAbstract && !inputType.IsSealed) + // In C#, static classes are both abstract and sealed in metadata + if (inputType.IsAbstract && inputType.IsSealed) { - // Static class in C# is abstract sealed typeAttributes |= TypeAttributes.Abstract; } @@ -705,7 +708,7 @@ private static string GetSynthesizedInterfaceName(string className, SynthesizedI private void AddSynthesizedInterfaces(TypeDefinition inputType, TypeDefinition classOutputType, TypeDeclaration classDeclaration) { - // TODO: bool isStaticClass = inputType.IsAbstract && inputType.IsSealed; + // Static vs non-static member filtering is handled below per-member // Collect members that come from interface implementations HashSet membersFromInterfaces = []; @@ -986,6 +989,9 @@ private void AddMethodToInterface(TypeDefinition outputType, MethodDefinition in } outputType.Methods.Add(outputMethod); + + // Copy custom attributes from the input method + CopyCustomAttributes(inputMethod, outputMethod); } private void AddMethodToClass(TypeDefinition outputType, MethodDefinition inputMethod) @@ -1041,6 +1047,9 @@ private void AddMethodToClass(TypeDefinition outputType, MethodDefinition inputM } outputType.Methods.Add(outputMethod); + + // Copy custom attributes from the input method + CopyCustomAttributes(inputMethod, outputMethod); } private void AddPropertyToType(TypeDefinition outputType, PropertyDefinition inputProperty, bool isInterfaceParent) @@ -1119,6 +1128,9 @@ private void AddPropertyToType(TypeDefinition outputType, PropertyDefinition inp } outputType.Properties.Add(outputProperty); + + // Copy custom attributes from the input property + CopyCustomAttributes(inputProperty, outputProperty); } private void AddEventToType(TypeDefinition outputType, EventDefinition inputEvent, bool isInterfaceParent) @@ -1197,6 +1209,9 @@ private void AddEventToType(TypeDefinition outputType, EventDefinition inputEven } outputType.Events.Add(outputEvent); + + // Copy custom attributes from the input event + CopyCustomAttributes(inputEvent, outputEvent); } #endregion @@ -1567,27 +1582,146 @@ private int GetVersion(TypeDefinition type) return Version.Parse(_version).Major; } - private static void AddCustomAttributesFromInput(TypeDefinition inputType, TypeDefinition outputType) + /// + /// 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) { - // Currently unused but reserved for future custom attribute encoding - _ = outputType; - - foreach (CustomAttribute attr in inputType.CustomAttributes) + foreach (CustomAttribute attr in source.CustomAttributes) { - string? attrTypeName = attr.Constructor?.DeclaringType?.FullName; + if (!ShouldCopyAttribute(attr)) + { + continue; + } - // Skip attributes that shouldn't be in the WinMD - if (attrTypeName is "System.Runtime.InteropServices.GuidAttribute" or - "WinRT.GeneratedBindableCustomPropertyAttribute" or - "System.Runtime.CompilerServices.NullableAttribute" or - "System.Runtime.CompilerServices.NullableContextAttribute") + MemberReference? importedCtor = ImportAttributeConstructor(attr.Constructor); + if (importedCtor == null) { continue; } - // For now, skip complex attributes — they require deeper encoding logic - // TODO: Port full custom attribute encoding in a later phase + 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) + { + 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 + "WinRT.GeneratedBindableCustomPropertyAttribute" or + "Windows.Foundation.Metadata.VersionAttribute") + { + 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) + TypeDefinition? attrTypeDef = attr.Constructor?.DeclaringType?.Resolve(); + if (attrTypeDef != null && !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); + + TypeSignature[] mappedParams = [.. methodSig.ParameterTypes + .Select(MapTypeSignatureToOutput)]; + + MethodSignature importedSig = MethodSignature.CreateInstance( + _outputModule.CorLibTypeFactory.Void, + mappedParams); + + 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; + } + + /// + /// Clones a single custom attribute argument, remapping type references. + /// + private CustomAttributeArgument CloneAttributeArgument(CustomAttributeArgument arg) + { + TypeSignature mappedType = MapTypeSignatureToOutput(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; } #endregion From 07c6ab4d19c53a70ad85ea57551199643eab4ac4 Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Mon, 23 Mar 2026 20:32:27 -0700 Subject: [PATCH 03/50] Fix interface filtering, method visibility check, and MethodImpl matching - Filter class interface implementations: skip interfaces that have a WinRT mapping (added separately by ProcessCustomMappedInterfaces) and interfaces in ImplementedInterfacesWithoutMapping (ICollection, IEnumerator, etc.) to avoid duplicate and spurious interfaces in the WinMD output - Fix interface method filter: change '!IsPublic && IsPrivate' to '!IsPublic' so internal/protected methods are correctly excluded from WinMD interfaces - Improve FindMatchingMethod to compare parameter types (via FullName) in addition to name and count, preventing incorrect MethodImpl records for overloaded methods with different parameter types Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generation/WinmdWriter.cs | 44 ++++++++++++++++--- 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs index c9f198afd..b0226c704 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs @@ -224,7 +224,22 @@ public void FinalizeGeneration() continue; } - // Name and parameter count matches, match found. + // Match parameter types + bool parametersMatch = true; + for (int i = 0; i < (classMethod.Signature?.ParameterTypes.Count ?? 0); i++) + { + if (classMethod.Signature!.ParameterTypes[i].FullName != interfaceMethod.Signature!.ParameterTypes[i].FullName) + { + parametersMatch = false; + break; + } + } + + if (!parametersMatch) + { + continue; + } + return classMethod; } @@ -458,7 +473,7 @@ private void AddInterfaceType(TypeDefinition inputType) // Add methods foreach (MethodDefinition method in inputType.Methods) { - if (!method.IsPublic && method.IsPrivate) + if (!method.IsPublic) { continue; } @@ -655,14 +670,31 @@ private void AddClassType(TypeDefinition inputType) hasDefaultConstructor = true; } - // Add interface implementations + // Add interface implementations (excluding mapped and unmappable interfaces) foreach (InterfaceImplementation impl in inputType.Interfaces) { - if (impl.Interface != null && IsPubliclyAccessible(impl.Interface)) + if (impl.Interface == null || !IsPubliclyAccessible(impl.Interface)) { - ITypeDefOrRef outputInterfaceRef = ImportTypeReference(impl.Interface); - outputType.Interfaces.Add(new InterfaceImplementation(outputInterfaceRef)); + 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 = ImportTypeReference(impl.Interface); + outputType.Interfaces.Add(new InterfaceImplementation(outputInterfaceRef)); } _outputModule.TopLevelTypes.Add(outputType); From 427903301ead36f8a102f956407022c301d4c238 Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Mon, 23 Mar 2026 20:38:47 -0700 Subject: [PATCH 04/50] Handle nested public types in WinMD generation The original source generator discovers nested types via recursive syntax walking. The port only walked TopLevelTypes, missing nested public types. - DiscoverPublicTypes now recursively walks TypeDefinition.NestedTypes - Add GetEffectiveNamespace helper that traverses the declaring type chain to find the namespace for nested types (which have null namespace in ECMA-335 metadata) - Update GetQualifiedName to use GetEffectiveNamespace so nested types get correct qualified names (e.g. 'MyNs.Inner' instead of just 'Inner') - Update all Add*Type methods in WinmdWriter to use GetEffectiveNamespace instead of inputType.Namespace directly Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Discovery/AssemblyAnalyzer.cs | 64 +++++++++++++++---- .../Generation/WinmdWriter.cs | 12 ++-- 2 files changed, 59 insertions(+), 17 deletions(-) diff --git a/src/WinRT.WinMD.Generator/Discovery/AssemblyAnalyzer.cs b/src/WinRT.WinMD.Generator/Discovery/AssemblyAnalyzer.cs index 627092499..931e5889a 100644 --- a/src/WinRT.WinMD.Generator/Discovery/AssemblyAnalyzer.cs +++ b/src/WinRT.WinMD.Generator/Discovery/AssemblyAnalyzer.cs @@ -39,21 +39,35 @@ public IReadOnlyList DiscoverPublicTypes() foreach (TypeDefinition type in _inputModule.TopLevelTypes) { - if (!IsPublicType(type)) - { - continue; - } - - // We include classes, interfaces, structs, enums, and delegates - if (type.IsClass || type.IsInterface || type.IsValueType || type.IsEnum || IsDelegate(type)) - { - publicTypes.Add(type); - } + 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 || IsDelegate(type)) + { + 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). /// @@ -96,8 +110,35 @@ internal static bool IsWinRTType(TypeDefinition type) 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) { @@ -107,7 +148,8 @@ internal static string GetQualifiedName(TypeDefinition type) name += $"`{type.GenericParameters.Count}"; } - return type.Namespace is { Value: { Length: > 0 } ns } + string? ns = GetEffectiveNamespace(type); + return ns is { Length: > 0 } ? $"{ns}.{name}" : name; } diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs index b0226c704..df61693c9 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs @@ -313,7 +313,7 @@ private void AddEnumType(TypeDefinition inputType) TypeReference baseType = GetOrCreateTypeReference("System", "Enum", "mscorlib"); TypeDefinition outputType = new( - inputType.Namespace?.Value, + AssemblyAnalyzer.GetEffectiveNamespace(inputType), inputType.Name!.Value, typeAttributes, baseType); @@ -390,7 +390,7 @@ private void AddDelegateType(TypeDefinition inputType) TypeReference baseType = GetOrCreateTypeReference("System", "MulticastDelegate", "mscorlib"); TypeDefinition outputType = new( - inputType.Namespace?.Value, + AssemblyAnalyzer.GetEffectiveNamespace(inputType), inputType.Name!.Value, typeAttributes, baseType); @@ -466,7 +466,7 @@ private void AddInterfaceType(TypeDefinition inputType) TypeAttributes.Abstract; TypeDefinition outputType = new( - inputType.Namespace?.Value, + AssemblyAnalyzer.GetEffectiveNamespace(inputType), inputType.Name!.Value, typeAttributes); @@ -530,7 +530,7 @@ private void AddStructType(TypeDefinition inputType) TypeReference baseType = GetOrCreateTypeReference("System", "ValueType", "mscorlib"); TypeDefinition outputType = new( - inputType.Namespace?.Value, + AssemblyAnalyzer.GetEffectiveNamespace(inputType), inputType.Name!.Value, typeAttributes, baseType); @@ -602,7 +602,7 @@ private void AddClassType(TypeDefinition inputType) } TypeDefinition outputType = new( - inputType.Namespace?.Value, + AssemblyAnalyzer.GetEffectiveNamespace(inputType), inputType.Name!.Value, typeAttributes, baseType); @@ -773,7 +773,7 @@ private void AddSynthesizedInterface( HashSet membersFromInterfaces) { bool hasMembers = false; - string ns = inputType.Namespace?.Value ?? ""; + string ns = AssemblyAnalyzer.GetEffectiveNamespace(inputType) ?? ""; string className = inputType.Name!.Value; string interfaceName = GetSynthesizedInterfaceName(className, interfaceType); From 66c86679f0439a257d70a49505d69703bcbbd28c Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Mon, 23 Mar 2026 21:39:47 -0700 Subject: [PATCH 05/50] Fix synthesized interface duplication and nested type reference handling - Exclude properties and events from the synthesized default interface when they are already provided by an implemented interface, matching the original source generator behavior. Uses accessor name matching (get_/add_) against the membersFromInterfaces set. - Use GetEffectiveNamespace in ImportTypeReference fallback path so nested TypeDefinitions that aren't yet in the type mapping get correct namespace in their TypeReference. - Update GetQualifiedName(ITypeDefOrRef) to use GetEffectiveNamespace for TypeDefinition arguments, ensuring nested types get correct qualified names for TypeMapper lookups. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Discovery/AssemblyAnalyzer.cs | 8 ++++++- .../Generation/WinmdWriter.cs | 22 ++++++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/WinRT.WinMD.Generator/Discovery/AssemblyAnalyzer.cs b/src/WinRT.WinMD.Generator/Discovery/AssemblyAnalyzer.cs index 931e5889a..26d0ec518 100644 --- a/src/WinRT.WinMD.Generator/Discovery/AssemblyAnalyzer.cs +++ b/src/WinRT.WinMD.Generator/Discovery/AssemblyAnalyzer.cs @@ -156,6 +156,7 @@ internal static string GetQualifiedName(TypeDefinition type) /// /// 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) { @@ -165,7 +166,12 @@ internal static string GetQualifiedName(ITypeDefOrRef type) name += $"`{td.GenericParameters.Count}"; } - return type.Namespace is { Value: { Length: > 0 } ns } + // For TypeDefinition, use GetEffectiveNamespace to handle nested types + string? ns = type is TypeDefinition typeDef + ? GetEffectiveNamespace(typeDef) + : type.Namespace?.Value; + + return ns is { Length: > 0 } ? $"{ns}.{name}" : name; } diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs index df61693c9..dc0c6d9a4 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs @@ -833,6 +833,16 @@ private void AddSynthesizedInterface( 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); } @@ -852,6 +862,16 @@ private void AddSynthesizedInterface( 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); } @@ -1337,7 +1357,7 @@ private ITypeDefOrRef ImportTypeReference(ITypeDefOrRef type) // Otherwise create a type reference return GetOrCreateTypeReference( - typeDef.Namespace?.Value ?? "", + AssemblyAnalyzer.GetEffectiveNamespace(typeDef) ?? "", typeDef.Name!.Value, _inputModule.Assembly?.Name?.Value ?? "mscorlib"); } From 66cfbfbe9040484a4115f58fe20a17fdf495bd65 Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Tue, 24 Mar 2026 00:17:22 -0700 Subject: [PATCH 06/50] Reorganize WinMD generator to match other build task conventions Align WinRT.WinMD.Generator with the patterns used by WinRT.Interop.Generator, WinRT.Impl.Generator, and WinRT.Projection.Generator: csproj: - Add IsBuildTool with conditional RuntimeIdentifier/SelfContained for publishing - Add common metadata properties (Description, AssemblyTitle, Copyright, etc.) - Split NativeAOT settings into separate PropertyGroup with missing settings: ControlFlowGuard, IlcDehydrate, IlcResilient, UseSystemResourceKeys (Release) - Change LangVersion from 14.0 to preview (matches Impl/Projection) Error handling: - Add WellKnownWinMDException base class with error ID and formatted ToString() - Add UnhandledWinMDException with phase tracking and bug-report message - Update WellKnownWinMDExceptions to use error code prefix CSWINRTWINMDGEN with numbered IDs (0001-0006) instead of generic InvalidOperationException - Add Extensions/WinMDExceptionExtensions.cs with IsWellKnown extension Generator structure: - Make WinMDGenerator a partial class split by phase: - WinMDGenerator.cs: Run() with try-catch-when per phase - WinMDGenerator.Discover.cs: assembly loading and type discovery - WinMDGenerator.Generate.cs: WinMD generation and writing - Add WinMDGeneratorDiscoveryState to pass state between phases WinmdWriter decomposition: - Split 1819-line WinmdWriter.cs into partial files: - WinmdWriter.cs: core (fields, ProcessType, FinalizeGeneration, Write) - WinmdWriter.Types.cs: enum/delegate/interface/struct/class processing - WinmdWriter.SynthesizedInterfaces.cs: synthesized + custom mapped interfaces - WinmdWriter.Members.cs: method/property/event helpers - WinmdWriter.TypeMapping.cs: type signature mapping + type references - WinmdWriter.Attributes.cs: attribute creation, copying, and helpers - Move TypeDeclaration class to Models/TypeDeclaration.cs - Clean up unnecessary using directives per file Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../CommandLineArgumentNameAttribute.cs | 2 +- .../Discovery/AssemblyAnalyzer.cs | 2 +- .../Errors/UnhandledWinMDException.cs | 38 + .../Errors/WellKnownWinMDException.cs | 37 + .../Errors/WellKnownWinMDExceptions.cs | 51 +- .../Extensions/WinMDExceptionExtensions.cs | 21 + .../Generation/WinMDGenerator.Discover.cs | 42 + .../Generation/WinMDGenerator.Generate.cs | 42 + .../Generation/WinMDGenerator.cs | 76 +- .../Generation/WinMDGeneratorArgs.Parsing.cs | 2 +- .../Generation/WinMDGeneratorArgs.cs | 2 +- .../WinMDGeneratorDiscoveryState.cs | 28 + .../Generation/WinmdWriter.Attributes.cs | 379 ++++ .../Generation/WinmdWriter.Members.cs | 278 +++ .../WinmdWriter.SynthesizedInterfaces.cs | 292 ++++ .../Generation/WinmdWriter.TypeMapping.cs | 176 ++ .../Generation/WinmdWriter.Types.cs | 419 +++++ .../Generation/WinmdWriter.cs | 1554 +---------------- .../Models/TypeDeclaration.cs | 34 + .../Models/TypeMapper.cs | 2 +- src/WinRT.WinMD.Generator/Program.cs | 2 +- .../Resolvers/PathAssemblyResolver.cs | 2 +- .../WinRT.WinMD.Generator.csproj | 32 +- 23 files changed, 1875 insertions(+), 1638 deletions(-) create mode 100644 src/WinRT.WinMD.Generator/Errors/UnhandledWinMDException.cs create mode 100644 src/WinRT.WinMD.Generator/Errors/WellKnownWinMDException.cs create mode 100644 src/WinRT.WinMD.Generator/Extensions/WinMDExceptionExtensions.cs create mode 100644 src/WinRT.WinMD.Generator/Generation/WinMDGenerator.Discover.cs create mode 100644 src/WinRT.WinMD.Generator/Generation/WinMDGenerator.Generate.cs create mode 100644 src/WinRT.WinMD.Generator/Generation/WinMDGeneratorDiscoveryState.cs create mode 100644 src/WinRT.WinMD.Generator/Generation/WinmdWriter.Attributes.cs create mode 100644 src/WinRT.WinMD.Generator/Generation/WinmdWriter.Members.cs create mode 100644 src/WinRT.WinMD.Generator/Generation/WinmdWriter.SynthesizedInterfaces.cs create mode 100644 src/WinRT.WinMD.Generator/Generation/WinmdWriter.TypeMapping.cs create mode 100644 src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs create mode 100644 src/WinRT.WinMD.Generator/Models/TypeDeclaration.cs diff --git a/src/WinRT.WinMD.Generator/Attributes/CommandLineArgumentNameAttribute.cs b/src/WinRT.WinMD.Generator/Attributes/CommandLineArgumentNameAttribute.cs index 5fe2632af..78ff10de6 100644 --- a/src/WinRT.WinMD.Generator/Attributes/CommandLineArgumentNameAttribute.cs +++ b/src/WinRT.WinMD.Generator/Attributes/CommandLineArgumentNameAttribute.cs @@ -16,4 +16,4 @@ 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 index 26d0ec518..2f25e3b99 100644 --- a/src/WinRT.WinMD.Generator/Discovery/AssemblyAnalyzer.cs +++ b/src/WinRT.WinMD.Generator/Discovery/AssemblyAnalyzer.cs @@ -175,4 +175,4 @@ internal static string GetQualifiedName(ITypeDefOrRef type) ? $"{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 000000000..96e3700bd --- /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 000000000..16ba08edc --- /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 index c69b95473..4b69ade6b 100644 --- a/src/WinRT.WinMD.Generator/Errors/WellKnownWinMDExceptions.cs +++ b/src/WinRT.WinMD.Generator/Errors/WellKnownWinMDExceptions.cs @@ -11,42 +11,63 @@ namespace WindowsRuntime.WinMDGenerator.Errors; internal static class WellKnownWinMDExceptions { /// - /// Creates an exception for a response file read error. + /// The prefix for all errors produced by this tool. /// - public static Exception ResponseFileReadError(Exception inner) + public const string ErrorPrefix = "CSWINRTWINMDGEN"; + + /// + /// Some exception was thrown when trying to read the response file. + /// + public static Exception ResponseFileReadError(Exception exception) { - return new InvalidOperationException("Failed to read the response file.", inner); + return Exception(1, "Failed to read the response file to run 'cswinrtwinmdgen'.", exception); } /// - /// Creates an exception for a malformed response file. + /// The input response file is malformed. /// public static Exception MalformedResponseFile() { - return new InvalidOperationException("The response file is malformed."); + 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); } /// - /// Creates an exception for a response file argument parsing error. + /// Failed to generate the WinMD file. /// - public static Exception ResponseFileArgumentParsingError(string propertyName, Exception? inner = null) + public static Exception WinMDGenerationError(Exception exception) { - return new InvalidOperationException($"Failed to parse the '{propertyName}' argument from the response file.", inner); + return Exception(5, "Failed to generate the WinMD file.", exception); } /// - /// Creates an exception for a WinMD generation error. + /// Failed to write the WinMD file to disk. /// - public static Exception WinMDGenerationError(Exception inner) + public static Exception WinMDWriteError(Exception exception) { - return new InvalidOperationException("Failed to generate the WinMD file.", inner); + return Exception(6, "Failed to write the WinMD file to disk.", exception); } /// - /// Creates an exception for a WinMD write error. + /// Creates a new exception with the specified id and message. /// - public static Exception WinMDWriteError(Exception inner) + private static Exception Exception(int id, string message, Exception? innerException = null) { - return new InvalidOperationException("Failed to write the WinMD file.", inner); + return new WellKnownWinMDException($"{ErrorPrefix}{id:0000}", message, innerException); } -} +} \ No newline at end of file diff --git a/src/WinRT.WinMD.Generator/Extensions/WinMDExceptionExtensions.cs b/src/WinRT.WinMD.Generator/Extensions/WinMDExceptionExtensions.cs new file mode 100644 index 000000000..c376c4911 --- /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 000000000..b4e17ffb2 --- /dev/null +++ b/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.Discover.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using AsmResolver.DotNet; +using WindowsRuntime.WinMDGenerator.Discovery; +using WindowsRuntime.WinMDGenerator.Errors; +using WindowsRuntime.WinMDGenerator.Resolvers; + +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]; + PathAssemblyResolver assemblyResolver = new(allReferencePaths); + inputModule = ModuleDefinition.FromFile(args.InputAssemblyPath, assemblyResolver.ReaderParameters); + } + 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 000000000..62e28876f --- /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 index eab9bd40a..4cfecec80 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.cs @@ -2,22 +2,16 @@ // Licensed under the MIT License. using System; -using System.Collections.Generic; -using System.IO; using System.Threading; -using AsmResolver.DotNet; using ConsoleAppFramework; -using WindowsRuntime.WinMDGenerator.Discovery; using WindowsRuntime.WinMDGenerator.Errors; -using WindowsRuntime.WinMDGenerator.Models; -using WindowsRuntime.WinMDGenerator.Resolvers; namespace WindowsRuntime.WinMDGenerator.Generation; /// /// The implementation of the CsWinRT WinMD generator. /// -internal static class WinMDGenerator +internal static partial class WinMDGenerator { /// /// Runs the WinMD generator to produce a .winmd file from a compiled assembly. @@ -28,81 +22,47 @@ public static void Run([Argument] string inputFilePath, CancellationToken token) { WinMDGeneratorArgs args; - // Parse the actual arguments from the response file + // Phase 1: Parse the actual arguments from the response file try { args = WinMDGeneratorArgs.ParseFromResponseFile(inputFilePath, token); } - catch (Exception e) + catch (Exception e) when (!e.IsWellKnown) { - ConsoleApp.Log($"Error parsing response file: {e.Message}"); - throw; + throw new UnhandledWinMDException("parsing", e); } token.ThrowIfCancellationRequested(); - ConsoleApp.Log($"Generating WinMD for assembly: {Path.GetFileName(args.InputAssemblyPath)}"); + ConsoleApp.Log($"Generating WinMD for assembly: {System.IO.Path.GetFileName(args.InputAssemblyPath)}"); ConsoleApp.Log($"Output: {args.OutputWinmdPath}"); - // Phase 1: Load the input assembly using AsmResolver - ModuleDefinition inputModule; + // Phase 2: Load and discover + WinMDGeneratorDiscoveryState discoveryState; try { - string[] allReferencePaths = [args.InputAssemblyPath, .. args.ReferenceAssemblyPaths]; - PathAssemblyResolver assemblyResolver = new(allReferencePaths); - inputModule = ModuleDefinition.FromFile(args.InputAssemblyPath, assemblyResolver.ReaderParameters); + discoveryState = Discover(args); } - catch (Exception e) + catch (Exception e) when (!e.IsWellKnown) { - ConsoleApp.Log($"Error loading input assembly: {e.Message}"); - throw WellKnownWinMDExceptions.WinMDGenerationError(e); + throw new UnhandledWinMDException("discovery", e); } token.ThrowIfCancellationRequested(); - // Phase 2: Discover public types - AssemblyAnalyzer analyzer = new(inputModule); - IReadOnlyList publicTypes = analyzer.DiscoverPublicTypes(); + ConsoleApp.Log($"Found {discoveryState.PublicTypes.Count} public types"); - ConsoleApp.Log($"Found {publicTypes.Count} public types"); - - token.ThrowIfCancellationRequested(); - - // Phase 3: Generate the WinMD + // Phase 3: Generate and write the WinMD try { - string assemblyName = analyzer.AssemblyName; - TypeMapper mapper = new(args.UseWindowsUIXamlProjections); - - WinmdWriter writer = new( - assemblyName, - args.AssemblyVersion, - mapper, - inputModule); - - foreach (TypeDefinition type in 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); - - ConsoleApp.Log($"WinMD generated successfully: {args.OutputWinmdPath}"); + Generate(args, discoveryState); } - catch (Exception e) + catch (Exception e) when (!e.IsWellKnown) { - ConsoleApp.Log($"Error generating WinMD: {e.Message}"); - throw WellKnownWinMDExceptions.WinMDGenerationError(e); + 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 index 4101754f4..0ac4c8517 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinMDGeneratorArgs.Parsing.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinMDGeneratorArgs.Parsing.cs @@ -155,4 +155,4 @@ private static bool GetBooleanArgument(Dictionary argsMap, strin 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 index 1efa197a5..abd4f7126 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinMDGeneratorArgs.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinMDGeneratorArgs.cs @@ -33,4 +33,4 @@ internal sealed partial class WinMDGeneratorArgs /// 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 000000000..5a15fb99f --- /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 000000000..3a2616bde --- /dev/null +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Attributes.cs @@ -0,0 +1,379 @@ +// 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 string guidString && + Guid.TryParse(guidString, 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 + using (SHA1 sha = SHA1.Create()) + { + byte[] hash = sha.ComputeHash(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(), + _outputModule.CorLibTypeFactory.UInt32)); + + CustomAttributeSignature sig = new(); + sig.FixedArguments.Add(new CustomAttributeArgument(systemType.ToTypeSignature(), 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(), + _outputModule.CorLibTypeFactory.UInt32)); + + CustomAttributeSignature sig = new(); + sig.FixedArguments.Add(new CustomAttributeArgument(systemType.ToTypeSignature(), 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())); + + CustomAttributeSignature sig = new(); + sig.FixedArguments.Add(new CustomAttributeArgument(systemType.ToTypeSignature(), 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 == "VersionAttribute"); + } + + 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)) + { + 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) + { + 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 + "WinRT.GeneratedBindableCustomPropertyAttribute" or + "Windows.Foundation.Metadata.VersionAttribute") + { + 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) + TypeDefinition? attrTypeDef = attr.Constructor?.DeclaringType?.Resolve(); + if (attrTypeDef != null && !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); + + TypeSignature[] mappedParams = [.. methodSig.ParameterTypes + .Select(MapTypeSignatureToOutput)]; + + MethodSignature importedSig = MethodSignature.CreateInstance( + _outputModule.CorLibTypeFactory.Void, + mappedParams); + + 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; + } + + /// + /// Clones a single custom attribute argument, remapping type references. + /// + private CustomAttributeArgument CloneAttributeArgument(CustomAttributeArgument arg) + { + TypeSignature mappedType = MapTypeSignatureToOutput(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); + + // 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); + } +} \ 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 000000000..51e51383f --- /dev/null +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Members.cs @@ -0,0 +1,278 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Linq; +using AsmResolver.DotNet; +using AsmResolver.DotNet.Signatures; +using AsmResolver.PE.DotNet.Metadata.Tables; +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 + int paramIndex = 1; + foreach (ParameterDefinition inputParam in inputMethod.ParameterDefinitions) + { + outputMethod.ParameterDefinitions.Add(new ParameterDefinition( + (ushort)paramIndex++, + inputParam.Name!.Value, + inputParam.Attributes)); + } + + 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 + int paramIndex = 1; + foreach (ParameterDefinition inputParam in inputMethod.ParameterDefinitions) + { + outputMethod.ParameterDefinitions.Add(new ParameterDefinition( + (ushort)paramIndex++, + inputParam.Name!.Value, + inputParam.Attributes)); + } + + outputType.Methods.Add(outputMethod); + + // Copy custom attributes from the input method + CopyCustomAttributes(inputMethod, outputMethod); + } + + private void AddPropertyToType(TypeDefinition outputType, PropertyDefinition inputProperty, bool isInterfaceParent) + { + TypeSignature propertyType = MapTypeSignatureToOutput(inputProperty.Signature!.ReturnType); + + PropertyDefinition outputProperty = new( + inputProperty.Name!.Value, + 0, + PropertySignature.CreateInstance(propertyType)); + + bool isStatic = inputProperty.GetMethod?.IsStatic == true || inputProperty.SetMethod?.IsStatic == true; + + // 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); + + bool isStatic = 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(); + TypeSignature tokenSig = eventRegistrationTokenType.ToTypeSignature(); + + 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(); + + 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 000000000..bdb3cb3c8 --- /dev/null +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.SynthesizedInterfaces.cs @@ -0,0 +1,292 @@ +// 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 WindowsRuntime.WinMDGenerator.Models; +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 = []; + foreach (InterfaceImplementation impl in inputType.Interfaces) + { + TypeDefinition? interfaceDef = impl.Interface?.Resolve(); + if (interfaceDef == null) + { + continue; + } + + foreach (MethodDefinition interfaceMethod in interfaceDef.Methods) + { + // Find the class member that implements this interface member + string methodName = interfaceMethod.Name?.Value ?? ""; + _ = membersFromInterfaces.Add(methodName); + } + } + + 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 + TypeReference interfaceRef = GetOrCreateTypeReference(ns, interfaceName, _assemblyName); + InterfaceImplementation interfaceImpl = new(interfaceRef); + 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) + { + TypeSignature returnType = ImportTypeReference(classType).ToTypeSignature(); + + 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 names + int paramIndex = 1; + foreach (ParameterDefinition inputParam in constructor.ParameterDefinitions) + { + factoryMethod.ParameterDefinitions.Add(new ParameterDefinition( + (ushort)paramIndex++, + inputParam.Name!.Value, + inputParam.Attributes)); + } + + synthesizedInterface.Methods.Add(factoryMethod); + } + + /// + /// Processes custom mapped interfaces for a class type. This maps .NET collection interfaces, + /// IDisposable, INotifyPropertyChanged, etc. to their WinRT equivalents. + /// + private void ProcessCustomMappedInterfaces(TypeDefinition inputType, TypeDefinition outputType) + { + foreach (InterfaceImplementation impl in inputType.Interfaces) + { + if (impl.Interface == null) + { + continue; + } + + string interfaceName = GetInterfaceQualifiedName(impl.Interface); + + if (!_mapper.HasMappingForType(interfaceName)) + { + continue; + } + + MappedType mapping = _mapper.GetMappedType(interfaceName); + (string mappedNs, string mappedName, string mappedAssembly, _, _) = mapping.GetMapping(); + + // Add the mapped interface as an implementation on the output type + TypeReference mappedInterfaceRef = GetOrCreateTypeReference(mappedNs, mappedName, mappedAssembly); + + // For generic interfaces, we need to handle type arguments + if (impl.Interface is TypeSpecification typeSpec && typeSpec.Signature is GenericInstanceTypeSignature genericInst) + { + TypeSignature[] mappedArgs = [.. genericInst.TypeArguments + .Select(MapTypeSignatureToOutput)]; + TypeSpecification mappedSpec = new(new GenericInstanceTypeSignature(mappedInterfaceRef, false, mappedArgs)); + outputType.Interfaces.Add(new InterfaceImplementation(mappedSpec)); + } + else + { + outputType.Interfaces.Add(new InterfaceImplementation(mappedInterfaceRef)); + } + } + } + + 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 000000000..85b372a73 --- /dev/null +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.TypeMapping.cs @@ -0,0 +1,176 @@ +// 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 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) + { + ITypeDefOrRef importedType = ImportTypeReference(genericInst.GenericType); + TypeSignature[] mappedArgs = [.. genericInst.TypeArguments + .Select(MapTypeSignatureToOutput)]; + return new GenericInstanceTypeSignature(importedType, genericInst.IsValueType, mappedArgs); + } + + // 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) + { + 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; + } + + // Otherwise 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 assembly = GetAssemblyNameFromScope(typeRef.Scope); + 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" + }; + } + + 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 000000000..410a1fffe --- /dev/null +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs @@ -0,0 +1,419 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +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 TypeAttributes = AsmResolver.PE.DotNet.Metadata.Tables.TypeAttributes; + +namespace WindowsRuntime.WinMDGenerator.Generation; + +internal sealed partial class WinmdWriter +{ + private void AddEnumType(TypeDefinition inputType) + { + 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); + + // 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(underlyingType)); + + if (field.Constant != null) + { + outputField.Constant = new Constant(field.Constant.Type, new DataBlobSignature(field.Constant.Value!.Data)); + } + + outputType.Fields.Add(outputField); + } + + _outputModule.TopLevelTypes.Add(outputType); + + TypeDeclaration declaration = new(inputType, outputType, isComponentType: true); + _typeDefinitionMapping[qualifiedName] = declaration; + } + + 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); + + // Add .ctor(object, IntPtr) + MethodDefinition ctor = new( + ".ctor", + MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RuntimeSpecialName, + MethodSignature.CreateInstance( + _outputModule.CorLibTypeFactory.Void, + _outputModule.CorLibTypeFactory.Object, + _outputModule.CorLibTypeFactory.IntPtr)) + { + ImplAttributes = MethodImplAttributes.Runtime | MethodImplAttributes.Managed + }; + 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 + int paramIndex = 1; + foreach (ParameterDefinition inputParam in inputInvoke.ParameterDefinitions) + { + invoke.ParameterDefinitions.Add(new ParameterDefinition( + (ushort)paramIndex++, + inputParam.Name!.Value, + inputParam.Attributes)); + } + + outputType.Methods.Add(invoke); + } + + _outputModule.TopLevelTypes.Add(outputType); + + TypeDeclaration declaration = new(inputType, outputType, isComponentType: true); + _typeDefinitionMapping[qualifiedName] = declaration; + + // 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); + + // Add methods + foreach (MethodDefinition method in inputType.Methods) + { + if (!method.IsPublic) + { + 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 = ImportTypeReference(impl.Interface); + outputType.Interfaces.Add(new InterfaceImplementation(outputInterfaceRef)); + } + } + + _outputModule.TopLevelTypes.Add(outputType); + + TypeDeclaration declaration = new(inputType, outputType, isComponentType: true); + _typeDefinitionMapping[qualifiedName] = declaration; + + // 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); + + // 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); + } + + _outputModule.TopLevelTypes.Add(outputType); + + TypeDeclaration declaration = new(inputType, outputType, isComponentType: true); + _typeDefinitionMapping[qualifiedName] = declaration; + } + + 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 = inputType.BaseType.Resolve(); + 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); + + bool hasConstructor = false; + bool hasDefaultConstructor = false; + bool hasAtLeastOneNonPublicConstructor = false; + bool isStaticClass = inputType.IsAbstract && inputType.IsSealed; + + // 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) + { + AddMethodToClass(outputType, method); + } + } + + // Add properties + foreach (PropertyDefinition property in inputType.Properties) + { + // 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) + { + 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 = ImportTypeReference(impl.Interface); + outputType.Interfaces.Add(new InterfaceImplementation(outputInterfaceRef)); + } + + _outputModule.TopLevelTypes.Add(outputType); + + TypeDeclaration declaration = new(inputType, outputType, isComponentType: true); + _typeDefinitionMapping[qualifiedName] = declaration; + + // 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); + } +} \ No newline at end of file diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs index dc0c6d9a4..34c42fdcd 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs @@ -4,21 +4,11 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Security.Cryptography; -using System.Text; using AsmResolver.DotNet; using AsmResolver.DotNet.Signatures; -using AsmResolver.PE.DotNet.Metadata.Tables; using WindowsRuntime.WinMDGenerator.Discovery; using WindowsRuntime.WinMDGenerator.Errors; 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; -using FieldAttributes = AsmResolver.PE.DotNet.Metadata.Tables.FieldAttributes; -using TypeAttributes = AsmResolver.PE.DotNet.Metadata.Tables.TypeAttributes; using AssemblyAttributes = AsmResolver.PE.DotNet.Metadata.Tables.AssemblyAttributes; namespace WindowsRuntime.WinMDGenerator.Generation; @@ -26,7 +16,7 @@ namespace WindowsRuntime.WinMDGenerator.Generation; /// /// Writes a WinMD file from analyzed assembly types using AsmResolver. /// -internal sealed class WinmdWriter +internal sealed partial class WinmdWriter { private readonly string _assemblyName; private readonly string _version; @@ -296,1544 +286,4 @@ public void Write(string outputPath) throw WellKnownWinMDExceptions.WinMDWriteError(e); } } - - #region Enum Types - - private void AddEnumType(TypeDefinition inputType) - { - 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); - - // 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(underlyingType)); - - if (field.Constant != null) - { - outputField.Constant = new Constant(field.Constant.Type, new DataBlobSignature(field.Constant.Value!.Data)); - } - - outputType.Fields.Add(outputField); - } - - _outputModule.TopLevelTypes.Add(outputType); - - TypeDeclaration declaration = new(inputType, outputType, isComponentType: true); - _typeDefinitionMapping[qualifiedName] = declaration; - } - - 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; - } - - #endregion - - #region Delegate Types - - 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); - - // Add .ctor(object, IntPtr) - MethodDefinition ctor = new( - ".ctor", - MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RuntimeSpecialName, - MethodSignature.CreateInstance( - _outputModule.CorLibTypeFactory.Void, - _outputModule.CorLibTypeFactory.Object, - _outputModule.CorLibTypeFactory.IntPtr)) - { - ImplAttributes = MethodImplAttributes.Runtime | MethodImplAttributes.Managed - }; - 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 - int paramIndex = 1; - foreach (ParameterDefinition inputParam in inputInvoke.ParameterDefinitions) - { - invoke.ParameterDefinitions.Add(new ParameterDefinition( - (ushort)paramIndex++, - inputParam.Name!.Value, - inputParam.Attributes)); - } - - outputType.Methods.Add(invoke); - } - - _outputModule.TopLevelTypes.Add(outputType); - - TypeDeclaration declaration = new(inputType, outputType, isComponentType: true); - _typeDefinitionMapping[qualifiedName] = declaration; - - // Add GUID attribute - AddGuidAttribute(outputType, inputType); - } - - #endregion - - #region Interface Types - - 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); - - // Add methods - foreach (MethodDefinition method in inputType.Methods) - { - if (!method.IsPublic) - { - 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 = ImportTypeReference(impl.Interface); - outputType.Interfaces.Add(new InterfaceImplementation(outputInterfaceRef)); - } - } - - _outputModule.TopLevelTypes.Add(outputType); - - TypeDeclaration declaration = new(inputType, outputType, isComponentType: true); - _typeDefinitionMapping[qualifiedName] = declaration; - - // Add GUID attribute - AddGuidAttribute(outputType, inputType); - } - - #endregion - - #region Struct Types - - 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); - - // 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); - } - - _outputModule.TopLevelTypes.Add(outputType); - - TypeDeclaration declaration = new(inputType, outputType, isComponentType: true); - _typeDefinitionMapping[qualifiedName] = declaration; - } - - #endregion - - #region Class Types - - 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 = inputType.BaseType.Resolve(); - 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); - - bool hasConstructor = false; - bool hasDefaultConstructor = false; - bool hasAtLeastOneNonPublicConstructor = false; - bool isStaticClass = inputType.IsAbstract && inputType.IsSealed; - - // 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) - { - AddMethodToClass(outputType, method); - } - } - - // Add properties - foreach (PropertyDefinition property in inputType.Properties) - { - // 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) - { - 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 = ImportTypeReference(impl.Interface); - outputType.Interfaces.Add(new InterfaceImplementation(outputInterfaceRef)); - } - - _outputModule.TopLevelTypes.Add(outputType); - - TypeDeclaration declaration = new(inputType, outputType, isComponentType: true); - _typeDefinitionMapping[qualifiedName] = declaration; - - // 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); - } - - #endregion - - #region Synthesized Interfaces - - 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 = []; - foreach (InterfaceImplementation impl in inputType.Interfaces) - { - TypeDefinition? interfaceDef = impl.Interface?.Resolve(); - if (interfaceDef == null) - { - continue; - } - - foreach (MethodDefinition interfaceMethod in interfaceDef.Methods) - { - // Find the class member that implements this interface member - string methodName = interfaceMethod.Name?.Value ?? ""; - _ = membersFromInterfaces.Add(methodName); - } - } - - 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 - TypeReference interfaceRef = GetOrCreateTypeReference(ns, interfaceName, _assemblyName); - InterfaceImplementation interfaceImpl = new(interfaceRef); - 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) - { - TypeSignature returnType = ImportTypeReference(classType).ToTypeSignature(); - - 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 names - int paramIndex = 1; - foreach (ParameterDefinition inputParam in constructor.ParameterDefinitions) - { - factoryMethod.ParameterDefinitions.Add(new ParameterDefinition( - (ushort)paramIndex++, - inputParam.Name!.Value, - inputParam.Attributes)); - } - - synthesizedInterface.Methods.Add(factoryMethod); - } - - #endregion - - #region Custom Mapped Interfaces - - /// - /// Processes custom mapped interfaces for a class type. This maps .NET collection interfaces, - /// IDisposable, INotifyPropertyChanged, etc. to their WinRT equivalents. - /// - private void ProcessCustomMappedInterfaces(TypeDefinition inputType, TypeDefinition outputType) - { - foreach (InterfaceImplementation impl in inputType.Interfaces) - { - if (impl.Interface == null) - { - continue; - } - - string interfaceName = GetInterfaceQualifiedName(impl.Interface); - - if (!_mapper.HasMappingForType(interfaceName)) - { - continue; - } - - MappedType mapping = _mapper.GetMappedType(interfaceName); - (string mappedNs, string mappedName, string mappedAssembly, _, _) = mapping.GetMapping(); - - // Add the mapped interface as an implementation on the output type - TypeReference mappedInterfaceRef = GetOrCreateTypeReference(mappedNs, mappedName, mappedAssembly); - - // For generic interfaces, we need to handle type arguments - if (impl.Interface is TypeSpecification typeSpec && typeSpec.Signature is GenericInstanceTypeSignature genericInst) - { - TypeSignature[] mappedArgs = [.. genericInst.TypeArguments - .Select(MapTypeSignatureToOutput)]; - TypeSpecification mappedSpec = new(new GenericInstanceTypeSignature(mappedInterfaceRef, false, mappedArgs)); - outputType.Interfaces.Add(new InterfaceImplementation(mappedSpec)); - } - else - { - outputType.Interfaces.Add(new InterfaceImplementation(mappedInterfaceRef)); - } - } - } - - private static string GetInterfaceQualifiedName(ITypeDefOrRef type) - { - return type is TypeSpecification typeSpec && typeSpec.Signature is GenericInstanceTypeSignature genericInst - ? AssemblyAnalyzer.GetQualifiedName(genericInst.GenericType) - : AssemblyAnalyzer.GetQualifiedName(type); - } - - #endregion - - #region Method/Property/Event Helpers - - 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 - int paramIndex = 1; - foreach (ParameterDefinition inputParam in inputMethod.ParameterDefinitions) - { - outputMethod.ParameterDefinitions.Add(new ParameterDefinition( - (ushort)paramIndex++, - inputParam.Name!.Value, - inputParam.Attributes)); - } - - 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 - int paramIndex = 1; - foreach (ParameterDefinition inputParam in inputMethod.ParameterDefinitions) - { - outputMethod.ParameterDefinitions.Add(new ParameterDefinition( - (ushort)paramIndex++, - inputParam.Name!.Value, - inputParam.Attributes)); - } - - outputType.Methods.Add(outputMethod); - - // Copy custom attributes from the input method - CopyCustomAttributes(inputMethod, outputMethod); - } - - private void AddPropertyToType(TypeDefinition outputType, PropertyDefinition inputProperty, bool isInterfaceParent) - { - TypeSignature propertyType = MapTypeSignatureToOutput(inputProperty.Signature!.ReturnType); - - PropertyDefinition outputProperty = new( - inputProperty.Name!.Value, - 0, - PropertySignature.CreateInstance(propertyType)); - - bool isStatic = inputProperty.GetMethod?.IsStatic == true || inputProperty.SetMethod?.IsStatic == true; - - // 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); - - bool isStatic = 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(); - TypeSignature tokenSig = eventRegistrationTokenType.ToTypeSignature(); - - 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(); - - 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); - } - - #endregion - - #region Type Mapping - - /// - /// 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) - { - ITypeDefOrRef importedType = ImportTypeReference(genericInst.GenericType); - TypeSignature[] mappedArgs = [.. genericInst.TypeArguments - .Select(MapTypeSignatureToOutput)]; - return new GenericInstanceTypeSignature(importedType, genericInst.IsValueType, mappedArgs); - } - - // 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) - { - 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; - } - - // Otherwise 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 assembly = GetAssemblyNameFromScope(typeRef.Scope); - 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" - }; - } - - #endregion - - #region Type References - - 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; - } - - #endregion - - #region Attributes - - 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 string guidString && - Guid.TryParse(guidString, 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 - using (SHA1 sha = SHA1.Create()) - { - byte[] hash = sha.ComputeHash(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(), - _outputModule.CorLibTypeFactory.UInt32)); - - CustomAttributeSignature sig = new(); - sig.FixedArguments.Add(new CustomAttributeArgument(systemType.ToTypeSignature(), 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(), - _outputModule.CorLibTypeFactory.UInt32)); - - CustomAttributeSignature sig = new(); - sig.FixedArguments.Add(new CustomAttributeArgument(systemType.ToTypeSignature(), 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())); - - CustomAttributeSignature sig = new(); - sig.FixedArguments.Add(new CustomAttributeArgument(systemType.ToTypeSignature(), 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 == "VersionAttribute"); - } - - 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)) - { - 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) - { - 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 - "WinRT.GeneratedBindableCustomPropertyAttribute" or - "Windows.Foundation.Metadata.VersionAttribute") - { - 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) - TypeDefinition? attrTypeDef = attr.Constructor?.DeclaringType?.Resolve(); - if (attrTypeDef != null && !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); - - TypeSignature[] mappedParams = [.. methodSig.ParameterTypes - .Select(MapTypeSignatureToOutput)]; - - MethodSignature importedSig = MethodSignature.CreateInstance( - _outputModule.CorLibTypeFactory.Void, - mappedParams); - - 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; - } - - /// - /// Clones a single custom attribute argument, remapping type references. - /// - private CustomAttributeArgument CloneAttributeArgument(CustomAttributeArgument arg) - { - TypeSignature mappedType = MapTypeSignatureToOutput(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; - } - - #endregion - - #region Helpers - - 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); - - // 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); - } - - #endregion -} - -/// -/// 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/TypeDeclaration.cs b/src/WinRT.WinMD.Generator/Models/TypeDeclaration.cs new file mode 100644 index 000000000..1c4675edf --- /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 index 841d0c811..842e0a9d8 100644 --- a/src/WinRT.WinMD.Generator/Models/TypeMapper.cs +++ b/src/WinRT.WinMD.Generator/Models/TypeMapper.cs @@ -219,4 +219,4 @@ public MappedType GetMappedType(string typeName) "System.Runtime.InteropServices.IDynamicInterfaceCastable", "WinRT.IWinRTObject" }; -} +} \ No newline at end of file diff --git a/src/WinRT.WinMD.Generator/Program.cs b/src/WinRT.WinMD.Generator/Program.cs index 04c9b9afb..84fa701fd 100644 --- a/src/WinRT.WinMD.Generator/Program.cs +++ b/src/WinRT.WinMD.Generator/Program.cs @@ -5,4 +5,4 @@ using WindowsRuntime.WinMDGenerator.Generation; // Run the WinMD generator with all parsed arguments -ConsoleApp.Run(args, WinMDGenerator.Run); +ConsoleApp.Run(args, WinMDGenerator.Run); \ No newline at end of file diff --git a/src/WinRT.WinMD.Generator/Resolvers/PathAssemblyResolver.cs b/src/WinRT.WinMD.Generator/Resolvers/PathAssemblyResolver.cs index 8adc5a32d..fa4a1225d 100644 --- a/src/WinRT.WinMD.Generator/Resolvers/PathAssemblyResolver.cs +++ b/src/WinRT.WinMD.Generator/Resolvers/PathAssemblyResolver.cs @@ -94,4 +94,4 @@ public void ClearCache() { _cache.Clear(); } -} +} \ 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 index 8367af499..bd46487da 100644 --- a/src/WinRT.WinMD.Generator/WinRT.WinMD.Generator.csproj +++ b/src/WinRT.WinMD.Generator/WinRT.WinMD.Generator.csproj @@ -2,25 +2,29 @@ Exe net10.0 - 14.0 + preview enable true true true - true - Speed - false + + + 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 @@ -28,6 +32,22 @@ true strict true + + + true + win-$(BuildToolArch) + true + + + + + true + Speed + false + true + Guard + false + false From 6d50286151bda75ebeedbd387bf71a6929ca82c1 Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Sat, 11 Apr 2026 17:32:26 -0700 Subject: [PATCH 07/50] Reuse shared properties from CsWinRTGen.targets in WinMD generator targets Remove duplicate CsWinRTGenTasksAssembly definition (already set by CsWinRTGen.targets) and remove WinMD-specific output importance properties in favor of the shared CsWinRTGeneratorStandardOutputImportance, CsWinRTGeneratorStandardErrorImportance, and CsWinRTGeneratorLogStandardErrorAsError properties. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- nuget/Microsoft.Windows.CsWinMD.Generator.targets | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/nuget/Microsoft.Windows.CsWinMD.Generator.targets b/nuget/Microsoft.Windows.CsWinMD.Generator.targets index e1eaeaa25..f7d11facd 100644 --- a/nuget/Microsoft.Windows.CsWinMD.Generator.targets +++ b/nuget/Microsoft.Windows.CsWinMD.Generator.targets @@ -6,7 +6,6 @@ Copyright (C) Microsoft Corporation. All rights reserved. - $(MSBuildThisFileDirectory)..\tools\WinRT.Generator.Tasks.dll $(SolutionDir)WinRT.WinMD.Generator\bin\$(Configuration)\net10.0\ @@ -16,11 +15,6 @@ Copyright (C) Microsoft Corporation. All rights reserved. $(IntermediateOutputPath) <_CsWinRTWinMDOutputPath>$(CsWinRTWinMDOutputDirectory)$(AssemblyName).winmd - - High - High - true - From 1b7a4218c4ade01b791651e57952b9f522a53e1d Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Sat, 11 Apr 2026 17:59:33 -0700 Subject: [PATCH 08/50] Use shared tools directory and gate WinMD generation on CsWinRTComponent - Remove CsWinRTGenerateWinMD property use CsWinRTComponent directly since WinMD generation is inherently a WinRT component authoring scenario - Add CsWinRTWinMDGenEffectiveToolsDirectory to Directory.Build.props alongside the other generator tool directory overrides for solution builds - Add _ResolveCsWinRTWinMDGenToolsDirectory target that depends on _ResolveCsWinRToolsDirectory and falls back to _CsWinRTToolsDirectory for NuGet package consumers - Use instead of hardcoded AnyCPU - Remove hardcoded path from targets file Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...icrosoft.Windows.CsWinMD.Generator.targets | 29 +++++++++++++------ src/Directory.Build.props | 1 + 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/nuget/Microsoft.Windows.CsWinMD.Generator.targets b/nuget/Microsoft.Windows.CsWinMD.Generator.targets index f7d11facd..fdc76e118 100644 --- a/nuget/Microsoft.Windows.CsWinMD.Generator.targets +++ b/nuget/Microsoft.Windows.CsWinMD.Generator.targets @@ -6,10 +6,6 @@ Copyright (C) Microsoft Corporation. All rights reserved. - $(SolutionDir)WinRT.WinMD.Generator\bin\$(Configuration)\net10.0\ - - - false $(IntermediateOutputPath) @@ -23,6 +19,21 @@ Copyright (C) Microsoft Corporation. All rights reserved. <_RunCsWinRTWinMDGeneratorPropertyInputsCachePath>$([MSBuild]::NormalizePath('$(MSBuildProjectDirectory)', '$(_RunCsWinRTWinMDGeneratorPropertyInputsCachePath)')) + + + + $(_CsWinRTToolsDirectory) + + + + DependsOnTargets="CoreCompile;_ResolveCsWinRTWinMDGenToolsDirectory"> - <_RunCsWinRTWinMDGeneratorInputsCacheToHash Include="$(CsWinRTWinMDGenToolsDirectory)" /> + <_RunCsWinRTWinMDGeneratorInputsCacheToHash Include="$(CsWinRTWinMDGenEffectiveToolsDirectory)" /> <_RunCsWinRTWinMDGeneratorInputsCacheToHash Include="$(CsWinRTUseWindowsUIXamlProjections)" /> <_RunCsWinRTWinMDGeneratorInputsCacheToHash Include="$(AssemblyVersion)" /> @@ -66,7 +77,7 @@ Copyright (C) Microsoft Corporation. All rights reserved. AfterTargets="CoreCompile" Inputs="@(IntermediateAssembly);@(ReferencePath);$(_RunCsWinRTWinMDGeneratorPropertyInputsCachePath)" Outputs="$(_CsWinRTWinMDOutputPath)" - Condition="'$(CsWinRTGenerateWinMD)' == 'true'"> + Condition="'$(CsWinRTComponent)' == 'true'"> diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 4d76517b1..8d3fa0ea3 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 From ba8c215c411d2ec10bb1353e652b60c0a24ca958 Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Sat, 11 Apr 2026 18:16:32 -0700 Subject: [PATCH 09/50] Import WinMD generator targets from CsWinRT.targets Add import of Microsoft.Windows.CsWinMD.Generator.targets after CsWinRTGen.targets, conditioned on CsWinRTComponent being true. Placed after CsWinRTGen.targets since it depends on shared properties like CsWinRTGenTasksAssembly and _CsWinRTToolsDirectory. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- nuget/Microsoft.Windows.CsWinRT.targets | 1 + 1 file changed, 1 insertion(+) diff --git a/nuget/Microsoft.Windows.CsWinRT.targets b/nuget/Microsoft.Windows.CsWinRT.targets index 8ae76bd4f..d59c77c9c 100644 --- a/nuget/Microsoft.Windows.CsWinRT.targets +++ b/nuget/Microsoft.Windows.CsWinRT.targets @@ -586,6 +586,7 @@ $(CsWinRTInternalProjection) + From ee57a13c13d0c47cbbba0df00313e9f06748f63b Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Sat, 11 Apr 2026 18:50:26 -0700 Subject: [PATCH 10/50] Fix UsingTask condition to use CsWinRTGeneratorTasksOverriden2 CsWinRTGeneratorTasksOverriden is set to true by CsWinRTGen.targets to suppress .NET SDK built-in tasks. The other generator UsingTask entries use CsWinRTGeneratorTasksOverriden2 to avoid being blocked by this. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- nuget/Microsoft.Windows.CsWinMD.Generator.targets | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nuget/Microsoft.Windows.CsWinMD.Generator.targets b/nuget/Microsoft.Windows.CsWinMD.Generator.targets index fdc76e118..5cc6bad54 100644 --- a/nuget/Microsoft.Windows.CsWinMD.Generator.targets +++ b/nuget/Microsoft.Windows.CsWinMD.Generator.targets @@ -70,7 +70,7 @@ Copyright (C) Microsoft Corporation. All rights reserved. This target runs after CoreCompile since it needs the compiled DLL. ============================================================ --> - + Date: Sat, 11 Apr 2026 18:53:18 -0700 Subject: [PATCH 11/50] Fix System.Type attribute arguments to use TypeSignature instead of string AsmResolver requires TypeSignature values for System.Type-typed custom attribute arguments, not raw strings. The ActivatableAttribute, StaticAttribute, and ExclusiveToAttribute were passing qualified type name strings which caused a cast failure during PE image construction. Add ResolveTypeNameToSignature helper that looks up types in the output type mapping first, then falls back to creating a TypeReference. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generation/WinmdWriter.Attributes.cs | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Attributes.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Attributes.cs index 3a2616bde..1fe6ef43b 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Attributes.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Attributes.cs @@ -38,11 +38,8 @@ 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 - using (SHA1 sha = SHA1.Create()) - { - byte[] hash = sha.ComputeHash(Encoding.UTF8.GetBytes(name)); - guid = EncodeGuid(hash); - } + byte[] hash = SHA1.HashData(Encoding.UTF8.GetBytes(name)); + guid = EncodeGuid(hash); #pragma warning restore CA5350 AddGuidAttribute(outputType, guid); @@ -120,7 +117,7 @@ private void AddActivatableAttribute(TypeDefinition outputType, uint version, st _outputModule.CorLibTypeFactory.UInt32)); CustomAttributeSignature sig = new(); - sig.FixedArguments.Add(new CustomAttributeArgument(systemType.ToTypeSignature(), factoryInterface)); + sig.FixedArguments.Add(new CustomAttributeArgument(systemType.ToTypeSignature(), ResolveTypeNameToSignature(factoryInterface))); sig.FixedArguments.Add(new CustomAttributeArgument(_outputModule.CorLibTypeFactory.UInt32, version)); outputType.CustomAttributes.Add(new CustomAttribute(ctor, sig)); @@ -153,7 +150,7 @@ private void AddStaticAttribute(TypeDefinition classOutputType, uint version, st _outputModule.CorLibTypeFactory.UInt32)); CustomAttributeSignature sig = new(); - sig.FixedArguments.Add(new CustomAttributeArgument(systemType.ToTypeSignature(), staticInterfaceName)); + sig.FixedArguments.Add(new CustomAttributeArgument(systemType.ToTypeSignature(), ResolveTypeNameToSignature(staticInterfaceName))); sig.FixedArguments.Add(new CustomAttributeArgument(_outputModule.CorLibTypeFactory.UInt32, version)); classOutputType.CustomAttributes.Add(new CustomAttribute(ctor, sig)); @@ -171,7 +168,7 @@ private void AddExclusiveToAttribute(TypeDefinition interfaceType, string classN systemType.ToTypeSignature())); CustomAttributeSignature sig = new(); - sig.FixedArguments.Add(new CustomAttributeArgument(systemType.ToTypeSignature(), className)); + sig.FixedArguments.Add(new CustomAttributeArgument(systemType.ToTypeSignature(), ResolveTypeNameToSignature(className))); interfaceType.CustomAttributes.Add(new CustomAttribute(ctor, sig)); } @@ -376,4 +373,23 @@ private static Guid EncodeGuid(byte[] hash) return new Guid(guidBytes); } + + /// + /// Resolves a fully-qualified type name to a for use in custom attribute arguments. + /// + private TypeSignature ResolveTypeNameToSignature(string qualifiedTypeName) + { + // Check if we have this type in our output type definitions + if (_typeDefinitionMapping.TryGetValue(qualifiedTypeName, out TypeDeclaration? declaration) && declaration.OutputType != null) + { + return declaration.OutputType.ToTypeSignature(); + } + + // Parse the qualified name into namespace and type name, then create a reference + int lastDot = qualifiedTypeName.LastIndexOf('.'); + string ns = lastDot > 0 ? qualifiedTypeName[..lastDot] : ""; + string name = lastDot > 0 ? qualifiedTypeName[(lastDot + 1)..] : qualifiedTypeName; + + return GetOrCreateTypeReference(ns, name, _assemblyName).ToTypeSignature(); + } } \ No newline at end of file From 841910dba9cf850759bca1d34ddd0fbca576f471 Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Sat, 11 Apr 2026 19:30:26 -0700 Subject: [PATCH 12/50] Fix GuidAttribute detection for explicit user-specified GUIDs AsmResolver stores string custom attribute arguments as Utf8String, not System.String. The 'is string' pattern match was always failing, causing explicit [Guid] attributes to be ignored and SHA1-generated GUIDs to be used instead. Use ToString() on the element value to handle both Utf8String and string representations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generation/WinmdWriter.Attributes.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Attributes.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Attributes.cs index 1fe6ef43b..614f03f47 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Attributes.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Attributes.cs @@ -20,8 +20,8 @@ private void AddGuidAttribute(TypeDefinition outputType, TypeDefinition inputTyp { if (attr.Constructor?.DeclaringType?.FullName == "System.Runtime.InteropServices.GuidAttribute" && attr.Signature?.FixedArguments.Count > 0 && - attr.Signature.FixedArguments[0].Element is string guidString && - Guid.TryParse(guidString, out Guid guid)) + attr.Signature.FixedArguments[0].Element is { } guidElement && + Guid.TryParse(guidElement.ToString(), out Guid guid)) { AddGuidAttribute(outputType, guid); return; From bdc92c0abd2645aa50917f4cf29eea6a2bf3738a Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Sat, 11 Apr 2026 20:01:25 -0700 Subject: [PATCH 13/50] Fix GUID encoding, delegate ctor, and delegate parameter attributes Three differences found when comparing new WinMD output against old: 1. EncodeGuid: Add byte-swapping for little-endian systems to match the UUID v5 spec and the original source generator. The Guid(byte[]) constructor interprets bytes 0-3 as little-endian int32, 4-5 as little-endian int16, 6-7 as little-endian int16, so the SHA1 hash bytes (big-endian) must be swapped before constructing the GUID. 2. Delegate .ctor: Change from Public to Private and add parameter definitions for (object, IntPtr) to match WinRT delegate convention and the original source generator output. 3. Delegate Invoke parameters: Mark all parameters with ParameterAttributes.In to match the original source generator output. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generation/WinmdWriter.Attributes.cs | 22 +++++++++++++++++-- .../Generation/WinmdWriter.Types.cs | 10 +++++---- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Attributes.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Attributes.cs index 614f03f47..566c39fce 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Attributes.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Attributes.cs @@ -366,8 +366,26 @@ private static Guid EncodeGuid(byte[] hash) byte[] guidBytes = new byte[16]; Array.Copy(hash, guidBytes, 16); - // Set version to 5 (SHA1) - guidBytes[7] = (byte)((guidBytes[7] & 0x0F) | 0x50); + 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); diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs index 410a1fffe..913d7ded9 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs @@ -108,10 +108,10 @@ private void AddDelegateType(TypeDefinition inputType) typeAttributes, baseType); - // Add .ctor(object, IntPtr) + // Add .ctor(object, IntPtr) — private per WinRT delegate convention MethodDefinition ctor = new( ".ctor", - MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RuntimeSpecialName, + MethodAttributes.Private | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RuntimeSpecialName, MethodSignature.CreateInstance( _outputModule.CorLibTypeFactory.Void, _outputModule.CorLibTypeFactory.Object, @@ -119,6 +119,8 @@ private void AddDelegateType(TypeDefinition inputType) { 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 @@ -140,14 +142,14 @@ private void AddDelegateType(TypeDefinition inputType) ImplAttributes = MethodImplAttributes.Runtime | MethodImplAttributes.Managed }; - // Add parameter names + // 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, - inputParam.Attributes)); + ParameterAttributes.In)); } outputType.Methods.Add(invoke); From abf6da0693bc490bba4d4e2358975c96b55bab02 Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Sat, 11 Apr 2026 20:14:05 -0700 Subject: [PATCH 14/50] Set WinMD metadata version to WindowsRuntime 1.4 The output module was using the default metadata version 'v4.0.30319' instead of 'WindowsRuntime 1.4'. This version string is how tools like ILSpy, MIDL, and the CLR itself identify a PE file as a WinMD rather than a regular .NET assembly. Without it, the file is treated as a normal assembly and WinRT-specific decompilation and activation behavior does not apply. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generation/WinmdWriter.cs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs index 34c42fdcd..1385b53da 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs @@ -46,7 +46,10 @@ public WinmdWriter( _inputModule = inputModule; // Create the output WinMD module - _outputModule = new ModuleDefinition(assemblyName + ".winmd"); + _outputModule = new ModuleDefinition(assemblyName + ".winmd") + { + RuntimeVersion = "WindowsRuntime 1.4" + }; // Create the output assembly with WindowsRuntime flag (keep reference alive via module) _ = new AssemblyDefinition(assemblyName, new Version(version)) @@ -75,7 +78,7 @@ public void ProcessType(TypeDefinition inputType) { AddEnumType(inputType); } - else if (AssemblyAnalyzer.IsDelegate(inputType)) + else if (inputType.IsDelegate) { AddDelegateType(inputType); } @@ -83,7 +86,7 @@ public void ProcessType(TypeDefinition inputType) { AddInterfaceType(inputType); } - else if (inputType.IsValueType && !inputType.IsEnum) + else if (inputType.IsValueType) { AddStructType(inputType); } @@ -112,12 +115,7 @@ public void FinalizeGeneration() // Add MethodImpls for implemented interfaces foreach (InterfaceImplementation classInterfaceImpl in classOutputType.Interfaces) { - if (classInterfaceImpl.Interface == null) - { - continue; - } - - TypeDefinition? interfaceDef = classInterfaceImpl.Interface.Resolve(); + TypeDefinition? interfaceDef = classInterfaceImpl.Interface?.Resolve(); if (interfaceDef == null) { continue; From 466b802d83c57de62263a5e4da62a3be0b1c53e8 Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Sat, 11 Apr 2026 20:18:22 -0700 Subject: [PATCH 15/50] Set assembly hash algorithm to SHA1 for WinMD output The old source generator defaults to SHA1 via System.Reflection.Metadata. Match this for consistency in the generated WinMD. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs index 1385b53da..c89a22ab0 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs @@ -55,7 +55,8 @@ public WinmdWriter( _ = new AssemblyDefinition(assemblyName, new Version(version)) { Modules = { _outputModule }, - Attributes = AssemblyAttributes.ContentWindowsRuntime // WindowsRuntime + Attributes = AssemblyAttributes.ContentWindowsRuntime, + HashAlgorithm = AsmResolver.PE.DotNet.Metadata.Tables.AssemblyHashAlgorithm.Sha1 }; // Add the type From d7f6793a4c558cccf49c9f3b32fc133299123cd9 Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Sat, 11 Apr 2026 20:20:49 -0700 Subject: [PATCH 16/50] Fix MethodImplementation argument order (declaration, body) AsmResolver's MethodImplementation constructor takes (declaration, body) where declaration is the interface method and body is the class method. Our code had them reversed, causing the .override directives to point the wrong way. Confirmed by comparing with Interop.Generator usage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs index c89a22ab0..9966a415c 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs @@ -129,7 +129,7 @@ public void FinalizeGeneration() if (classMethod != null) { MemberReference interfaceMethodRef = new(classInterfaceImpl.Interface, interfaceMethod.Name!.Value, interfaceMethod.Signature); - classOutputType.MethodImplementations.Add(new MethodImplementation(classMethod, interfaceMethodRef)); + classOutputType.MethodImplementations.Add(new MethodImplementation(interfaceMethodRef, classMethod)); } } } @@ -151,7 +151,7 @@ public void FinalizeGeneration() if (classMethod != null) { MemberReference interfaceMethodRef = new(interfaceRef, interfaceMethod.Name!.Value, interfaceMethod.Signature); - classOutputType.MethodImplementations.Add(new MethodImplementation(classMethod, interfaceMethodRef)); + classOutputType.MethodImplementations.Add(new MethodImplementation(interfaceMethodRef, classMethod)); } } } From 5909a236ddca4fe112cd9114022ea7c6bd827d49 Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Sat, 11 Apr 2026 20:45:48 -0700 Subject: [PATCH 17/50] Reference synthesized interfaces as TypeDefinition instead of TypeReference Synthesized interfaces (IFooClass, IFooStatic, IFooFactory) are defined in the same WinMD module. Using GetOrCreateTypeReference created external TypeReference entries pointing back to the same assembly, causing ILSpy to display them with full namespace qualification. Use the TypeDefinition directly for both the interface implementation and MethodImpl records. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generation/WinmdWriter.SynthesizedInterfaces.cs | 5 ++--- src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs | 6 +----- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.SynthesizedInterfaces.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.SynthesizedInterfaces.cs index bdb3cb3c8..b5dac56ff 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.SynthesizedInterfaces.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.SynthesizedInterfaces.cs @@ -187,9 +187,8 @@ private void AddSynthesizedInterface( { classDeclaration.DefaultInterface = qualifiedInterfaceName; - // Add interface implementation on the class - TypeReference interfaceRef = GetOrCreateTypeReference(ns, interfaceName, _assemblyName); - InterfaceImplementation interfaceImpl = new(interfaceRef); + // Add interface implementation on the class (use the TypeDefinition directly since it's in the same module) + InterfaceImplementation interfaceImpl = new(synthesizedInterface); classOutputType.Interfaces.Add(interfaceImpl); // Add DefaultAttribute on the interface implementation diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs index 9966a415c..493c2fb10 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs @@ -140,17 +140,13 @@ public void FinalizeGeneration() defaultInterfaceDecl.OutputType != null) { TypeDefinition defaultInterface = defaultInterfaceDecl.OutputType; - ITypeDefOrRef interfaceRef = GetOrCreateTypeReference( - defaultInterface.Namespace?.Value ?? "", - defaultInterface.Name!.Value, - _assemblyName); foreach (MethodDefinition interfaceMethod in defaultInterface.Methods) { MethodDefinition? classMethod = FindMatchingMethod(classOutputType, interfaceMethod); if (classMethod != null) { - MemberReference interfaceMethodRef = new(interfaceRef, interfaceMethod.Name!.Value, interfaceMethod.Signature); + MemberReference interfaceMethodRef = new(defaultInterface, interfaceMethod.Name!.Value, interfaceMethod.Signature); classOutputType.MethodImplementations.Add(new MethodImplementation(interfaceMethodRef, classMethod)); } } From 31f151df060f4ebb175eb3a547e5888fb9851357 Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Sat, 11 Apr 2026 23:54:16 -0700 Subject: [PATCH 18/50] Fix self-referencing TypeRef/AssemblyRef and duplicate MethodImpls Several fixes for correct same-module type references in WinMD output: - Register class TypeDefinition in _typeDefinitionMapping early (before processing methods/properties/events) so self-referencing return types like TypeOnlyActivatableViaItsOwnFactory.Create() resolve to the output TypeDefinition instead of creating a TypeRef to self - Add _typeDefinitionMapping lookup in ImportTypeReference's TypeReference branch to catch same-module type references from input assembly - Use output TypeDefinition directly in AddFactoryMethod for the return type instead of going through ImportTypeReference - Use TypeDefinition.ToTypeSignature() in ResolveTypeNameToSignature without a fallback to GetOrCreateTypeReference, preventing self-ref AssemblyRef creation from attribute arguments - Skip default synthesized interface in the first MethodImpl loop since it is already handled by the dedicated second loop, fixing duplicate MethodImpl records - Remove unused _assemblyName field Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generation/WinmdWriter.Attributes.cs | 16 ++++------------ .../WinmdWriter.SynthesizedInterfaces.cs | 5 ++++- .../Generation/WinmdWriter.TypeMapping.cs | 8 ++++++++ .../Generation/WinmdWriter.Types.cs | 10 +++++----- .../Generation/WinmdWriter.cs | 11 ++++++++--- 5 files changed, 29 insertions(+), 21 deletions(-) diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Attributes.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Attributes.cs index 566c39fce..1d95e953b 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Attributes.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Attributes.cs @@ -394,20 +394,12 @@ private static Guid EncodeGuid(byte[] hash) /// /// 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) { - // Check if we have this type in our output type definitions - if (_typeDefinitionMapping.TryGetValue(qualifiedTypeName, out TypeDeclaration? declaration) && declaration.OutputType != null) - { - return declaration.OutputType.ToTypeSignature(); - } - - // Parse the qualified name into namespace and type name, then create a reference - int lastDot = qualifiedTypeName.LastIndexOf('.'); - string ns = lastDot > 0 ? qualifiedTypeName[..lastDot] : ""; - string name = lastDot > 0 ? qualifiedTypeName[(lastDot + 1)..] : qualifiedTypeName; - - return GetOrCreateTypeReference(ns, name, _assemblyName).ToTypeSignature(); + 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.SynthesizedInterfaces.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.SynthesizedInterfaces.cs index b5dac56ff..c1f5fac56 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.SynthesizedInterfaces.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.SynthesizedInterfaces.cs @@ -218,7 +218,10 @@ private void AddSynthesizedInterface( private void AddFactoryMethod(TypeDefinition synthesizedInterface, TypeDefinition classType, MethodDefinition constructor) { - TypeSignature returnType = ImportTypeReference(classType).ToTypeSignature(); + // 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)]; diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.TypeMapping.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.TypeMapping.cs index 85b372a73..089886d75 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.TypeMapping.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.TypeMapping.cs @@ -109,6 +109,14 @@ private ITypeDefOrRef ImportTypeReference(ITypeDefOrRef type) { 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; + } + string assembly = GetAssemblyNameFromScope(typeRef.Scope); return GetOrCreateTypeReference(ns, name, assembly); } diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs index 913d7ded9..7282f6408 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs @@ -310,6 +310,11 @@ private void AddClassType(TypeDefinition inputType) 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; @@ -400,11 +405,6 @@ private void AddClassType(TypeDefinition inputType) outputType.Interfaces.Add(new InterfaceImplementation(outputInterfaceRef)); } - _outputModule.TopLevelTypes.Add(outputType); - - TypeDeclaration declaration = new(inputType, outputType, isComponentType: true); - _typeDefinitionMapping[qualifiedName] = declaration; - // Add activatable attribute if it has a default constructor if (hasDefaultConstructor) { diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs index 493c2fb10..14692be99 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs @@ -18,7 +18,6 @@ namespace WindowsRuntime.WinMDGenerator.Generation; /// internal sealed partial class WinmdWriter { - private readonly string _assemblyName; private readonly string _version; private readonly TypeMapper _mapper; private readonly ModuleDefinition _inputModule; @@ -40,7 +39,6 @@ public WinmdWriter( TypeMapper mapper, ModuleDefinition inputModule) { - _assemblyName = assemblyName; _version = version; _mapper = mapper; _inputModule = inputModule; @@ -113,7 +111,7 @@ public void FinalizeGeneration() TypeDefinition classOutputType = declaration.OutputType; TypeDefinition classInputType = declaration.InputType; - // Add MethodImpls for implemented interfaces + // Add MethodImpls for implemented interfaces (excluding the default synthesized interface, handled below) foreach (InterfaceImplementation classInterfaceImpl in classOutputType.Interfaces) { TypeDefinition? interfaceDef = classInterfaceImpl.Interface?.Resolve(); @@ -122,6 +120,13 @@ public void FinalizeGeneration() continue; } + // Skip the default synthesized interface — it's handled separately below + string interfaceQualName = AssemblyAnalyzer.GetQualifiedName(interfaceDef); + if (interfaceQualName == declaration.DefaultInterface) + { + continue; + } + foreach (MethodDefinition interfaceMethod in interfaceDef.Methods) { // Find the corresponding method on the class From a337574b8c383115ac3429eea7c0924350489fb0 Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Sun, 12 Apr 2026 00:07:10 -0700 Subject: [PATCH 19/50] Fix enum field types and add DefaultAttribute on first user interface Two differences found in thorough WinMD comparison: 1. Enum literal fields: Use the enum type itself (e.g. AuthoringTest.BasicEnum) as the field type instead of the underlying primitive (System.Int32). The value__ field still uses the underlying type. Also moved type registration before field creation so the TypeDefinition is available for the signature. 2. DefaultAttribute: When a class implements user interfaces but has no synthesized default interface (all instance members come from interfaces), mark the first user interface implementation as [Default]. This matches the old source generator behavior where ICustomInterfaceGuid gets [Default] on CustomInterfaceGuidClass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generation/WinmdWriter.Types.cs | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs index 7282f6408..ceb9d8079 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs @@ -43,6 +43,14 @@ private void AddEnumType(TypeDefinition inputType) 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) { @@ -59,7 +67,7 @@ private void AddEnumType(TypeDefinition inputType) FieldDefinition outputField = new( field.Name!.Value, FieldAttributes.Public | FieldAttributes.Static | FieldAttributes.Literal | FieldAttributes.HasDefault, - new FieldSignature(underlyingType)); + new FieldSignature(enumTypeSignature)); if (field.Constant != null) { @@ -68,11 +76,6 @@ private void AddEnumType(TypeDefinition inputType) outputType.Fields.Add(outputField); } - - _outputModule.TopLevelTypes.Add(outputType); - - TypeDeclaration declaration = new(inputType, outputType, isComponentType: true); - _typeDefinitionMapping[qualifiedName] = declaration; } private TypeSignature GetEnumUnderlyingType(TypeDefinition enumType) @@ -417,5 +420,12 @@ private void AddClassType(TypeDefinition inputType) // Add synthesized interfaces (IFooClass, IFooFactory, IFooStatic) AddSynthesizedInterfaces(inputType, outputType, declaration); + + // If no default synthesized interface was created but the class implements + // user interfaces, mark the first interface implementation as [Default] + if (declaration.DefaultInterface == null && outputType.Interfaces.Count > 0) + { + AddDefaultAttribute(outputType.Interfaces[0]); + } } } \ No newline at end of file From d6c5faf4f21ba67f174809277c827749c559d058 Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Sun, 12 Apr 2026 00:16:19 -0700 Subject: [PATCH 20/50] Fix duplicate Module type and duplicate mscorlib AssemblyRef - Remove explicit type addition since AsmResolver's ModuleDefinition constructor already creates one automatically - Mutate the default CorLib AssemblyReference in-place to use mscorlib v255.255.255.255 with PKT, matching WinMD convention, instead of creating a separate AssemblyReference - Cache the mutated CorLib reference in _assemblyReferenceCache so GetOrCreateAssemblyReference reuses it Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs index 14692be99..5a8279452 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs @@ -49,6 +49,12 @@ public WinmdWriter( 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)) { @@ -56,9 +62,6 @@ public WinmdWriter( Attributes = AssemblyAttributes.ContentWindowsRuntime, HashAlgorithm = AsmResolver.PE.DotNet.Metadata.Tables.AssemblyHashAlgorithm.Sha1 }; - - // Add the type - _outputModule.TopLevelTypes.Add(new TypeDefinition(null, "", 0)); } /// From c7faf165fe2c60854cc545b1ce85ed8374e9d2fb Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Sun, 12 Apr 2026 00:27:19 -0700 Subject: [PATCH 21/50] Fix parameter attributes and early type registration for all type kinds Two fixes from comparison with new types: 1. Mark all method parameters as ParameterAttributes.In in interface methods, class methods, and factory methods. The old source generator uses [In] for all non-out parameters. Our code was copying the input assembly's parameter attributes which are 0 (None) in compiled .NET. 2. Register all type kinds (interfaces, structs, delegates) in _typeDefinitionMapping early (before processing members), not just classes. This prevents self-referencing TypeRef/AssemblyRef entries when a type is referenced by another type's method signature before being processed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generation/WinmdWriter.Members.cs | 4 +-- .../WinmdWriter.SynthesizedInterfaces.cs | 5 ++-- .../Generation/WinmdWriter.Types.cs | 30 +++++++++---------- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Members.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Members.cs index 51e51383f..946b07203 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Members.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Members.cs @@ -47,7 +47,7 @@ private void AddMethodToInterface(TypeDefinition outputType, MethodDefinition in outputMethod.ParameterDefinitions.Add(new ParameterDefinition( (ushort)paramIndex++, inputParam.Name!.Value, - inputParam.Attributes)); + ParameterAttributes.In)); } outputType.Methods.Add(outputMethod); @@ -105,7 +105,7 @@ private void AddMethodToClass(TypeDefinition outputType, MethodDefinition inputM outputMethod.ParameterDefinitions.Add(new ParameterDefinition( (ushort)paramIndex++, inputParam.Name!.Value, - inputParam.Attributes)); + ParameterAttributes.In)); } outputType.Methods.Add(outputMethod); diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.SynthesizedInterfaces.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.SynthesizedInterfaces.cs index c1f5fac56..badbcdc02 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.SynthesizedInterfaces.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.SynthesizedInterfaces.cs @@ -8,6 +8,7 @@ using WindowsRuntime.WinMDGenerator.Discovery; using WindowsRuntime.WinMDGenerator.Models; using MethodAttributes = AsmResolver.PE.DotNet.Metadata.Tables.MethodAttributes; +using ParameterAttributes = AsmResolver.PE.DotNet.Metadata.Tables.ParameterAttributes; using TypeAttributes = AsmResolver.PE.DotNet.Metadata.Tables.TypeAttributes; namespace WindowsRuntime.WinMDGenerator.Generation; @@ -231,14 +232,14 @@ private void AddFactoryMethod(TypeDefinition synthesizedInterface, TypeDefinitio MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.Abstract | MethodAttributes.Virtual | MethodAttributes.NewSlot, MethodSignature.CreateInstance(returnType, parameterTypes)); - // Add parameter names + // Add parameter names with [In] attribute int paramIndex = 1; foreach (ParameterDefinition inputParam in constructor.ParameterDefinitions) { factoryMethod.ParameterDefinitions.Add(new ParameterDefinition( (ushort)paramIndex++, inputParam.Name!.Value, - inputParam.Attributes)); + ParameterAttributes.In)); } synthesizedInterface.Methods.Add(factoryMethod); diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs index ceb9d8079..ae20ddf7b 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs @@ -111,6 +111,11 @@ private void AddDelegateType(TypeDefinition inputType) 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", @@ -158,11 +163,6 @@ private void AddDelegateType(TypeDefinition inputType) outputType.Methods.Add(invoke); } - _outputModule.TopLevelTypes.Add(outputType); - - TypeDeclaration declaration = new(inputType, outputType, isComponentType: true); - _typeDefinitionMapping[qualifiedName] = declaration; - // Add GUID attribute AddGuidAttribute(outputType, inputType); } @@ -184,6 +184,11 @@ private void AddInterfaceType(TypeDefinition 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 foreach (MethodDefinition method in inputType.Methods) { @@ -217,11 +222,6 @@ private void AddInterfaceType(TypeDefinition inputType) } } - _outputModule.TopLevelTypes.Add(outputType); - - TypeDeclaration declaration = new(inputType, outputType, isComponentType: true); - _typeDefinitionMapping[qualifiedName] = declaration; - // Add GUID attribute AddGuidAttribute(outputType, inputType); } @@ -245,6 +245,11 @@ private void AddStructType(TypeDefinition inputType) 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) { @@ -259,11 +264,6 @@ private void AddStructType(TypeDefinition inputType) new FieldSignature(MapTypeSignatureToOutput(field.Signature!.FieldType))); outputType.Fields.Add(outputField); } - - _outputModule.TopLevelTypes.Add(outputType); - - TypeDeclaration declaration = new(inputType, outputType, isComponentType: true); - _typeDefinitionMapping[qualifiedName] = declaration; } private void AddClassType(TypeDefinition inputType) From 092c4d97773f6fad3cbf7e60e66cc158abac299e Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Sun, 12 Apr 2026 00:32:15 -0700 Subject: [PATCH 22/50] Process forward-referenced types on demand in ImportTypeReference When a method signature references a public type from the input assembly that hasn't been processed yet (e.g. PartialClass.GetPartialStruct() referencing PartialStruct which appears later), process it on demand via ProcessType instead of creating a self-referencing TypeRef. This eliminates the self-referencing AssemblyReference entry. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generation/WinmdWriter.TypeMapping.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.TypeMapping.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.TypeMapping.cs index 089886d75..9cabf0407 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.TypeMapping.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.TypeMapping.cs @@ -98,7 +98,17 @@ private ITypeDefOrRef ImportTypeReference(ITypeDefOrRef type) return declaration.OutputType; } - // Otherwise create a type reference + // 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, From adae9f0fb084b494108356bf291cae541d5c6ed9 Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Sun, 12 Apr 2026 00:46:31 -0700 Subject: [PATCH 23/50] Minor cleanup in AssemblyAnalyzer and TypeMapper Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Discovery/AssemblyAnalyzer.cs | 29 +++++-------------- .../Models/TypeMapper.cs | 1 - 2 files changed, 7 insertions(+), 23 deletions(-) diff --git a/src/WinRT.WinMD.Generator/Discovery/AssemblyAnalyzer.cs b/src/WinRT.WinMD.Generator/Discovery/AssemblyAnalyzer.cs index 2f25e3b99..10a4495d9 100644 --- a/src/WinRT.WinMD.Generator/Discovery/AssemblyAnalyzer.cs +++ b/src/WinRT.WinMD.Generator/Discovery/AssemblyAnalyzer.cs @@ -56,7 +56,7 @@ private static void CollectPublicTypes(TypeDefinition type, List } // We include classes, interfaces, structs, enums, and delegates - if (type.IsClass || type.IsInterface || type.IsValueType || type.IsEnum || IsDelegate(type)) + if (type.IsClass || type.IsInterface || type.IsValueType || type.IsEnum || type.IsDelegate) { publicTypes.Add(type); } @@ -76,14 +76,6 @@ private static bool IsPublicType(TypeDefinition type) return type.IsPublic || type.IsNestedPublic; } - /// - /// Checks whether a type is a delegate (inherits from System.MulticastDelegate). - /// - internal static bool IsDelegate(TypeDefinition type) - { - return type.BaseType?.FullName == "System.MulticastDelegate"; - } - /// /// Checks whether a type is a WinRT type (has the WindowsRuntimeTypeAttribute). /// @@ -149,9 +141,7 @@ internal static string GetQualifiedName(TypeDefinition type) } string? ns = GetEffectiveNamespace(type); - return ns is { Length: > 0 } - ? $"{ns}.{name}" - : name; + return ns is { Length: > 0 } ? $"{ns}.{name}" : name; } /// @@ -160,19 +150,14 @@ internal static string GetQualifiedName(TypeDefinition type) /// internal static string GetQualifiedName(ITypeDefOrRef type) { - string name = type.Name!.Value; - if (type is TypeDefinition td && td.GenericParameters.Count > 0) + if (type is TypeDefinition td) { - name += $"`{td.GenericParameters.Count}"; + return GetQualifiedName(td); } - // For TypeDefinition, use GetEffectiveNamespace to handle nested types - string? ns = type is TypeDefinition typeDef - ? GetEffectiveNamespace(typeDef) - : type.Namespace?.Value; + string name = type.Name!.Value; + string? ns = type.Namespace?.Value; - return ns is { Length: > 0 } - ? $"{ns}.{name}" - : name; + return ns is { Length: > 0 } ? $"{ns}.{name}" : name; } } \ No newline at end of file diff --git a/src/WinRT.WinMD.Generator/Models/TypeMapper.cs b/src/WinRT.WinMD.Generator/Models/TypeMapper.cs index 842e0a9d8..c0e81bf16 100644 --- a/src/WinRT.WinMD.Generator/Models/TypeMapper.cs +++ b/src/WinRT.WinMD.Generator/Models/TypeMapper.cs @@ -217,6 +217,5 @@ public MappedType GetMappedType(string typeName) "System.IEquatable`1", "System.Runtime.InteropServices.ICustomQueryInterface", "System.Runtime.InteropServices.IDynamicInterfaceCastable", - "WinRT.IWinRTObject" }; } \ No newline at end of file From a8d1eba8b0e597d96cb86f6b1f71da828b2dc097 Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Sun, 12 Apr 2026 00:54:57 -0700 Subject: [PATCH 24/50] Implement custom mapped interface members for WinMD generation Port the WriteCustomMappedTypeMembers functionality from the original source generator. When a class implements .NET interfaces that map to WinRT equivalents (IDictionary->IMap, IList->IVector, IDisposable-> IClosable, etc.), the WinMD must contain explicit implementation methods with WinRT-style signatures and MethodImpl records. New file WinmdWriter.CustomMappedMembers.cs handles: - All 15 mapped interface types (IClosable, IIterable, IMap, IMapView, IVector, IVectorView, IBindableIterable, IBindableVector, INotifyPropertyChanged, ICommand, INotifyCollectionChanged, INotifyDataErrorInfo, IXamlServiceProvider, IIterator) - Public vs explicit (private) implementation based on whether the class publicly exposes the mapped members - Generic type argument mapping (KeyValuePair->IKeyValuePair) - Filtering duplicate IBindableIterable when IIterable present - Events with EventRegistrationToken pattern - Properties with get/put accessors - MethodImpl records for all mapped methods Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../WinmdWriter.CustomMappedMembers.cs | 444 ++++++++++++++++++ .../WinmdWriter.SynthesizedInterfaces.cs | 42 -- 2 files changed, 444 insertions(+), 42 deletions(-) create mode 100644 src/WinRT.WinMD.Generator/Generation/WinmdWriter.CustomMappedMembers.cs 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 000000000..238eb9ba2 --- /dev/null +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.CustomMappedMembers.cs @@ -0,0 +1,444 @@ +// 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 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) + { + // 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 inputType.Interfaces) + { + 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 by checking if any + // implementing member on the class is public + bool isPublic = false; + TypeDefinition? interfaceDef = impl.Interface is TypeSpecification ts + ? (ts.Signature as GenericInstanceTypeSignature)?.GenericType.Resolve() + : impl.Interface.Resolve(); + + if (interfaceDef != null) + { + foreach (MethodDefinition interfaceMethod in interfaceDef.Methods) + { + // Check if the class has a public method that could implement this interface member + string methodName = interfaceMethod.Name?.Value ?? ""; + if (inputType.Methods.Any(m => m.IsPublic && m.Name?.Value == methodName)) + { + isPublic = true; + break; + } + } + } + + mappedInterfaces.Add((impl, interfaceName, mapping, isPublic)); + } + + // If generic IEnumerable (IIterable) is present, skip non-generic IEnumerable (IBindableIterable) + bool hasGenericEnumerable = mappedInterfaces.Any(m => + m.interfaceName == "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); + + 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(); + } + } + + // 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 + string qualifiedPrefix = mappedInterfaceRef.FullName ?? mappedTypeName; + + 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 + MemberReference interfaceMethodRef = new(mappedInterfaceRef, name, MethodSignature.CreateInstance(returnType ?? _outputModule.CorLibTypeFactory.Void, paramTypes)); + 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 + MemberReference getterRef = new(mappedInterfaceRef, $"get_{name}", MethodSignature.CreateInstance(propertyType)); + 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, propertyType)); + 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) => + GetOrCreateTypeReference(ns, name, asm).ToTypeSignature(); + + 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"), 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(); + + 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 + MemberReference addRef = new(mappedInterfaceRef, $"add_{eventName}", MethodSignature.CreateInstance(tokenSig, handlerType)); + 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)); + } +} \ 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 index badbcdc02..8d0879c19 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.SynthesizedInterfaces.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.SynthesizedInterfaces.cs @@ -6,7 +6,6 @@ using AsmResolver.DotNet; using AsmResolver.DotNet.Signatures; using WindowsRuntime.WinMDGenerator.Discovery; -using WindowsRuntime.WinMDGenerator.Models; using MethodAttributes = AsmResolver.PE.DotNet.Metadata.Tables.MethodAttributes; using ParameterAttributes = AsmResolver.PE.DotNet.Metadata.Tables.ParameterAttributes; using TypeAttributes = AsmResolver.PE.DotNet.Metadata.Tables.TypeAttributes; @@ -245,47 +244,6 @@ private void AddFactoryMethod(TypeDefinition synthesizedInterface, TypeDefinitio synthesizedInterface.Methods.Add(factoryMethod); } - /// - /// Processes custom mapped interfaces for a class type. This maps .NET collection interfaces, - /// IDisposable, INotifyPropertyChanged, etc. to their WinRT equivalents. - /// - private void ProcessCustomMappedInterfaces(TypeDefinition inputType, TypeDefinition outputType) - { - foreach (InterfaceImplementation impl in inputType.Interfaces) - { - if (impl.Interface == null) - { - continue; - } - - string interfaceName = GetInterfaceQualifiedName(impl.Interface); - - if (!_mapper.HasMappingForType(interfaceName)) - { - continue; - } - - MappedType mapping = _mapper.GetMappedType(interfaceName); - (string mappedNs, string mappedName, string mappedAssembly, _, _) = mapping.GetMapping(); - - // Add the mapped interface as an implementation on the output type - TypeReference mappedInterfaceRef = GetOrCreateTypeReference(mappedNs, mappedName, mappedAssembly); - - // For generic interfaces, we need to handle type arguments - if (impl.Interface is TypeSpecification typeSpec && typeSpec.Signature is GenericInstanceTypeSignature genericInst) - { - TypeSignature[] mappedArgs = [.. genericInst.TypeArguments - .Select(MapTypeSignatureToOutput)]; - TypeSpecification mappedSpec = new(new GenericInstanceTypeSignature(mappedInterfaceRef, false, mappedArgs)); - outputType.Interfaces.Add(new InterfaceImplementation(mappedSpec)); - } - else - { - outputType.Interfaces.Add(new InterfaceImplementation(mappedInterfaceRef)); - } - } - } - private static string GetInterfaceQualifiedName(ITypeDefOrRef type) { return type is TypeSpecification typeSpec && typeSpec.Signature is GenericInstanceTypeSignature genericInst From 6df117ef0ae81cbbb26b3fc9db1f439a4a3ecce8 Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Sun, 12 Apr 2026 00:58:57 -0700 Subject: [PATCH 25/50] Gather all interfaces including inherited ones for mapped interface processing Walk the base type chain and interface inheritance to collect all interfaces, not just directly declared ones. This fixes types like TestCollection which inherits IList from a base class the IBindableVector/IBindableIterable mapped members were missing because only direct interfaces were checked. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../WinmdWriter.CustomMappedMembers.cs | 62 ++++++++++++++++++- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.CustomMappedMembers.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.CustomMappedMembers.cs index 238eb9ba2..4594ece4c 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.CustomMappedMembers.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.CustomMappedMembers.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; using System.Collections.Generic; using System.Linq; using AsmResolver.DotNet; @@ -23,10 +24,13 @@ internal sealed partial class WinmdWriter /// 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 inputType.Interfaces) + foreach (InterfaceImplementation impl in allInterfaces) { if (impl.Interface == null) { @@ -53,7 +57,6 @@ private void ProcessCustomMappedInterfaces(TypeDefinition inputType, TypeDefinit { foreach (MethodDefinition interfaceMethod in interfaceDef.Methods) { - // Check if the class has a public method that could implement this interface member string methodName = interfaceMethod.Name?.Value ?? ""; if (inputType.Methods.Any(m => m.IsPublic && m.Name?.Value == methodName)) { @@ -441,4 +444,59 @@ private void AddMappedEvent( 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. This matches the old source generator's AllInterfaces behavior. + /// + private static List GatherAllInterfaces(TypeDefinition type) + { + HashSet seen = new(StringComparer.Ordinal); + List result = []; + + void CollectFromType(TypeDefinition typeDef) + { + foreach (InterfaceImplementation impl in typeDef.Interfaces) + { + if (impl.Interface == null) + { + continue; + } + + string name = impl.Interface.FullName ?? ""; + if (!seen.Add(name)) + { + continue; + } + + if (IsPubliclyAccessible(impl.Interface)) + { + result.Add(impl); + } + + // Also collect interfaces inherited by this interface + TypeDefinition? interfaceDef = impl.Interface is TypeSpecification ts + ? (ts.Signature as GenericInstanceTypeSignature)?.GenericType.Resolve() + : impl.Interface.Resolve(); + + if (interfaceDef != null) + { + CollectFromType(interfaceDef); + } + } + } + + // Collect from the type itself + CollectFromType(type); + + // Walk base types + TypeDefinition? current = type.BaseType?.Resolve(); + while (current != null) + { + CollectFromType(current); + current = current.BaseType?.Resolve(); + } + + return result; + } } \ No newline at end of file From 496f4485e368c2ea6c91226a63996ae2906b2d25 Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Sun, 12 Apr 2026 01:08:04 -0700 Subject: [PATCH 26/50] Fix public detection for mapped interfaces and use short generic type names Two fixes for custom mapped interface methods: 1. Public detection: Walk the class hierarchy when checking if interface members are publicly implemented, fixing inherited interfaces like IList on TestCollection. Also checks .NET interface member names (not WinRT names) against the class methods. 2. Generic type name formatting: Use short type names in explicit method name prefixes (e.g., 'IMap\2' instead of 'IMap\2') to match the old source generator output. These are actual metadata strings that affect tooling. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../WinmdWriter.CustomMappedMembers.cs | 99 +++++++++++++++---- 1 file changed, 78 insertions(+), 21 deletions(-) diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.CustomMappedMembers.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.CustomMappedMembers.cs index 4594ece4c..782469a6c 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.CustomMappedMembers.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.CustomMappedMembers.cs @@ -46,25 +46,11 @@ private void ProcessCustomMappedInterfaces(TypeDefinition inputType, TypeDefinit MappedType mapping = _mapper.GetMappedType(interfaceName); - // Determine if the interface is publicly implemented by checking if any - // implementing member on the class is public - bool isPublic = false; - TypeDefinition? interfaceDef = impl.Interface is TypeSpecification ts - ? (ts.Signature as GenericInstanceTypeSignature)?.GenericType.Resolve() - : impl.Interface.Resolve(); - - if (interfaceDef != null) - { - foreach (MethodDefinition interfaceMethod in interfaceDef.Methods) - { - string methodName = interfaceMethod.Name?.Value ?? ""; - if (inputType.Methods.Any(m => m.IsPublic && m.Name?.Value == methodName)) - { - isPublic = true; - break; - } - } - } + // 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(inputType, impl); mappedInterfaces.Add((impl, interfaceName, mapping, isPublic)); } @@ -155,8 +141,9 @@ private void AddCustomMappedTypeMembers( ITypeDefOrRef mappedInterfaceRef, bool isPublic) { - // Build the qualified prefix for explicit implementation method names - string qualifiedPrefix = mappedInterfaceRef.FullName ?? mappedTypeName; + // 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); void AddMappedMethod(string name, (string name, TypeSignature type, ParameterAttributes attrs)[]? parameters, TypeSignature? returnType) { @@ -499,4 +486,74 @@ void CollectFromType(TypeDefinition typeDef) return result; } + + /// + /// 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) + { + TypeDefinition? interfaceDef = impl.Interface is TypeSpecification ts + ? (ts.Signature as GenericInstanceTypeSignature)?.GenericType.Resolve() + : impl.Interface?.Resolve(); + + 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(); + } + + 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 is TypeDefOrRefSignature tdrs + ? tdrs.Type.Name?.Value ?? sig.FullName + : sig.FullName; + } } \ No newline at end of file From fd9c06b0c566e05a531022d0589bcabd0e910cd9 Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Sun, 12 Apr 2026 01:20:04 -0700 Subject: [PATCH 27/50] Fix PropertySignature for static properties to use CreateStatic Static properties were using PropertySignature.CreateInstance which sets HasThis, but the getter/setter methods use MethodAttributes.Static without HasThis. AsmResolver validates this consistency and throws 'An instance method requires a signature with the HasThis flag set' when a static method has an instance-style property signature. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/WinRT.WinMD.Generator/Generation/WinmdWriter.Members.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Members.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Members.cs index 946b07203..fc70c310b 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Members.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Members.cs @@ -118,12 +118,12 @@ private void AddPropertyToType(TypeDefinition outputType, PropertyDefinition inp { TypeSignature propertyType = MapTypeSignatureToOutput(inputProperty.Signature!.ReturnType); + bool isStatic = inputProperty.GetMethod?.IsStatic == true || inputProperty.SetMethod?.IsStatic == true; + PropertyDefinition outputProperty = new( inputProperty.Name!.Value, 0, - PropertySignature.CreateInstance(propertyType)); - - bool isStatic = inputProperty.GetMethod?.IsStatic == true || inputProperty.SetMethod?.IsStatic == true; + isStatic ? PropertySignature.CreateStatic(propertyType) : PropertySignature.CreateInstance(propertyType)); // Add getter if (inputProperty.GetMethod != null) From 343b99bfdb99a151077806650818c5cafbd0f3b2 Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Sun, 12 Apr 2026 01:23:57 -0700 Subject: [PATCH 28/50] Fix HasThis for static properties/events on synthesized interfaces When a static property or event from a class is added to a synthesized interface (IFooStatic), the method signature must use CreateInstance (HasThis) because interface methods are always instance methods, even when they model static class members. The isStatic flag is now forced to false when isInterfaceParent is true. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generation/WinmdWriter.Members.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Members.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Members.cs index fc70c310b..0c92d8e80 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Members.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Members.cs @@ -118,7 +118,9 @@ private void AddPropertyToType(TypeDefinition outputType, PropertyDefinition inp { TypeSignature propertyType = MapTypeSignatureToOutput(inputProperty.Signature!.ReturnType); - bool isStatic = inputProperty.GetMethod?.IsStatic == true || inputProperty.SetMethod?.IsStatic == true; + // 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, @@ -203,7 +205,8 @@ private void AddEventToType(TypeDefinition outputType, EventDefinition inputEven EventDefinition outputEvent = new(inputEvent.Name!.Value, 0, eventType); - bool isStatic = inputEvent.AddMethod?.IsStatic == true; + // For interface parents (synthesized interfaces), always use instance signatures + bool isStatic = !isInterfaceParent && inputEvent.AddMethod?.IsStatic == true; // Add method { From 1b4b83f0abca3b3581d8bdccd0672c5b20e54fa9 Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Sun, 12 Apr 2026 01:36:38 -0700 Subject: [PATCH 29/50] Apply WinRT type mappings in method signatures and filter mapped members Major fixes for WinMD correctness: 1. Type mapping in signatures: MapTypeSignatureToOutput now checks the TypeMapper for WinRT mappings on TypeDefOrRefSignature and GenericInstanceTypeSignature types. This transforms .NET types like System.IDisposable, IList, Nullable, System.Type to their WinRT equivalents (IClosable, IVector, IReference, TypeName) in all method signatures, property types, and struct fields. 2. Collection member filtering: Class members from custom mapped and unmapped .NET interfaces (Add, Contains, Count, IsReadOnly, GetEnumerator, etc.) are now excluded from the WinMD output via CollectCustomMappedMemberNames. These .NET-specific members are replaced by the WinRT mapped interface members. 3. Interface method deduplication: Skip IsSpecialName methods (property accessors, event accessors) in AddInterfaceType's method loop since they are already created by the property/event processing. This prevents duplicate methods and wrong parameter names. 4. Version attribute from source: FinalizeGeneration now reads the version from the input type's VersionAttribute if present, instead of always using the default version. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../WinmdWriter.CustomMappedMembers.cs | 65 +++++++++++++++++++ .../Generation/WinmdWriter.TypeMapping.cs | 29 ++++++++- .../Generation/WinmdWriter.Types.cs | 26 +++++++- .../Generation/WinmdWriter.cs | 4 +- 4 files changed, 118 insertions(+), 6 deletions(-) diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.CustomMappedMembers.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.CustomMappedMembers.cs index 782469a6c..c16c97851 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.CustomMappedMembers.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.CustomMappedMembers.cs @@ -556,4 +556,69 @@ private static string FormatShortTypeName(TypeSignature sig) ? tdrs.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 + ? (ts.Signature as GenericInstanceTypeSignature)?.GenericType.Resolve() + : impl.Interface.Resolve(); + + 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.TypeMapping.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.TypeMapping.cs index 9cabf0407..da1a09fc7 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.TypeMapping.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.TypeMapping.cs @@ -7,6 +7,7 @@ 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; @@ -55,10 +56,21 @@ private TypeSignature MapTypeSignatureToOutput(TypeSignature inputSig) // Handle generic instance types if (inputSig is GenericInstanceTypeSignature genericInst) { + string genericTypeName = AssemblyAnalyzer.GetQualifiedName(genericInst.GenericType); + + // 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[] mappedArgs = [.. genericInst.TypeArguments - .Select(MapTypeSignatureToOutput)]; - return new GenericInstanceTypeSignature(importedType, genericInst.IsValueType, mappedArgs); + TypeSignature[] importedArgs = [.. genericInst.TypeArguments.Select(MapTypeSignatureToOutput)]; + return new GenericInstanceTypeSignature(importedType, genericInst.IsValueType, importedArgs); } // Handle generic method/type parameters @@ -76,6 +88,17 @@ private TypeSignature MapTypeSignatureToOutput(TypeSignature inputSig) // 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); } diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs index ae20ddf7b..316117c11 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Collections.Generic; using System.Linq; using AsmResolver.DotNet; using AsmResolver.DotNet.Signatures; @@ -189,10 +190,10 @@ private void AddInterfaceType(TypeDefinition inputType) TypeDeclaration declaration = new(inputType, outputType, isComponentType: true); _typeDefinitionMapping[qualifiedName] = declaration; - // Add methods + // Add methods (skip property/event accessors — they're added by property/event processing below) foreach (MethodDefinition method in inputType.Methods) { - if (!method.IsPublic) + if (!method.IsPublic || method.IsSpecialName) { continue; } @@ -323,6 +324,9 @@ private void AddClassType(TypeDefinition inputType) 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) { @@ -340,6 +344,12 @@ private void AddClassType(TypeDefinition inputType) } 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); } } @@ -347,6 +357,12 @@ private void AddClassType(TypeDefinition inputType) // 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; @@ -359,6 +375,12 @@ private void AddClassType(TypeDefinition inputType) // 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) diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs index 5a8279452..a81f32000 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs @@ -173,7 +173,9 @@ public void FinalizeGeneration() if (!HasVersionAttribute(declaration.OutputType)) { - AddVersionAttribute(declaration.OutputType, defaultVersion); + // 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); } } From 6a1ce98bd2751d5ffa5a4d92d56b894eb90d00ec Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Sun, 12 Apr 2026 01:40:55 -0700 Subject: [PATCH 30/50] Filter DefaultMemberAttribute and fix generic type name formatting - Add System.Reflection.DefaultMemberAttribute to the skip list in ShouldCopyAttribute since it's a .NET-specific attribute that shouldn't appear in WinMD output - Fix FormatShortTypeName to only use short names for CorLib primitive types (String, Int32, etc.) and full qualified names for user types (AuthoringTest.DisposableClass), matching the old generator Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generation/WinmdWriter.Attributes.cs | 3 ++- .../Generation/WinmdWriter.CustomMappedMembers.cs | 4 +--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Attributes.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Attributes.cs index 1d95e953b..c0e452195 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Attributes.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Attributes.cs @@ -245,7 +245,8 @@ private static bool ShouldCopyAttribute(CustomAttribute attr) if (attrTypeName is "System.Runtime.InteropServices.GuidAttribute" or "WinRT.GeneratedBindableCustomPropertyAttribute" or - "Windows.Foundation.Metadata.VersionAttribute") + "Windows.Foundation.Metadata.VersionAttribute" or + "System.Reflection.DefaultMemberAttribute") { return false; } diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.CustomMappedMembers.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.CustomMappedMembers.cs index c16c97851..800e93e3d 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.CustomMappedMembers.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.CustomMappedMembers.cs @@ -552,9 +552,7 @@ private static string FormatShortTypeName(TypeSignature sig) return sig is CorLibTypeSignature corLib ? corLib.Type.Name?.Value ?? sig.FullName - : sig is TypeDefOrRefSignature tdrs - ? tdrs.Type.Name?.Value ?? sig.FullName - : sig.FullName; + : sig.FullName; } /// From ef4fe048a9a98f5419527011edfea9a72cc0dbda Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Sun, 12 Apr 2026 01:50:09 -0700 Subject: [PATCH 31/50] Fix DefaultAttribute, explicit interface implementations, and ObservableVector Three fixes: 1. DefaultAttribute: Improved membersFromInterfaces collection to use GatherAllInterfaces, include property/event names, union with custom mapped member names, and detect explicit implementations from compiled IL. This prevents creating empty synthesized default interfaces when all instance members come from user interfaces. 2. Explicit interface implementations: Add private qualified-name methods (e.g., AuthoringTest.IDouble.GetDouble) for types with explicit .NET interface implementations, including properties, events, and MethodImpl records. 3. ObservableVector: Skip adding mapped interface implementations when the output type already implements the WinRT interface through another interface (e.g., IObservableVector already brings IVector). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../WinmdWriter.CustomMappedMembers.cs | 16 +++ .../WinmdWriter.SynthesizedInterfaces.cs | 48 +++++-- .../Generation/WinmdWriter.Types.cs | 134 ++++++++++++++++++ 3 files changed, 190 insertions(+), 8 deletions(-) diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.CustomMappedMembers.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.CustomMappedMembers.cs index 800e93e3d..102460cc6 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.CustomMappedMembers.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.CustomMappedMembers.cs @@ -72,6 +72,22 @@ private void ProcessCustomMappedInterfaces(TypeDefinition inputType, TypeDefinit // 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.) diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.SynthesizedInterfaces.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.SynthesizedInterfaces.cs index 8d0879c19..05f4329da 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.SynthesizedInterfaces.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.SynthesizedInterfaces.cs @@ -38,19 +38,51 @@ private void AddSynthesizedInterfaces(TypeDefinition inputType, TypeDefinition c // Collect members that come from interface implementations HashSet membersFromInterfaces = []; - foreach (InterfaceImplementation impl in inputType.Interfaces) + + // Use all interfaces including inherited ones + List allInterfaces = GatherAllInterfaces(inputType); + foreach (InterfaceImplementation impl in allInterfaces) { - TypeDefinition? interfaceDef = impl.Interface?.Resolve(); - if (interfaceDef == null) + TypeDefinition? interfaceDef = impl.Interface is TypeSpecification ts + ? (ts.Signature as GenericInstanceTypeSignature)?.GenericType.Resolve() + : impl.Interface?.Resolve(); + + if (interfaceDef != null) { - continue; + 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); - foreach (MethodDefinition interfaceMethod in interfaceDef.Methods) + // 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) { - // Find the class member that implements this interface member - string methodName = interfaceMethod.Name?.Value ?? ""; - _ = membersFromInterfaces.Add(methodName); + // 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)..]); + } } } diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs index 316117c11..574689a52 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; using System.Collections.Generic; using System.Linq; using AsmResolver.DotNet; @@ -11,6 +12,8 @@ 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; @@ -443,6 +446,9 @@ private void AddClassType(TypeDefinition inputType) // 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] if (declaration.DefaultInterface == null && outputType.Interfaces.Count > 0) @@ -450,4 +456,132 @@ private void AddClassType(TypeDefinition inputType) AddDefaultAttribute(outputType.Interfaces[0]); } } + + /// + /// Adds explicit interface implementation methods from the input class. + /// These are private methods with qualified names (e.g., "AuthoringTest.IDouble.GetDouble"). + /// + private void AddExplicitInterfaceImplementations(TypeDefinition inputType, TypeDefinition outputType) + { + foreach (MethodDefinition method in inputType.Methods) + { + // Explicit implementations in compiled IL are private methods with dots in the name + if (method.IsPublic || method.Name?.Value?.Contains('.') != true) + { + continue; + } + + string fullMethodName = method.Name.Value; + + // Extract interface name and method name (e.g., "AuthoringTest.IDouble.GetDouble" -> "AuthoringTest.IDouble" + "GetDouble") + int lastDot = fullMethodName.LastIndexOf('.'); + if (lastDot <= 0) + { + continue; + } + + string interfaceQualName = fullMethodName[..lastDot]; + string shortMethodName = fullMethodName[(lastDot + 1)..]; + + // Check if this interface is one we've processed (skip mapped/unmapped interfaces) + if (!_typeDefinitionMapping.ContainsKey(interfaceQualName)) + { + continue; + } + + // Create the output method + TypeSignature returnType = method.Signature!.ReturnType is CorLibTypeSignature { ElementType: ElementType.Void } + ? _outputModule.CorLibTypeFactory.Void + : MapTypeSignatureToOutput(method.Signature.ReturnType); + + TypeSignature[] parameterTypes = [.. method.Signature.ParameterTypes + .Select(MapTypeSignatureToOutput)]; + + MethodAttributes attrs = + MethodAttributes.Private | + MethodAttributes.Final | + MethodAttributes.Virtual | + MethodAttributes.HideBySig | + MethodAttributes.NewSlot; + + if (method.IsSpecialName) + { + attrs |= MethodAttributes.SpecialName; + } + + MethodDefinition outputMethod = new( + fullMethodName, + attrs, + MethodSignature.CreateInstance(returnType, parameterTypes)) + { + ImplAttributes = MethodImplAttributes.Runtime | MethodImplAttributes.Managed + }; + + int paramIndex = 1; + foreach (ParameterDefinition inputParam in method.ParameterDefinitions) + { + outputMethod.ParameterDefinitions.Add(new ParameterDefinition( + (ushort)paramIndex++, + inputParam.Name!.Value, + ParameterAttributes.In)); + } + + outputType.Methods.Add(outputMethod); + + // Also add properties/events for accessor methods + if (shortMethodName.StartsWith("get_", StringComparison.Ordinal) || shortMethodName.StartsWith("put_", StringComparison.Ordinal)) + { + string propName = $"{interfaceQualName}.{shortMethodName[4..]}"; + if (!outputType.Properties.Any(p => p.Name?.Value == propName)) + { + PropertyDefinition prop = new(propName, 0, PropertySignature.CreateInstance( + shortMethodName.StartsWith("get_", StringComparison.Ordinal) ? returnType : parameterTypes[0])); + + if (shortMethodName.StartsWith("get_", StringComparison.Ordinal)) + { + prop.Semantics.Add(new MethodSemantics(outputMethod, MethodSemanticsAttributes.Getter)); + } + else + { + prop.Semantics.Add(new MethodSemantics(outputMethod, MethodSemanticsAttributes.Setter)); + } + + outputType.Properties.Add(prop); + } + else + { + // Add setter to existing property + PropertyDefinition existing = outputType.Properties.First(p => p.Name?.Value == propName); + existing.Semantics.Add(new MethodSemantics(outputMethod, MethodSemanticsAttributes.Setter)); + } + } + else if (shortMethodName.StartsWith("add_", StringComparison.Ordinal)) + { + string evtName = $"{interfaceQualName}.{shortMethodName[4..]}"; + // Find the event handler type from the parameter + ITypeDefOrRef eventType = parameterTypes.Length > 0 && parameterTypes[0] is TypeDefOrRefSignature tdrs + ? tdrs.Type + : 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 (shortMethodName.StartsWith("remove_", StringComparison.Ordinal)) + { + string evtName = $"{interfaceQualName}.{shortMethodName[7..]}"; + EventDefinition? existingEvt = outputType.Events.FirstOrDefault(e => e.Name?.Value == evtName); + existingEvt?.Semantics.Add(new MethodSemantics(outputMethod, MethodSemanticsAttributes.RemoveOn)); + } + + // Add MethodImpl pointing to the interface method + TypeDeclaration interfaceDecl = _typeDefinitionMapping[interfaceQualName]; + if (interfaceDecl.OutputType != null) + { + MemberReference interfaceMethodRef = new(interfaceDecl.OutputType, shortMethodName, + MethodSignature.CreateInstance(returnType, parameterTypes)); + outputType.MethodImplementations.Add(new MethodImplementation(interfaceMethodRef, outputMethod)); + } + } + } } \ No newline at end of file From b9f6f77ea91fc1d2ebef68c263050815f77a4a9c Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Sun, 12 Apr 2026 12:38:24 -0700 Subject: [PATCH 32/50] Fix explicit impl WinRT conventions and resolve generic base type args Issue 2: Explicit interface implementation methods now apply WinRT conventions: - set_ renamed to put_ in method names and MethodImpl targets - Event add_ returns EventRegistrationToken (not void) with param named 'handler' - Event remove_ takes EventRegistrationToken param named 'token' Issue 3: GatherAllInterfaces now resolves generic type parameters when walking the base class chain. When a class inherits from a generic base (e.g., ObservableCollection), the base type's interfaces (like IList) have their generic parameters substituted with the concrete type arguments (IList), producing correct mapped interface signatures. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../WinmdWriter.CustomMappedMembers.cs | 62 +++++++--- .../Generation/WinmdWriter.Types.cs | 116 ++++++++++-------- 2 files changed, 109 insertions(+), 69 deletions(-) diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.CustomMappedMembers.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.CustomMappedMembers.cs index 102460cc6..c7c59c183 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.CustomMappedMembers.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.CustomMappedMembers.cs @@ -450,14 +450,14 @@ private void AddMappedEvent( /// /// Gathers all interfaces from a type and its base type chain, including interfaces - /// inherited from interfaces. This matches the old source generator's AllInterfaces behavior. + /// inherited from interfaces. Resolves generic type parameters through the base class chain. /// - private static List GatherAllInterfaces(TypeDefinition type) + private List GatherAllInterfaces(TypeDefinition type) { HashSet seen = new(StringComparer.Ordinal); List result = []; - void CollectFromType(TypeDefinition typeDef) + void CollectFromType(TypeDefinition typeDef, TypeSignature[]? genericArgs) { foreach (InterfaceImplementation impl in typeDef.Interfaces) { @@ -466,38 +466,68 @@ void CollectFromType(TypeDefinition typeDef) continue; } - string name = impl.Interface.FullName ?? ""; + // 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 + TypeSignature[] resolvedArgs = [.. gits.TypeArguments.Select(arg => + arg is GenericParameterSignature gps && gps.Index < genericArgs.Length + ? genericArgs[gps.Index] + : arg)]; + resolvedInterface = new TypeSpecification(new GenericInstanceTypeSignature(gits.GenericType, gits.IsValueType, resolvedArgs)); + } + + string name = resolvedInterface.FullName ?? ""; if (!seen.Add(name)) { continue; } - if (IsPubliclyAccessible(impl.Interface)) + if (IsPubliclyAccessible(resolvedInterface)) { - result.Add(impl); + result.Add(new InterfaceImplementation(resolvedInterface)); } // Also collect interfaces inherited by this interface - TypeDefinition? interfaceDef = impl.Interface is TypeSpecification ts - ? (ts.Signature as GenericInstanceTypeSignature)?.GenericType.Resolve() - : impl.Interface.Resolve(); + TypeDefinition? interfaceDef = resolvedInterface is TypeSpecification ts2 + ? (ts2.Signature as GenericInstanceTypeSignature)?.GenericType.Resolve() + : resolvedInterface.Resolve(); if (interfaceDef != null) { - CollectFromType(interfaceDef); + // 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); + CollectFromType(type, null); - // Walk base types - TypeDefinition? current = type.BaseType?.Resolve(); - while (current != null) + // Walk base types, resolving generic arguments + ITypeDefOrRef? baseTypeRef = type.BaseType; + while (baseTypeRef != null) { - CollectFromType(current); - current = current.BaseType?.Resolve(); + TypeDefinition? baseDef = baseTypeRef.Resolve(); + 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; diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs index 574689a52..15007b526 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs @@ -459,21 +459,23 @@ private void AddClassType(TypeDefinition inputType) /// /// Adds explicit interface implementation methods from the input class. - /// These are private methods with qualified names (e.g., "AuthoringTest.IDouble.GetDouble"). + /// 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(); + foreach (MethodDefinition method in inputType.Methods) { - // Explicit implementations in compiled IL are private methods with dots in the name if (method.IsPublic || method.Name?.Value?.Contains('.') != true) { continue; } string fullMethodName = method.Name.Value; - - // Extract interface name and method name (e.g., "AuthoringTest.IDouble.GetDouble" -> "AuthoringTest.IDouble" + "GetDouble") int lastDot = fullMethodName.LastIndexOf('.'); if (lastDot <= 0) { @@ -483,105 +485,113 @@ private void AddExplicitInterfaceImplementations(TypeDefinition inputType, TypeD string interfaceQualName = fullMethodName[..lastDot]; string shortMethodName = fullMethodName[(lastDot + 1)..]; - // Check if this interface is one we've processed (skip mapped/unmapped interfaces) if (!_typeDefinitionMapping.ContainsKey(interfaceQualName)) { continue; } - // Create the output method - TypeSignature returnType = method.Signature!.ReturnType is CorLibTypeSignature { ElementType: ElementType.Void } - ? _outputModule.CorLibTypeFactory.Void - : MapTypeSignatureToOutput(method.Signature.ReturnType); + // Apply WinRT naming: set_ to put_ + string winrtShortName = shortMethodName; + if (winrtShortName.StartsWith("set_", StringComparison.Ordinal)) + { + winrtShortName = "put_" + winrtShortName[4..]; + } - TypeSignature[] parameterTypes = [.. method.Signature.ParameterTypes - .Select(MapTypeSignatureToOutput)]; + 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; + MethodAttributes.Private | MethodAttributes.Final | MethodAttributes.Virtual | + MethodAttributes.HideBySig | MethodAttributes.NewSlot; if (method.IsSpecialName) { attrs |= MethodAttributes.SpecialName; } - MethodDefinition outputMethod = new( - fullMethodName, - attrs, + MethodDefinition outputMethod = new(winrtFullName, attrs, MethodSignature.CreateInstance(returnType, parameterTypes)) { ImplAttributes = MethodImplAttributes.Runtime | MethodImplAttributes.Managed }; - int paramIndex = 1; - foreach (ParameterDefinition inputParam in method.ParameterDefinitions) + for (int i = 0; i < paramNames.Length; i++) { outputMethod.ParameterDefinitions.Add(new ParameterDefinition( - (ushort)paramIndex++, - inputParam.Name!.Value, - ParameterAttributes.In)); + (ushort)(i + 1), paramNames[i], ParameterAttributes.In)); } outputType.Methods.Add(outputMethod); - // Also add properties/events for accessor methods - if (shortMethodName.StartsWith("get_", StringComparison.Ordinal) || shortMethodName.StartsWith("put_", StringComparison.Ordinal)) + if (winrtShortName.StartsWith("get_", StringComparison.Ordinal) || winrtShortName.StartsWith("put_", StringComparison.Ordinal)) { - string propName = $"{interfaceQualName}.{shortMethodName[4..]}"; - if (!outputType.Properties.Any(p => p.Name?.Value == propName)) + string propName = $"{interfaceQualName}.{winrtShortName[4..]}"; + PropertyDefinition? existingProp = outputType.Properties.FirstOrDefault(p => p.Name?.Value == propName); + if (existingProp == null) { - PropertyDefinition prop = new(propName, 0, PropertySignature.CreateInstance( - shortMethodName.StartsWith("get_", StringComparison.Ordinal) ? returnType : parameterTypes[0])); - - if (shortMethodName.StartsWith("get_", StringComparison.Ordinal)) - { - prop.Semantics.Add(new MethodSemantics(outputMethod, MethodSemanticsAttributes.Getter)); - } - else - { - prop.Semantics.Add(new MethodSemantics(outputMethod, MethodSemanticsAttributes.Setter)); - } - + 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 { - // Add setter to existing property - PropertyDefinition existing = outputType.Properties.First(p => p.Name?.Value == propName); - existing.Semantics.Add(new MethodSemantics(outputMethod, MethodSemanticsAttributes.Setter)); + existingProp.Semantics.Add(new MethodSemantics(outputMethod, MethodSemanticsAttributes.Setter)); } } - else if (shortMethodName.StartsWith("add_", StringComparison.Ordinal)) + else if (winrtShortName.StartsWith("add_", StringComparison.Ordinal)) { - string evtName = $"{interfaceQualName}.{shortMethodName[4..]}"; - // Find the event handler type from the parameter + string evtName = $"{interfaceQualName}.{winrtShortName[4..]}"; ITypeDefOrRef eventType = parameterTypes.Length > 0 && parameterTypes[0] is TypeDefOrRefSignature tdrs ? tdrs.Type - : GetOrCreateTypeReference("Windows.Foundation", "EventHandler`1", "Windows.Foundation.FoundationContract"); - + : 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 (shortMethodName.StartsWith("remove_", StringComparison.Ordinal)) + else if (winrtShortName.StartsWith("remove_", StringComparison.Ordinal)) { - string evtName = $"{interfaceQualName}.{shortMethodName[7..]}"; + string evtName = $"{interfaceQualName}.{winrtShortName[7..]}"; EventDefinition? existingEvt = outputType.Events.FirstOrDefault(e => e.Name?.Value == evtName); existingEvt?.Semantics.Add(new MethodSemantics(outputMethod, MethodSemanticsAttributes.RemoveOn)); } - // Add MethodImpl pointing to the interface method TypeDeclaration interfaceDecl = _typeDefinitionMapping[interfaceQualName]; if (interfaceDecl.OutputType != null) { - MemberReference interfaceMethodRef = new(interfaceDecl.OutputType, shortMethodName, + MemberReference interfaceMethodRef = new(interfaceDecl.OutputType, winrtShortName, MethodSignature.CreateInstance(returnType, parameterTypes)); outputType.MethodImplementations.Add(new MethodImplementation(interfaceMethodRef, outputMethod)); } } } -} \ No newline at end of file +} From 87f7934290689350377346832368b93183ad7525 Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Sun, 12 Apr 2026 12:46:30 -0700 Subject: [PATCH 33/50] Fix IBindableIterable vs IIterable and ICollection member filtering - Check all gathered interfaces (not just mapped ones) for generic IEnumerable presence when deciding to suppress IBindableIterable. The generic IEnumerable might not be in the mapped interfaces list but still indicates IIterable should be used. - For unresolvable interfaces in ImplementedInterfacesWithoutMapping (like ICollection from System.Runtime), use MethodImpl records from the input type to find which class methods implement the interface and add their names to the exclusion set. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generation/WinmdWriter.CustomMappedMembers.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.CustomMappedMembers.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.CustomMappedMembers.cs index c7c59c183..4cce08af0 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.CustomMappedMembers.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.CustomMappedMembers.cs @@ -56,8 +56,8 @@ private void ProcessCustomMappedInterfaces(TypeDefinition inputType, TypeDefinit } // If generic IEnumerable (IIterable) is present, skip non-generic IEnumerable (IBindableIterable) - bool hasGenericEnumerable = mappedInterfaces.Any(m => - m.interfaceName == "System.Collections.Generic.IEnumerable`1"); + bool hasGenericEnumerable = allInterfaces.Any(i => + GetInterfaceQualifiedName(i.Interface!) == "System.Collections.Generic.IEnumerable`1"); foreach ((InterfaceImplementation impl, string interfaceName, MappedType mapping, bool isPublic) in mappedInterfaces) { From 4796c7201d6607ecb4eb7562c5735d0c005e5cbc Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Sun, 12 Apr 2026 16:28:03 -0700 Subject: [PATCH 34/50] Fix WinMD comparison issues: generic resolution, MethodImpl encoding, synthesized interfaces, assembly resolver - Fix recursive generic parameter resolution in GatherAllInterfaces for nested generic types like KeyValuePair - Fix double-arity bug in GetQualifiedName(TypeDefinition) where generic type names already include backtick suffix - Fix MethodImpl generic parameter encoding: use !0/!1 in declaration signatures instead of resolved concrete types for mapped interface members - Fix synthesized default interface creation: detect interface members from input MethodImplementations and avoid creating unnecessary synthesized interfaces - Fix MSBuild targets to use ReferencePathWithRefAssemblies instead of ReferencePath to get actual ref assemblies instead of forwarders - Add sibling directory fallback in PathAssemblyResolver for type forwarding - Fix FinalizeGeneration to resolve TypeSpecification interfaces and fall back to input type's interface references for unresolvable WinRT contract types - Support explicit interface implementation matching in MethodImpl generation Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...icrosoft.Windows.CsWinMD.Generator.targets | 4 +- .../Discovery/AssemblyAnalyzer.cs | 4 -- .../WinmdWriter.CustomMappedMembers.cs | 72 ++++++++++++++++--- .../WinmdWriter.SynthesizedInterfaces.cs | 14 +++- .../Generation/WinmdWriter.cs | 59 ++++++++++++++- .../Resolvers/PathAssemblyResolver.cs | 22 ++++++ 6 files changed, 155 insertions(+), 20 deletions(-) diff --git a/nuget/Microsoft.Windows.CsWinMD.Generator.targets b/nuget/Microsoft.Windows.CsWinMD.Generator.targets index 5cc6bad54..5590eca63 100644 --- a/nuget/Microsoft.Windows.CsWinMD.Generator.targets +++ b/nuget/Microsoft.Windows.CsWinMD.Generator.targets @@ -75,14 +75,14 @@ Copyright (C) Microsoft Corporation. All rights reserved. Name="_RunCsWinRTWinMDGenerator" DependsOnTargets="CoreCompile;_ComputeRunCsWinRTWinMDGeneratorCache" AfterTargets="CoreCompile" - Inputs="@(IntermediateAssembly);@(ReferencePath);$(_RunCsWinRTWinMDGeneratorPropertyInputsCachePath)" + Inputs="@(IntermediateAssembly);@(ReferencePathWithRefAssemblies);$(_RunCsWinRTWinMDGeneratorPropertyInputsCachePath)" Outputs="$(_CsWinRTWinMDOutputPath)" Condition="'$(CsWinRTComponent)' == 'true'"> 0) - { - name += $"`{type.GenericParameters.Count}"; - } string? ns = GetEffectiveNamespace(type); return ns is { Length: > 0 } ? $"{ns}.{name}" : name; diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.CustomMappedMembers.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.CustomMappedMembers.cs index 4cce08af0..deb450351 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.CustomMappedMembers.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.CustomMappedMembers.cs @@ -57,7 +57,7 @@ private void ProcessCustomMappedInterfaces(TypeDefinition inputType, TypeDefinit // If generic IEnumerable (IIterable) is present, skip non-generic IEnumerable (IBindableIterable) bool hasGenericEnumerable = allInterfaces.Any(i => - GetInterfaceQualifiedName(i.Interface!) == "System.Collections.Generic.IEnumerable`1"); + i.Interface != null && GetInterfaceQualifiedName(i.Interface) == "System.Collections.Generic.IEnumerable`1"); foreach ((InterfaceImplementation impl, string interfaceName, MappedType mapping, bool isPublic) in mappedInterfaces) { @@ -161,6 +161,36 @@ private void AddCustomMappedTypeMembers( // For generic types, use short type names (e.g. "IMap`2" not "IMap`2") string qualifiedPrefix = FormatQualifiedInterfaceName(mappedInterfaceRef); + // Build a reverse map from resolved type args back to generic parameters for MethodImpl signatures. + // MethodImpl declarations on generic interfaces should reference methods using !0, !1 etc. (not resolved types). + Dictionary? genericArgReverseMap = null; + if (mappedInterfaceRef is TypeSpecification mts && mts.Signature is GenericInstanceTypeSignature mgits) + { + genericArgReverseMap = []; + for (int i = 0; i < mgits.TypeArguments.Count; i++) + { + string argFullName = mgits.TypeArguments[i].FullName; + genericArgReverseMap[argFullName] = new GenericParameterSignature(_outputModule, GenericParameterType.Type, i); + } + } + + // Convert a resolved type signature to use generic parameters (!0, !1) for MethodImpl declarations + TypeSignature ToGenericParam(TypeSignature sig) => + genericArgReverseMap == null + ? sig + : genericArgReverseMap.TryGetValue(sig.FullName, out GenericParameterSignature? gps) + ? gps + : sig switch + { + GenericInstanceTypeSignature innerGits => + new GenericInstanceTypeSignature(innerGits.GenericType, innerGits.IsValueType, [.. innerGits.TypeArguments.Select(ToGenericParam)]), + SzArrayTypeSignature szArray => + new SzArrayTypeSignature(ToGenericParam(szArray.BaseType)), + ByReferenceTypeSignature byRef => + new ByReferenceTypeSignature(ToGenericParam(byRef.BaseType)), + _ => sig + }; + void AddMappedMethod(string name, (string name, TypeSignature type, ParameterAttributes attrs)[]? parameters, TypeSignature? returnType) { string methodName = isPublic ? name : $"{qualifiedPrefix}.{name}"; @@ -188,8 +218,10 @@ void AddMappedMethod(string name, (string name, TypeSignature type, ParameterAtt outputType.Methods.Add(method); - // Add MethodImpl pointing to the mapped interface method - MemberReference interfaceMethodRef = new(mappedInterfaceRef, name, MethodSignature.CreateInstance(returnType ?? _outputModule.CorLibTypeFactory.Void, paramTypes)); + // 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)); } @@ -213,8 +245,9 @@ void AddMappedProperty(string name, TypeSignature propertyType, bool hasSetter) PropertyDefinition prop = new(propName, 0, PropertySignature.CreateInstance(propertyType)); prop.Semantics.Add(new MethodSemantics(getter, MethodSemanticsAttributes.Getter)); - // MethodImpl for getter - MemberReference getterRef = new(mappedInterfaceRef, $"get_{name}", MethodSignature.CreateInstance(propertyType)); + // 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) @@ -228,7 +261,7 @@ void AddMappedProperty(string name, TypeSignature propertyType, bool hasSetter) outputType.Methods.Add(setter); prop.Semantics.Add(new MethodSemantics(setter, MethodSemanticsAttributes.Setter)); - MemberReference setterRef = new(mappedInterfaceRef, $"put_{name}", MethodSignature.CreateInstance(_outputModule.CorLibTypeFactory.Void, propertyType)); + MemberReference setterRef = new(mappedInterfaceRef, $"put_{name}", MethodSignature.CreateInstance(_outputModule.CorLibTypeFactory.Void, implPropertyType)); outputType.MethodImplementations.Add(new MethodImplementation(setterRef, setter)); } @@ -471,11 +504,8 @@ void CollectFromType(TypeDefinition typeDef, TypeSignature[]? genericArgs) if (genericArgs != null && impl.Interface is TypeSpecification ts && ts.Signature is GenericInstanceTypeSignature gits) { - // Resolve generic parameters in type arguments - TypeSignature[] resolvedArgs = [.. gits.TypeArguments.Select(arg => - arg is GenericParameterSignature gps && gps.Index < genericArgs.Length - ? genericArgs[gps.Index] - : arg)]; + // 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)); } @@ -533,6 +563,26 @@ baseTs.Signature is GenericInstanceTypeSignature baseGits 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; + } + /// /// 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. diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.SynthesizedInterfaces.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.SynthesizedInterfaces.cs index 05f4329da..eaec08f93 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.SynthesizedInterfaces.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.SynthesizedInterfaces.cs @@ -39,7 +39,7 @@ private void AddSynthesizedInterfaces(TypeDefinition inputType, TypeDefinition c // Collect members that come from interface implementations HashSet membersFromInterfaces = []; - // Use all interfaces including inherited ones + // Use all interfaces including inherited ones from the input type List allInterfaces = GatherAllInterfaces(inputType); foreach (InterfaceImplementation impl in allInterfaces) { @@ -86,6 +86,18 @@ private void AddSynthesizedInterfaces(TypeDefinition inputType, TypeDefinition c } } + // 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); diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs index a81f32000..7cf2d04d5 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs @@ -117,9 +117,34 @@ public void FinalizeGeneration() // Add MethodImpls for implemented interfaces (excluding the default synthesized interface, handled below) foreach (InterfaceImplementation classInterfaceImpl in classOutputType.Interfaces) { - TypeDefinition? interfaceDef = classInterfaceImpl.Interface?.Resolve(); + // Resolve the interface — handle TypeSpecification (generic instances) by resolving the GenericType + TypeDefinition? interfaceDef = classInterfaceImpl.Interface is TypeSpecification ts + && ts.Signature is GenericInstanceTypeSignature gits + ? gits.GenericType.Resolve() + : classInterfaceImpl.Interface?.Resolve(); + + // 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 + ? igits.GenericType.Resolve() + : inputImpl.Interface.Resolve(); + break; + } + } + } + + if (interfaceDef == null) + { + // Still unresolvable — MethodImpls for mapped interfaces are already + // created by AddCustomMappedTypeMembers, so this is expected for those. continue; } @@ -132,8 +157,38 @@ public void FinalizeGeneration() foreach (MethodDefinition interfaceMethod in interfaceDef.Methods) { - // Find the corresponding method on the class + // Find the corresponding method on the class (by name or explicit implementation pattern) MethodDefinition? classMethod = FindMatchingMethod(classOutputType, interfaceMethod); + if (classMethod == null) + { + // Try matching explicit implementation: look for "Namespace.IFoo.MethodName" pattern + // Also verify parameter count and types match to avoid wrong overload matches + string explicitName = $"{interfaceQualName}.{interfaceMethod.Name?.Value}"; + int paramCount = interfaceMethod.Signature?.ParameterTypes.Count ?? 0; + 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++) + { + if (m.Signature!.ParameterTypes[i].FullName != interfaceMethod.Signature!.ParameterTypes[i].FullName) + { + return false; + } + } + + return true; + }); + } + if (classMethod != null) { MemberReference interfaceMethodRef = new(classInterfaceImpl.Interface, interfaceMethod.Name!.Value, interfaceMethod.Signature); diff --git a/src/WinRT.WinMD.Generator/Resolvers/PathAssemblyResolver.cs b/src/WinRT.WinMD.Generator/Resolvers/PathAssemblyResolver.cs index fa4a1225d..d5593422a 100644 --- a/src/WinRT.WinMD.Generator/Resolvers/PathAssemblyResolver.cs +++ b/src/WinRT.WinMD.Generator/Resolvers/PathAssemblyResolver.cs @@ -68,6 +68,28 @@ public PathAssemblyResolver(string[] referencePaths) } } + // Fallback: search sibling directories of existing reference paths. + // This handles type forwarding scenarios where the forwarder assembly (e.g., Microsoft.Windows.SDK.NET) + // forwards to an assembly (e.g., WinRT.Sdk.Projection) that lives in a sibling directory. + string targetFileName = assembly.Name + ".dll"; + foreach (string path in _referencePaths) + { + string? dir = Path.GetDirectoryName(path); + if (dir is null) + { + continue; + } + + string candidate = Path.Combine(dir, targetFileName); + if (File.Exists(candidate)) + { + return _cache.GetOrAdd( + key: assembly, + valueFactory: static (_, args) => AssemblyDefinition.FromFile(args.Path, args.Parameters), + factoryArgument: (Path: candidate, Parameters: ReaderParameters)); + } + } + return null; } From 7798d371521222315c2e30ff28b4bd9abfc5e317 Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Sun, 12 Apr 2026 16:51:17 -0700 Subject: [PATCH 35/50] Fix projected type matching, MethodImpl signatures, and dictionary enumeration - Use IsProjectionEquivalent for type comparison instead of MapTypeSignatureToOutput to avoid unwanted side effects (type imports into output module) - Use class method signature for MethodImpl declarations when interface resolved from input ref assemblies (maps .NET projection types to WinRT types) - Snapshot _typeDefinitionMapping for all FinalizeGeneration phases to prevent InvalidOperationException from dictionary modification during enumeration - Support explicit interface implementation matching with parameter type verification Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generation/WinmdWriter.cs | 68 +++++++++++++++---- 1 file changed, 53 insertions(+), 15 deletions(-) diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs index 7cf2d04d5..a55ab0cb8 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs @@ -104,7 +104,9 @@ public void ProcessType(TypeDefinition inputType) public void FinalizeGeneration() { // Phase 1: Add MethodImpl fixups for classes - foreach ((string qualifiedName, TypeDeclaration declaration) in _typeDefinitionMapping) + // 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) { @@ -115,9 +117,12 @@ public void FinalizeGeneration() TypeDefinition classInputType = declaration.InputType; // Add MethodImpls for implemented interfaces (excluding the default synthesized interface, handled below) - foreach (InterfaceImplementation classInterfaceImpl in classOutputType.Interfaces) + // 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; TypeDefinition? interfaceDef = classInterfaceImpl.Interface is TypeSpecification ts && ts.Signature is GenericInstanceTypeSignature gits ? gits.GenericType.Resolve() @@ -136,6 +141,7 @@ public void FinalizeGeneration() && its.Signature is GenericInstanceTypeSignature igits ? igits.GenericType.Resolve() : inputImpl.Interface.Resolve(); + resolvedFromInput = interfaceDef != null; break; } } @@ -155,14 +161,15 @@ public void FinalizeGeneration() continue; } - foreach (MethodDefinition interfaceMethod in interfaceDef.Methods) + List interfaceMethods = [.. interfaceDef.Methods]; + foreach (MethodDefinition interfaceMethod in interfaceMethods) { - // Find the corresponding method on the class (by name or explicit implementation pattern) - MethodDefinition? classMethod = FindMatchingMethod(classOutputType, interfaceMethod); + // Find the corresponding method on the class (by name or explicit implementation pattern). + // When resolved from input ref assemblies, map .NET projection types to WinRT equivalents. + MethodDefinition? classMethod = FindMatchingMethod(classOutputType, interfaceMethod, resolvedFromInput); if (classMethod == null) { // Try matching explicit implementation: look for "Namespace.IFoo.MethodName" pattern - // Also verify parameter count and types match to avoid wrong overload matches string explicitName = $"{interfaceQualName}.{interfaceMethod.Name?.Value}"; int paramCount = interfaceMethod.Signature?.ParameterTypes.Count ?? 0; classMethod = classOutputType.Methods.FirstOrDefault(m => @@ -179,7 +186,11 @@ public void FinalizeGeneration() for (int i = 0; i < paramCount; i++) { - if (m.Signature!.ParameterTypes[i].FullName != interfaceMethod.Signature!.ParameterTypes[i].FullName) + string classParamName = m.Signature!.ParameterTypes[i].FullName; + string ifaceParamName = interfaceMethod.Signature!.ParameterTypes[i].FullName; + + if (classParamName != ifaceParamName && + !(resolvedFromInput && IsProjectionEquivalent(ifaceParamName, classParamName))) { return false; } @@ -191,7 +202,11 @@ public void FinalizeGeneration() if (classMethod != null) { - MemberReference interfaceMethodRef = new(classInterfaceImpl.Interface, interfaceMethod.Name!.Value, interfaceMethod.Signature); + // 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)); } } @@ -219,7 +234,7 @@ public void FinalizeGeneration() // Phase 2: Add default version attributes for types that don't have one int defaultVersion = Version.Parse(_version).Major; - foreach ((string _, TypeDeclaration declaration) in _typeDefinitionMapping) + foreach ((string _, TypeDeclaration declaration) in typeDeclarations) { if (declaration.OutputType == null) { @@ -235,7 +250,7 @@ public void FinalizeGeneration() } // Phase 3: Add custom attributes from input types to output types - foreach ((string _, TypeDeclaration declaration) in _typeDefinitionMapping) + foreach ((string _, TypeDeclaration declaration) in typeDeclarations) { if (declaration.OutputType == null || declaration.InputType == null || !declaration.IsComponentType) { @@ -246,7 +261,7 @@ public void FinalizeGeneration() } // Phase 4: Add overload attributes for methods with the same name - foreach ((string _, TypeDeclaration declaration) in _typeDefinitionMapping) + foreach ((string _, TypeDeclaration declaration) in typeDeclarations) { if (declaration.OutputType == null) { @@ -257,7 +272,7 @@ public void FinalizeGeneration() } } - private static MethodDefinition? FindMatchingMethod(TypeDefinition classType, MethodDefinition interfaceMethod) + private MethodDefinition? FindMatchingMethod(TypeDefinition classType, MethodDefinition interfaceMethod, bool mapInterfaceTypes = false) { string methodName = interfaceMethod.Name?.Value ?? ""; @@ -278,10 +293,18 @@ public void FinalizeGeneration() bool parametersMatch = true; for (int i = 0; i < (classMethod.Signature?.ParameterTypes.Count ?? 0); i++) { - if (classMethod.Signature!.ParameterTypes[i].FullName != interfaceMethod.Signature!.ParameterTypes[i].FullName) + string classParamName = classMethod.Signature!.ParameterTypes[i].FullName; + string ifaceParamName = interfaceMethod.Signature!.ParameterTypes[i].FullName; + + if (classParamName != ifaceParamName) { - parametersMatch = false; - break; + // 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; + } } } @@ -296,6 +319,21 @@ public void FinalizeGeneration() 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) + { + if (_mapper.HasMappingForType(dotNetTypeName)) + { + (string ns, string name, _, _, _) = _mapper.GetMappedType(dotNetTypeName).GetMapping(); + string mappedName = string.IsNullOrEmpty(ns) ? name : $"{ns}.{name}"; + return mappedName == winrtTypeName; + } + + return false; + } + private void AddOverloadAttributesForType(TypeDefinition type) { // Group methods by name to find overloaded methods From 7fa7d29db262bcb51b2ba5ce390a42539928b875 Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Sun, 12 Apr 2026 17:05:31 -0700 Subject: [PATCH 36/50] Fix ExplicltlyImplementedClass and ObservableVector MethodImpl matching - Prefer explicit interface implementations over public methods when both exist for the same interface method (prevents duplicate MethodImpls) - Resolve generic parameters (!0, !1) in interface method signatures when matching against class methods for generic interface instances - Extract and pass generic type arguments from TypeSpecification interfaces Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generation/WinmdWriter.cs | 58 ++++++++++++++----- 1 file changed, 45 insertions(+), 13 deletions(-) diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs index a55ab0cb8..79e462fed 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs @@ -123,10 +123,19 @@ public void FinalizeGeneration() { // Resolve the interface — handle TypeSpecification (generic instances) by resolving the GenericType bool resolvedFromInput = false; - TypeDefinition? interfaceDef = classInterfaceImpl.Interface is TypeSpecification ts - && ts.Signature is GenericInstanceTypeSignature gits - ? gits.GenericType.Resolve() - : classInterfaceImpl.Interface?.Resolve(); + TypeSignature[]? interfaceGenericArgs = null; + TypeDefinition? interfaceDef = null; + + if (classInterfaceImpl.Interface is TypeSpecification ts + && ts.Signature is GenericInstanceTypeSignature gits) + { + interfaceDef = gits.GenericType.Resolve(); + interfaceGenericArgs = [.. gits.TypeArguments]; + } + else + { + interfaceDef = classInterfaceImpl.Interface?.Resolve(); + } // 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 @@ -164,14 +173,19 @@ public void FinalizeGeneration() List interfaceMethods = [.. interfaceDef.Methods]; foreach (MethodDefinition interfaceMethod in interfaceMethods) { - // Find the corresponding method on the class (by name or explicit implementation pattern). - // When resolved from input ref assemblies, map .NET projection types to WinRT equivalents. - MethodDefinition? classMethod = FindMatchingMethod(classOutputType, interfaceMethod, resolvedFromInput); - if (classMethod == null) + // 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) { - // Try matching explicit implementation: look for "Namespace.IFoo.MethodName" pattern - string explicitName = $"{interfaceQualName}.{interfaceMethod.Name?.Value}"; - int paramCount = interfaceMethod.Signature?.ParameterTypes.Count ?? 0; + // Match by explicit name only classMethod = classOutputType.Methods.FirstOrDefault(m => { if (m.Name?.Value != explicitName) @@ -199,6 +213,12 @@ public void FinalizeGeneration() 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); + } if (classMethod != null) { @@ -272,7 +292,11 @@ public void FinalizeGeneration() } } - private MethodDefinition? FindMatchingMethod(TypeDefinition classType, MethodDefinition interfaceMethod, bool mapInterfaceTypes = false) + private MethodDefinition? FindMatchingMethod( + TypeDefinition classType, + MethodDefinition interfaceMethod, + bool mapInterfaceTypes = false, + TypeSignature[]? interfaceGenericArgs = null) { string methodName = interfaceMethod.Name?.Value ?? ""; @@ -294,7 +318,15 @@ public void FinalizeGeneration() for (int i = 0; i < (classMethod.Signature?.ParameterTypes.Count ?? 0); i++) { string classParamName = classMethod.Signature!.ParameterTypes[i].FullName; - string ifaceParamName = interfaceMethod.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) { From f8784248bef7b8d7d54f8043a537b4b0ae8173e5 Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Sun, 12 Apr 2026 17:16:09 -0700 Subject: [PATCH 37/50] Fix EventRegistrationToken mapping namespace Update from incorrect 'WinRT.EventRegistrationToken' to correct 'WindowsRuntime.InteropServices.EventRegistrationToken' matching the CsWinRT projection namespace in helpers.h. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/WinRT.WinMD.Generator/Models/TypeMapper.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/WinRT.WinMD.Generator/Models/TypeMapper.cs b/src/WinRT.WinMD.Generator/Models/TypeMapper.cs index c0e81bf16..4d99087a7 100644 --- a/src/WinRT.WinMD.Generator/Models/TypeMapper.cs +++ b/src/WinRT.WinMD.Generator/Models/TypeMapper.cs @@ -102,6 +102,7 @@ public TypeMapper(bool useWindowsUIXamlProjections) { "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") }, @@ -118,7 +119,7 @@ public TypeMapper(bool useWindowsUIXamlProjections) { "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") }, - { "WinRT.EventRegistrationToken", new MappedType("Windows.Foundation", "EventRegistrationToken", "Windows.Foundation.FoundationContract", true, true) }, + { "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) }, @@ -146,6 +147,7 @@ public TypeMapper(bool useWindowsUIXamlProjections) { "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") }, @@ -165,7 +167,7 @@ public TypeMapper(bool useWindowsUIXamlProjections) { "System.Collections.Specialized.NotifyCollectionChangedAction", new MappedType("Microsoft.UI.Xaml.Interop", "NotifyCollectionChangedAction", "Microsoft.UI", true, true) }, { "System.Collections.Specialized.NotifyCollectionChangedEventArgs", new MappedType("Microsoft.UI.Xaml.Interop", "NotifyCollectionChangedEventArgs", "Microsoft.UI") }, { "System.Collections.Specialized.NotifyCollectionChangedEventHandler", new MappedType("Microsoft.UI.Xaml.Interop", "NotifyCollectionChangedEventHandler", "Microsoft.UI") }, - { "WinRT.EventRegistrationToken", new MappedType("Windows.Foundation", "EventRegistrationToken", "Windows.Foundation.FoundationContract", true, true) }, + { "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) }, From 4cc3ec73f3acc6c8e497c72c3036b7cdc8121ca6 Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Sun, 12 Apr 2026 17:32:42 -0700 Subject: [PATCH 38/50] Fix ObservableVector event MethodImpl and add TypedEventHandler mapping - Add name-only fallback matching for event accessor methods from ref assemblies where CsWinRT projections change event signatures (.NET delegate vs WinRT EventRegistrationToken convention) - Add System.EventHandler\2 -> TypedEventHandler\2 type mapping - Remove debug logging Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs index 79e462fed..f63e64ff0 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs @@ -218,6 +218,15 @@ public void FinalizeGeneration() // 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) From 56a0b54a577de5f1f55616c1c24dabcf8ceaed12 Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Sun, 12 Apr 2026 18:02:14 -0700 Subject: [PATCH 39/50] Fix Default attribute placement on first user-declared interface Place [Default] on the WinRT equivalent of the first user-declared .NET interface, matching the intended behavior. Remove interface sorting since WinRT spec does not prescribe interface ordering - only requires exactly one DefaultAttribute on InterfaceImpl rows. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generation/WinmdWriter.Types.cs | 40 ++++++++++++++++++- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs index 15007b526..442bd1fed 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs @@ -450,13 +450,49 @@ private void AddClassType(TypeDefinition inputType) AddExplicitInterfaceImplementations(inputType, outputType); // If no default synthesized interface was created but the class implements - // user interfaces, mark the first interface implementation as [Default] + // 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) { - AddDefaultAttribute(outputType.Interfaces[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, From 8e95ab5bc01502beb316b2e5cc5422c50b5f676e Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Sun, 12 Apr 2026 18:22:20 -0700 Subject: [PATCH 40/50] Fix MethodImpl generic param encoding for event handlers and return types Convert GenericInstanceTypeSignature to open generic form in MethodImpl declaration signatures. E.g., EventHandler\1 becomes EventHandler\1 to match WinRT metadata conventions. Applies to both AddMappedMethod (via ToOpenGenericForm) and AddMappedEvent (via ToOpenGenericFormStatic). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../WinmdWriter.CustomMappedMembers.cs | 80 ++++++++++++++----- 1 file changed, 62 insertions(+), 18 deletions(-) diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.CustomMappedMembers.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.CustomMappedMembers.cs index deb450351..c3d628111 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.CustomMappedMembers.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.CustomMappedMembers.cs @@ -174,22 +174,48 @@ private void AddCustomMappedTypeMembers( } } - // Convert a resolved type signature to use generic parameters (!0, !1) for MethodImpl declarations - TypeSignature ToGenericParam(TypeSignature sig) => - genericArgReverseMap == null - ? sig - : genericArgReverseMap.TryGetValue(sig.FullName, out GenericParameterSignature? gps) - ? gps - : sig switch - { - GenericInstanceTypeSignature innerGits => - new GenericInstanceTypeSignature(innerGits.GenericType, innerGits.IsValueType, [.. innerGits.TypeArguments.Select(ToGenericParam)]), - SzArrayTypeSignature szArray => - new SzArrayTypeSignature(ToGenericParam(szArray.BaseType)), - ByReferenceTypeSignature byRef => - new ByReferenceTypeSignature(ToGenericParam(byRef.BaseType)), - _ => sig - }; + // Convert a resolved type signature to use generic parameters (!0, !1) for MethodImpl declarations. + // This handles two cases: + // 1. Parent interface generic args (e.g., IVector`1 -> !0 for T) + // 2. Generic type instances in signatures (e.g., EventHandler`1 -> EventHandler`1) + TypeSignature ToGenericParam(TypeSignature sig) => sig switch + { + _ when genericArgReverseMap != null + && genericArgReverseMap.TryGetValue(sig.FullName, out GenericParameterSignature? gps) => gps, + 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., EventHandler`1 -> EventHandler`1, IKeyValuePair`2 -> IKeyValuePair`2 + // For parent interface generic args, substitutes them back to !0, !1 etc. + 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]; + // First try parent interface generic arg substitution + if (genericArgReverseMap != null && genericArgReverseMap.TryGetValue(arg.FullName, out GenericParameterSignature? parentGps)) + { + openArgs[i] = parentGps; + } + else if (arg is GenericInstanceTypeSignature nestedGits) + { + // Recursively convert nested generic instances + 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) { @@ -474,8 +500,11 @@ private void AddMappedEvent( evt.Semantics.Add(new MethodSemantics(remover, MethodSemanticsAttributes.RemoveOn)); outputType.Events.Add(evt); - // MethodImpls - MemberReference addRef = new(mappedInterfaceRef, $"add_{eventName}", MethodSignature.CreateInstance(tokenSig, handlerType)); + // MethodImpls (use open generic form for handler type in declaration signature) + 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)); @@ -583,6 +612,21 @@ private static TypeSignature ResolveGenericArg(TypeSignature arg, TypeSignature[ 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. From bd257bdb1097d29b126a13307a533cac6ffd6879 Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Sun, 12 Apr 2026 19:17:16 -0700 Subject: [PATCH 41/50] Implement Span/ReadOnlySpan to WinRT array parameter mapping Map Span and ReadOnlySpan to T[] in WinMD output with correct WinRT array passing conventions: - ReadOnlySpan -> [in] T[] (PassArray: caller provides, callee reads) - Span -> [out] T[] without BYREF (FillArray: caller allocates, callee fills) - out T[] -> [out] T[] with BYREF (ReceiveArray: callee allocates) This replaces the old ReadOnlyArrayAttribute/WriteOnlyArrayAttribute pattern with idiomatic .NET span types for CsWinRT 3.0. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generation/WinmdWriter.Members.cs | 71 +++++++++++++++---- .../Generation/WinmdWriter.TypeMapping.cs | 8 +++ 2 files changed, 65 insertions(+), 14 deletions(-) diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Members.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Members.cs index 0c92d8e80..6f313b353 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Members.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Members.cs @@ -1,10 +1,12 @@ // 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; @@ -40,15 +42,8 @@ private void AddMethodToInterface(TypeDefinition outputType, MethodDefinition in attrs, MethodSignature.CreateInstance(returnType, parameterTypes)); - // Add parameter definitions - int paramIndex = 1; - foreach (ParameterDefinition inputParam in inputMethod.ParameterDefinitions) - { - outputMethod.ParameterDefinitions.Add(new ParameterDefinition( - (ushort)paramIndex++, - inputParam.Name!.Value, - ParameterAttributes.In)); - } + // Add parameter definitions with correct attributes for WinRT array conventions + AddParameterDefinitions(outputMethod, inputMethod); outputType.Methods.Add(outputMethod); @@ -98,20 +93,68 @@ private void AddMethodToClass(TypeDefinition outputType, MethodDefinition inputM ImplAttributes = MethodImplAttributes.Runtime | MethodImplAttributes.Managed }; - // Add parameter definitions + // 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, - ParameterAttributes.In)); + paramAttrs)); } + } - outputType.Methods.Add(outputMethod); + /// + /// 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; + } - // Copy custom attributes from the input method - CopyCustomAttributes(inputMethod, outputMethod); + // 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) diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.TypeMapping.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.TypeMapping.cs index da1a09fc7..5df492aa5 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.TypeMapping.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.TypeMapping.cs @@ -58,6 +58,14 @@ private TypeSignature MapTypeSignatureToOutput(TypeSignature inputSig) { 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)) { From 0b12fcd3e8d2c7af1b9ee842feb25dc112292658 Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Sun, 12 Apr 2026 20:08:37 -0700 Subject: [PATCH 42/50] Updated test --- src/Tests/AuthoringTest/AuthoringTest.csproj | 1 - src/Tests/AuthoringTest/Program.cs | 172 ++++--------------- 2 files changed, 33 insertions(+), 140 deletions(-) diff --git a/src/Tests/AuthoringTest/AuthoringTest.csproj b/src/Tests/AuthoringTest/AuthoringTest.csproj index f545de49b..c2b6147a3 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 f5b7a80f7..37d0035cd 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,18 @@ 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 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; #pragma warning disable CA1416 @@ -136,7 +132,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 +142,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++) { @@ -199,8 +200,8 @@ public sealed class CustomWWW : IWwwFormUrlDecoderEntry public string Value => "CsWinRT"; } - - [GeneratedBindableCustomProperty] + + // [GeneratedBindableCustomProperty] public sealed partial class CustomProperty { public int Number { get; } = 4; @@ -209,7 +210,7 @@ public sealed partial class CustomProperty public CustomPropertyStructType CustomPropertyStructType => new CustomPropertyStructType(); } - [GeneratedBindableCustomProperty] + // [GeneratedBindableCustomProperty] public partial struct CustomPropertyStructType { // Public WinRT struct types must have at least one field @@ -219,14 +220,14 @@ public partial struct CustomPropertyStructType public string Value => "CsWinRTFromStructType"; } - [GeneratedBindableCustomProperty] + // [GeneratedBindableCustomProperty] internal sealed partial record CustomPropertyRecordType { public int Number { get; } = 4; public string Value => "CsWinRTFromRecordType"; } - [GeneratedBindableCustomProperty] + // [GeneratedBindableCustomProperty] internal partial record struct CustomPropertyRecordStructType { public int Number => 4; @@ -241,7 +242,7 @@ public static class CustomPropertyRecordTypeFactory public static object CreateRecordStruct() => default(CustomPropertyRecordStructType); } - + public sealed partial class CustomPropertyProviderWithExplicitImplementation : ICustomPropertyProvider { public Type Type => typeof(CustomPropertyProviderWithExplicitImplementation); @@ -489,7 +490,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 +626,7 @@ public IList GetTypeErasedProjectedArrays() } } - [WinRTRuntimeClassName("AuthoringTest.DisposableClassImpl")] + [WindowsRuntimeClassName("AuthoringTest.DisposableClassImpl")] public sealed class DisposableClass : IDisposable { public bool IsDisposed { get; set; } @@ -741,7 +742,7 @@ IEnumerator IEnumerable.GetEnumerator() } } - [WinRTRuntimeClassName("AuthoringTest.CustomReadOnlyDictionaryImpl")] + [WindowsRuntimeClassName("AuthoringTest.CustomReadOnlyDictionaryImpl")] public sealed class CustomReadOnlyDictionary : IReadOnlyDictionary { private readonly CustomDictionary _dictionary; @@ -817,7 +818,7 @@ public void Reset() } } */ - + public sealed class CustomVector : IList { private IList _list; @@ -990,7 +991,7 @@ public void RemoveAt(int index) } } - [WinRTRuntimeClassName("AuthoringTest.StaticClassImpl")] + [WindowsRuntimeClassName("AuthoringTest.StaticClassImpl")] public static class StaticClass { public static int GetNumber() @@ -1014,7 +1015,7 @@ public static void FireDelegate(double value) DelegateEvent?.Invoke(value); } } - + public static class ButtonUtils { public static Button GetButton() @@ -1120,7 +1121,7 @@ protected override Size ArrangeOverride(Size finalSize) } } - public sealed class CustomXamlServiceProvider : IXamlServiceProvider + public sealed class CustomXamlServiceProvider : IServiceProvider { public object GetService(Type type) { @@ -1301,7 +1302,7 @@ public void RunInitializer() throw new NotImplementedException(); } } - + public sealed class SingleInterfaceClass : IDouble { private double _number; @@ -1637,7 +1638,7 @@ void IList.RemoveAt(int index) _list.RemoveAt(index); } } - + public sealed class CustomDictionary2 : IDictionary { private readonly Dictionary _dictionary = new Dictionary(); @@ -1711,7 +1712,7 @@ bool IDictionary.TryGetValue(string key, out int value) public sealed class TestCollection : CollectionBase { } - + public partial interface IPartialInterface { public string GetNumberAsString(); @@ -1842,6 +1843,7 @@ public void Dispose() } } + /* public sealed class TestMixedWinRTCOMWrapper : IGraphicsEffectSource, IPublicInterface, IInternalInterface1, SomeInternalType.IInternalInterface2 { public string HelloWorld() @@ -1863,101 +1865,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 +1899,7 @@ public string GetText() } } - [WinRTRuntimeClassName("AuthoringTest.NonActivatableFactoryImpl")] + [WindowsRuntimeClassName("AuthoringTest.NonActivatableFactoryImpl")] public static class NonActivatableFactory { public static NonActivatableType Create() @@ -2015,26 +1929,6 @@ public string GetText() } } -namespace ABI.AuthoringTest -{ - internal static class IInternalInterface1Methods - { - public static Guid IID => typeof(global::AuthoringTest.IInternalInterface1).GUID; - - public static IntPtr AbiToProjectionVftablePtr => global::AuthoringTest.IInternalInterface1.Vftbl.AbiToProjectionVftablePtr; - } - - internal struct SomeInternalType - { - internal static class IInternalInterface2Methods - { - public static Guid IID => typeof(global::AuthoringTest.SomeInternalType.IInternalInterface2).GUID; - - public static IntPtr AbiToProjectionVftablePtr => global::AuthoringTest.SomeInternalType.IInternalInterface2.Vftbl.AbiToProjectionVftablePtr; - } - } -} - namespace AnotherNamespace { internal partial class PartialClass3 From eed3250312bb8ea4b062ab9d115dd146714dbd37 Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Sun, 12 Apr 2026 20:32:43 -0700 Subject: [PATCH 43/50] Fix code review findings: generic params, duplicate args, parameter attributes - Fix !0 generic params on non-generic interfaces: only convert to open generic form when parent interface is generic (ToGenericParam, AddMappedEvent) - Fix duplicate type args overwrite in reverse map: replace string-keyed dictionary with index-based FindParentGenericParam that returns first match - Fix explicit interface impl params always [In]: use GetWinRTParameterAttributes to correctly set Out for ByRef/Span parameters - Fix factory method params always [In]: use AddParameterDefinitions helper - Fix resolver comment: accurately describe same-directory search behavior Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../WinmdWriter.CustomMappedMembers.cs | 74 +++++++++++++------ .../WinmdWriter.SynthesizedInterfaces.cs | 12 +-- .../Generation/WinmdWriter.Types.cs | 5 +- .../Resolvers/PathAssemblyResolver.cs | 6 +- 4 files changed, 59 insertions(+), 38 deletions(-) diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.CustomMappedMembers.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.CustomMappedMembers.cs index c3d628111..dae15cf3f 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.CustomMappedMembers.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.CustomMappedMembers.cs @@ -161,50 +161,75 @@ private void AddCustomMappedTypeMembers( // For generic types, use short type names (e.g. "IMap`2" not "IMap`2") string qualifiedPrefix = FormatQualifiedInterfaceName(mappedInterfaceRef); - // Build a reverse map from resolved type args back to generic parameters for MethodImpl signatures. + // 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). - Dictionary? genericArgReverseMap = null; - if (mappedInterfaceRef is TypeSpecification mts && mts.Signature is GenericInstanceTypeSignature mgits) + TypeSignature[]? parentGenericArgs = null; + if (mappedInterfaceRef is TypeSpecification mts2 && mts2.Signature is GenericInstanceTypeSignature mgits) { - genericArgReverseMap = []; - for (int i = 0; i < mgits.TypeArguments.Count; i++) + 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) { - string argFullName = mgits.TypeArguments[i].FullName; - genericArgReverseMap[argFullName] = new GenericParameterSignature(_outputModule, GenericParameterType.Type, i); + 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. - // This handles two cases: - // 1. Parent interface generic args (e.g., IVector`1 -> !0 for T) - // 2. Generic type instances in signatures (e.g., EventHandler`1 -> EventHandler`1) - TypeSignature ToGenericParam(TypeSignature sig) => sig switch + // 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) { - _ when genericArgReverseMap != null - && genericArgReverseMap.TryGetValue(sig.FullName, out GenericParameterSignature? gps) => gps, - GenericInstanceTypeSignature innerGits => ToOpenGenericForm(innerGits), - SzArrayTypeSignature szArray => new SzArrayTypeSignature(ToGenericParam(szArray.BaseType)), - ByReferenceTypeSignature byRef => new ByReferenceTypeSignature(ToGenericParam(byRef.BaseType)), - _ => 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., EventHandler`1 -> EventHandler`1, IKeyValuePair`2 -> IKeyValuePair`2 - // For parent interface generic args, substitutes them back to !0, !1 etc. + // 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]; - // First try parent interface generic arg substitution - if (genericArgReverseMap != null && genericArgReverseMap.TryGetValue(arg.FullName, out GenericParameterSignature? parentGps)) + // 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) { - // Recursively convert nested generic instances openArgs[i] = ToOpenGenericForm(nestedGits); } else @@ -500,7 +525,8 @@ private void AddMappedEvent( evt.Semantics.Add(new MethodSemantics(remover, MethodSemanticsAttributes.RemoveOn)); outputType.Events.Add(evt); - // MethodImpls (use open generic form for handler type in declaration signature) + // 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; diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.SynthesizedInterfaces.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.SynthesizedInterfaces.cs index eaec08f93..1fa31c2db 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.SynthesizedInterfaces.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.SynthesizedInterfaces.cs @@ -7,7 +7,6 @@ using AsmResolver.DotNet.Signatures; using WindowsRuntime.WinMDGenerator.Discovery; using MethodAttributes = AsmResolver.PE.DotNet.Metadata.Tables.MethodAttributes; -using ParameterAttributes = AsmResolver.PE.DotNet.Metadata.Tables.ParameterAttributes; using TypeAttributes = AsmResolver.PE.DotNet.Metadata.Tables.TypeAttributes; namespace WindowsRuntime.WinMDGenerator.Generation; @@ -275,15 +274,8 @@ private void AddFactoryMethod(TypeDefinition synthesizedInterface, TypeDefinitio MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.Abstract | MethodAttributes.Virtual | MethodAttributes.NewSlot, MethodSignature.CreateInstance(returnType, parameterTypes)); - // Add parameter names with [In] attribute - int paramIndex = 1; - foreach (ParameterDefinition inputParam in constructor.ParameterDefinitions) - { - factoryMethod.ParameterDefinitions.Add(new ParameterDefinition( - (ushort)paramIndex++, - inputParam.Name!.Value, - ParameterAttributes.In)); - } + // Add parameter definitions with correct WinRT attributes + AddParameterDefinitions(factoryMethod, constructor); synthesizedInterface.Methods.Add(factoryMethod); } diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs index 442bd1fed..435f7c801 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs @@ -579,8 +579,11 @@ private void AddExplicitInterfaceImplementations(TypeDefinition inputType, TypeD 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], ParameterAttributes.In)); + (ushort)(i + 1), paramNames[i], paramAttr)); } outputType.Methods.Add(outputMethod); diff --git a/src/WinRT.WinMD.Generator/Resolvers/PathAssemblyResolver.cs b/src/WinRT.WinMD.Generator/Resolvers/PathAssemblyResolver.cs index d5593422a..ef84deeb8 100644 --- a/src/WinRT.WinMD.Generator/Resolvers/PathAssemblyResolver.cs +++ b/src/WinRT.WinMD.Generator/Resolvers/PathAssemblyResolver.cs @@ -68,9 +68,9 @@ public PathAssemblyResolver(string[] referencePaths) } } - // Fallback: search sibling directories of existing reference paths. - // This handles type forwarding scenarios where the forwarder assembly (e.g., Microsoft.Windows.SDK.NET) - // forwards to an assembly (e.g., WinRT.Sdk.Projection) that lives in a sibling directory. + // Fallback: search the same directories as existing reference paths for assemblies + // not directly in the reference list. This handles type forwarding scenarios where + // a referenced assembly forwards to another assembly in the same directory. string targetFileName = assembly.Name + ".dll"; foreach (string path in _referencePaths) { From 2a7afa7449fbff2da62bef81220c8d2cad2cab49 Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Sun, 12 Apr 2026 21:02:44 -0700 Subject: [PATCH 44/50] Fix IsProjectionEquivalent for generic type instances Strip type arguments from FullName before TypeMapper lookup so instantiated generic types like IEnumerable\1 correctly match the open generic mapper key IEnumerable\1. Without this, MethodImpl matching for interfaces with mapped generic parameters would silently fail. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Generation/WinmdWriter.cs | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs index f63e64ff0..44b7bc9de 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs @@ -365,10 +365,29 @@ public void FinalizeGeneration() /// private bool IsProjectionEquivalent(string dotNetTypeName, string winrtTypeName) { - if (_mapper.HasMappingForType(dotNetTypeName)) + // 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) { - (string ns, string name, _, _, _) = _mapper.GetMappedType(dotNetTypeName).GetMapping(); + 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; } From 98472fe30466b34f8fef9e8c3e0f7654cc12b736 Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Sun, 12 Apr 2026 21:16:32 -0700 Subject: [PATCH 45/50] Fix assembly references and TypeDef redirection for WinMD convention - Use WindowsRuntimeTypeAttribute to get WinRT contract assembly names for external types instead of projection assembly names (e.g., StackPanel references Microsoft.UI.Xaml not Microsoft.WinUI) - Add EnsureTypeReference to convert TypeDef to TypeRef for interface implementations, matching WinMD TypeDef redirection convention - Apply to class interfaces, interface inheritance, and synthesized interfaces Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Discovery/AssemblyAnalyzer.cs | 12 +++---- .../WinmdWriter.SynthesizedInterfaces.cs | 4 +-- .../Generation/WinmdWriter.TypeMapping.cs | 31 +++++++++++++++++++ .../Generation/WinmdWriter.Types.cs | 4 +-- .../Generation/WinmdWriter.cs | 11 +++++++ .../Models/TypeMapper.cs | 26 ++++++++-------- 6 files changed, 65 insertions(+), 23 deletions(-) diff --git a/src/WinRT.WinMD.Generator/Discovery/AssemblyAnalyzer.cs b/src/WinRT.WinMD.Generator/Discovery/AssemblyAnalyzer.cs index b3e11d5d3..ac3025943 100644 --- a/src/WinRT.WinMD.Generator/Discovery/AssemblyAnalyzer.cs +++ b/src/WinRT.WinMD.Generator/Discovery/AssemblyAnalyzer.cs @@ -77,25 +77,25 @@ private static bool IsPublicType(TypeDefinition type) } /// - /// Checks whether a type is a WinRT type (has the WindowsRuntimeTypeAttribute). + /// 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 == "WindowsRuntimeTypeAttribute"); + attr => attr.Constructor?.DeclaringType?.Name?.Value == "WindowsRuntimeMetadataAttribute"); } /// - /// Gets the assembly name from a WindowsRuntimeTypeAttribute on a type, if present. + /// 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 == "WindowsRuntimeTypeAttribute" && - attr.Signature?.FixedArguments.Count > 0) + if (attr.Constructor?.DeclaringType?.Name?.Value == "WindowsRuntimeMetadataAttribute" + && attr.Signature?.FixedArguments.Count > 0) { - return attr.Signature.FixedArguments[0].Element as string; + return attr.Signature.FixedArguments[0].Element?.ToString(); } } diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.SynthesizedInterfaces.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.SynthesizedInterfaces.cs index 1fa31c2db..efffb391e 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.SynthesizedInterfaces.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.SynthesizedInterfaces.cs @@ -230,8 +230,8 @@ private void AddSynthesizedInterface( { classDeclaration.DefaultInterface = qualifiedInterfaceName; - // Add interface implementation on the class (use the TypeDefinition directly since it's in the same module) - InterfaceImplementation interfaceImpl = new(synthesizedInterface); + // 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 diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.TypeMapping.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.TypeMapping.cs index 5df492aa5..2e1b41b0a 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.TypeMapping.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.TypeMapping.cs @@ -158,7 +158,20 @@ private ITypeDefOrRef ImportTypeReference(ITypeDefOrRef type) 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 = typeRef.Resolve(); + if (resolvedType != null) + { + string? winrtAssembly = AssemblyAnalyzer.GetAssemblyForWinRTType(resolvedType); + if (winrtAssembly != null) + { + assembly = winrtAssembly; + } + } + return GetOrCreateTypeReference(ns, name, assembly); } @@ -182,6 +195,24 @@ private static string GetAssemblyNameFromScope(IResolutionScope? scope) }; } + /// + /// 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}"; diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs index 435f7c801..2615aa361 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs @@ -221,7 +221,7 @@ private void AddInterfaceType(TypeDefinition inputType) { if (impl.Interface != null) { - ITypeDefOrRef outputInterfaceRef = ImportTypeReference(impl.Interface); + ITypeDefOrRef outputInterfaceRef = EnsureTypeReference(ImportTypeReference(impl.Interface)); outputType.Interfaces.Add(new InterfaceImplementation(outputInterfaceRef)); } } @@ -429,7 +429,7 @@ private void AddClassType(TypeDefinition inputType) continue; } - ITypeDefOrRef outputInterfaceRef = ImportTypeReference(impl.Interface); + ITypeDefOrRef outputInterfaceRef = EnsureTypeReference(ImportTypeReference(impl.Interface)); outputType.Interfaces.Add(new InterfaceImplementation(outputInterfaceRef)); } diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs index 44b7bc9de..9c7c59516 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs @@ -135,6 +135,17 @@ public void FinalizeGeneration() else { interfaceDef = classInterfaceImpl.Interface?.Resolve(); + + // 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), diff --git a/src/WinRT.WinMD.Generator/Models/TypeMapper.cs b/src/WinRT.WinMD.Generator/Models/TypeMapper.cs index 4d99087a7..a2b09ee83 100644 --- a/src/WinRT.WinMD.Generator/Models/TypeMapper.cs +++ b/src/WinRT.WinMD.Generator/Models/TypeMapper.cs @@ -150,23 +150,23 @@ public TypeMapper(bool useWindowsUIXamlProjections) { "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") }, + { "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") }, - { "System.ComponentModel.INotifyDataErrorInfo", new MappedType("Microsoft.UI.Xaml.Data", "INotifyDataErrorInfo", "Microsoft.UI") }, - { "System.ComponentModel.INotifyPropertyChanged", new MappedType("Microsoft.UI.Xaml.Data", "INotifyPropertyChanged", "Microsoft.UI") }, - { "System.ComponentModel.PropertyChangedEventArgs", new MappedType("Microsoft.UI.Xaml.Data", "PropertyChangedEventArgs", "Microsoft.UI") }, - { "System.ComponentModel.PropertyChangedEventHandler", new MappedType("Microsoft.UI.Xaml.Data", "PropertyChangedEventHandler", "Microsoft.UI") }, - { "System.Windows.Input.ICommand", new MappedType("Microsoft.UI.Xaml.Input", "ICommand", "Microsoft.UI") }, - { "System.Collections.IEnumerable", new MappedType("Microsoft.UI.Xaml.Interop", "IBindableIterable", "Microsoft.UI") }, - { "System.Collections.IList", new MappedType("Microsoft.UI.Xaml.Interop", "IBindableVector", "Microsoft.UI") }, - { "System.Collections.Specialized.INotifyCollectionChanged", new MappedType("Microsoft.UI.Xaml.Interop", "INotifyCollectionChanged", "Microsoft.UI") }, - { "System.Collections.Specialized.NotifyCollectionChangedAction", new MappedType("Microsoft.UI.Xaml.Interop", "NotifyCollectionChangedAction", "Microsoft.UI", true, true) }, - { "System.Collections.Specialized.NotifyCollectionChangedEventArgs", new MappedType("Microsoft.UI.Xaml.Interop", "NotifyCollectionChangedEventArgs", "Microsoft.UI") }, - { "System.Collections.Specialized.NotifyCollectionChangedEventHandler", new MappedType("Microsoft.UI.Xaml.Interop", "NotifyCollectionChangedEventHandler", "Microsoft.UI") }, + { "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") }, From 37a6588e16d28a450ea4ef9289c64c2094c988bf Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Sun, 12 Apr 2026 22:47:28 -0700 Subject: [PATCH 46/50] Add comprehensive WinMD test types for edge case coverage Add test types covering: out params on interfaces, nullable/IReference parameters, mapped type parameters (DateTimeOffset, TimeSpan, Uri), mixed Span/array params, IDisposable + custom interface, multiple constructors, static complex props, overloaded methods with DefaultOverload, nested structs, multi-param delegates, Flags enums, signed enums, IAsyncAction, versioned interface members, deprecated members, INotifyPropertyChanged + custom interface, factory + static combined, and full-featured interface (props + methods + events). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Tests/AuthoringTest/Program.cs | 327 ++++++++++++++++++++++++++++- 1 file changed, 326 insertions(+), 1 deletion(-) diff --git a/src/Tests/AuthoringTest/Program.cs b/src/Tests/AuthoringTest/Program.cs index 37d0035cd..321dca921 100644 --- a/src/Tests/AuthoringTest/Program.cs +++ b/src/Tests/AuthoringTest/Program.cs @@ -9,6 +9,7 @@ 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; @@ -1951,4 +1952,328 @@ public void InternalFunction() { } } -} \ No newline at end of file + + // 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; + } + } + + // Mapped type parameters (DateTimeOffset, TimeSpan, Uri, Exception) + public sealed class MappedTypeParamClass + { + 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(); + } + } + + // Mixed Span and regular array parameters + public sealed class MixedArrayClass + { + 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(); + } + } + + // Classes implementing IDisposable + custom interface + public interface ICustomResource + { + 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))); + } + } + + // 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"); + } + } +} From 0a824e20077fe229037528456b872fe3faa1164d Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Sun, 12 Apr 2026 23:06:39 -0700 Subject: [PATCH 47/50] Add contract versioning support for WinMD generation - HasVersionAttribute now also checks for ContractVersionAttribute - Skip adding VersionAttribute when input type has ContractVersionAttribute (ContractVersionAttribute is copied via CopyCustomAttributes in Phase 3) - Type arguments in ContractVersionAttribute are mapped via existing CloneAttributeArgument/MapTypeSignatureToOutput - ApiContract empty structs emitted as-is (allowed per WinMD spec) - Add contract versioning test types (AnotherNamespaceContract, ContractVersionedClass) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Tests/AuthoringTest/Program.cs | 81 +++++++++++++++++++ .../Generation/WinmdWriter.Attributes.cs | 53 ++++++++++-- .../Generation/WinmdWriter.Types.cs | 45 +++++++++++ .../Generation/WinmdWriter.cs | 14 +++- 4 files changed, 184 insertions(+), 9 deletions(-) diff --git a/src/Tests/AuthoringTest/Program.cs b/src/Tests/AuthoringTest/Program.cs index 321dca921..95bbefbe9 100644 --- a/src/Tests/AuthoringTest/Program.cs +++ b/src/Tests/AuthoringTest/Program.cs @@ -2276,4 +2276,85 @@ 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.WinMD.Generator/Generation/WinmdWriter.Attributes.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Attributes.cs index c0e452195..6f03b65fa 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Attributes.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Attributes.cs @@ -187,7 +187,7 @@ private void AddDefaultAttribute(InterfaceImplementation interfaceImpl) private static bool HasVersionAttribute(TypeDefinition type) { return type.CustomAttributes.Any( - attr => attr.Constructor?.DeclaringType?.Name?.Value == "VersionAttribute"); + attr => attr.Constructor?.DeclaringType?.Name?.Value is "VersionAttribute" or "ContractVersionAttribute"); } private int GetVersion(TypeDefinition type) @@ -244,7 +244,7 @@ private static bool ShouldCopyAttribute(CustomAttribute attr) // Skip attributes already handled separately by the generator if (attrTypeName is "System.Runtime.InteropServices.GuidAttribute" or - "WinRT.GeneratedBindableCustomPropertyAttribute" or + "WindowsRuntime.Xaml.GeneratedCustomPropertyProviderAttribute" or "Windows.Foundation.Metadata.VersionAttribute" or "System.Reflection.DefaultMemberAttribute") { @@ -280,12 +280,15 @@ private static bool ShouldCopyAttribute(CustomAttribute attr) ITypeDefOrRef importedType = ImportTypeReference(ctor.DeclaringType); - TypeSignature[] mappedParams = [.. methodSig.ParameterTypes - .Select(MapTypeSignatureToOutput)]; + // 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, - mappedParams); + importedParams); return new MemberReference(importedType, ".ctor", importedSig); } @@ -322,12 +325,50 @@ private CustomAttributeSignature CloneAttributeSignature(CustomAttributeSignatur 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(); + } + + // Enum types: import the reference so the blob encoder can resolve the underlying type + TypeDefinition? resolved = typeDefOrRefSig.Type?.Resolve(); + 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 = MapTypeSignatureToOutput(arg.ArgumentType); + TypeSignature mappedType = ImportTypeSignatureForAttribute(arg.ArgumentType); CustomAttributeArgument clonedArg = new(mappedType); if (arg.IsNullArray) diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs index 2615aa361..19a0d1f1b 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs @@ -20,8 +20,53 @@ 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 = diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs index 9c7c59516..2c0688cb7 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs @@ -283,9 +283,17 @@ public void FinalizeGeneration() if (!HasVersionAttribute(declaration.OutputType)) { - // 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); + // 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); + } } } From 26987d91c95cb796933e435bc0dcb586a311ec7e Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Mon, 13 Apr 2026 00:23:59 -0700 Subject: [PATCH 48/50] Restore custom property --- src/Tests/AuthoringTest/Program.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Tests/AuthoringTest/Program.cs b/src/Tests/AuthoringTest/Program.cs index 95bbefbe9..b4005194b 100644 --- a/src/Tests/AuthoringTest/Program.cs +++ b/src/Tests/AuthoringTest/Program.cs @@ -19,6 +19,7 @@ using Windows.Graphics.Effects; using WindowsRuntime; using WindowsRuntime.InteropServices; +using WindowsRuntime.Xaml; #pragma warning disable CA1416 @@ -201,8 +202,8 @@ public sealed class CustomWWW : IWwwFormUrlDecoderEntry public string Value => "CsWinRT"; } - - // [GeneratedBindableCustomProperty] + + [GeneratedCustomPropertyProvider] public sealed partial class CustomProperty { public int Number { get; } = 4; @@ -211,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 @@ -221,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; From 0c69d9e1c07a60eb0243360ca761da9b1cc7bb14 Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Mon, 13 Apr 2026 00:46:46 -0700 Subject: [PATCH 49/50] Update slnx --- src/cswinrt.slnx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/cswinrt.slnx b/src/cswinrt.slnx index f020a72c7..2d293329a 100644 --- a/src/cswinrt.slnx +++ b/src/cswinrt.slnx @@ -165,7 +165,8 @@ - + + From 115dee9303e93b8b2e7bbe26fca10657337018e7 Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Wed, 15 Apr 2026 00:42:06 -0700 Subject: [PATCH 50/50] Update WinMD generator for AsmResolver 6.0.0-rc.1 Remove the custom PathAssemblyResolver and use the built-in one from AsmResolver with FromSearchDirectories for sibling-directory fallback. Add RuntimeContextExtensions for loading modules via RuntimeContext. Update all API calls for the new AsmResolver signatures: - Resolve() now requires RuntimeContext parameter - ToTypeSignature() now requires isValueType boolean - MethodSignature.CreateInstance params wrapped in collection expressions - MakeGenericReferenceType args wrapped in collection expressions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Extensions/RuntimeContextExtensions.cs | 39 ++++++ .../Generation/WinMDGenerator.Discover.cs | 21 +++- .../Generation/WinmdWriter.Attributes.cs | 39 +++--- .../WinmdWriter.CustomMappedMembers.cs | 44 +++---- .../Generation/WinmdWriter.Members.cs | 18 +-- .../WinmdWriter.SynthesizedInterfaces.cs | 4 +- .../Generation/WinmdWriter.TypeMapping.cs | 2 +- .../Generation/WinmdWriter.Types.cs | 8 +- .../Generation/WinmdWriter.cs | 27 +++- .../Resolvers/PathAssemblyResolver.cs | 119 ------------------ 10 files changed, 138 insertions(+), 183 deletions(-) create mode 100644 src/WinRT.WinMD.Generator/Extensions/RuntimeContextExtensions.cs delete mode 100644 src/WinRT.WinMD.Generator/Resolvers/PathAssemblyResolver.cs diff --git a/src/WinRT.WinMD.Generator/Extensions/RuntimeContextExtensions.cs b/src/WinRT.WinMD.Generator/Extensions/RuntimeContextExtensions.cs new file mode 100644 index 000000000..f35906305 --- /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/Generation/WinMDGenerator.Discover.cs b/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.Discover.cs index b4e17ffb2..42ef276ca 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.Discover.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.Discover.cs @@ -2,10 +2,11 @@ // Licensed under the MIT License. using System; +using System.IO; +using System.Linq; using AsmResolver.DotNet; using WindowsRuntime.WinMDGenerator.Discovery; using WindowsRuntime.WinMDGenerator.Errors; -using WindowsRuntime.WinMDGenerator.Resolvers; namespace WindowsRuntime.WinMDGenerator.Generation; @@ -22,8 +23,22 @@ private static WinMDGeneratorDiscoveryState Discover(WinMDGeneratorArgs args) try { string[] allReferencePaths = [args.InputAssemblyPath, .. args.ReferenceAssemblyPaths]; - PathAssemblyResolver assemblyResolver = new(allReferencePaths); - inputModule = ModuleDefinition.FromFile(args.InputAssemblyPath, assemblyResolver.ReaderParameters); + + // 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) { diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Attributes.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Attributes.cs index 6f03b65fa..d25f3b397 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Attributes.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Attributes.cs @@ -57,7 +57,7 @@ private void AddGuidAttribute(TypeDefinition outputType, Guid guid) MemberReference guidCtor = new(guidAttrType, ".ctor", MethodSignature.CreateInstance( _outputModule.CorLibTypeFactory.Void, - _outputModule.CorLibTypeFactory.UInt32, + [_outputModule.CorLibTypeFactory.UInt32, _outputModule.CorLibTypeFactory.UInt16, _outputModule.CorLibTypeFactory.UInt16, _outputModule.CorLibTypeFactory.Byte, @@ -67,7 +67,7 @@ private void AddGuidAttribute(TypeDefinition outputType, Guid guid) _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))); @@ -93,7 +93,7 @@ private void AddVersionAttribute(TypeDefinition outputType, int version) MemberReference versionCtor = new(versionAttrType, ".ctor", MethodSignature.CreateInstance( _outputModule.CorLibTypeFactory.Void, - _outputModule.CorLibTypeFactory.UInt32)); + [_outputModule.CorLibTypeFactory.UInt32])); CustomAttributeSignature sig = new(); sig.FixedArguments.Add(new CustomAttributeArgument(_outputModule.CorLibTypeFactory.UInt32, (uint)version)); @@ -113,11 +113,11 @@ private void AddActivatableAttribute(TypeDefinition outputType, uint version, st MemberReference ctor = new(activatableAttrType, ".ctor", MethodSignature.CreateInstance( _outputModule.CorLibTypeFactory.Void, - systemType.ToTypeSignature(), - _outputModule.CorLibTypeFactory.UInt32)); + [systemType.ToTypeSignature(false), + _outputModule.CorLibTypeFactory.UInt32])); CustomAttributeSignature sig = new(); - sig.FixedArguments.Add(new CustomAttributeArgument(systemType.ToTypeSignature(), ResolveTypeNameToSignature(factoryInterface))); + 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)); @@ -128,7 +128,7 @@ private void AddActivatableAttribute(TypeDefinition outputType, uint version, st MemberReference ctor = new(activatableAttrType, ".ctor", MethodSignature.CreateInstance( _outputModule.CorLibTypeFactory.Void, - _outputModule.CorLibTypeFactory.UInt32)); + [_outputModule.CorLibTypeFactory.UInt32])); CustomAttributeSignature sig = new(); sig.FixedArguments.Add(new CustomAttributeArgument(_outputModule.CorLibTypeFactory.UInt32, version)); @@ -146,11 +146,11 @@ private void AddStaticAttribute(TypeDefinition classOutputType, uint version, st MemberReference ctor = new(staticAttrType, ".ctor", MethodSignature.CreateInstance( _outputModule.CorLibTypeFactory.Void, - systemType.ToTypeSignature(), - _outputModule.CorLibTypeFactory.UInt32)); + [systemType.ToTypeSignature(false), + _outputModule.CorLibTypeFactory.UInt32])); CustomAttributeSignature sig = new(); - sig.FixedArguments.Add(new CustomAttributeArgument(systemType.ToTypeSignature(), ResolveTypeNameToSignature(staticInterfaceName))); + 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)); @@ -165,10 +165,10 @@ private void AddExclusiveToAttribute(TypeDefinition interfaceType, string classN MemberReference ctor = new(exclusiveToAttrType, ".ctor", MethodSignature.CreateInstance( _outputModule.CorLibTypeFactory.Void, - systemType.ToTypeSignature())); + [systemType.ToTypeSignature(false)])); CustomAttributeSignature sig = new(); - sig.FixedArguments.Add(new CustomAttributeArgument(systemType.ToTypeSignature(), ResolveTypeNameToSignature(className))); + sig.FixedArguments.Add(new CustomAttributeArgument(systemType.ToTypeSignature(false), ResolveTypeNameToSignature(className))); interfaceType.CustomAttributes.Add(new CustomAttribute(ctor, sig)); } @@ -213,7 +213,7 @@ private void CopyCustomAttributes(IHasCustomAttribute source, IHasCustomAttribut { foreach (CustomAttribute attr in source.CustomAttributes) { - if (!ShouldCopyAttribute(attr)) + if (!ShouldCopyAttribute(attr, _runtimeContext)) { continue; } @@ -232,7 +232,7 @@ private void CopyCustomAttributes(IHasCustomAttribute source, IHasCustomAttribut /// /// Determines whether a custom attribute should be copied to the output WinMD. /// - private static bool ShouldCopyAttribute(CustomAttribute attr) + private static bool ShouldCopyAttribute(CustomAttribute attr, RuntimeContext? runtimeContext) { string? attrTypeName = attr.Constructor?.DeclaringType?.FullName; @@ -258,8 +258,11 @@ private static bool ShouldCopyAttribute(CustomAttribute attr) } // Skip non-public attribute types (if resolvable) - TypeDefinition? attrTypeDef = attr.Constructor?.DeclaringType?.Resolve(); - if (attrTypeDef != null && !attrTypeDef.IsPublic && !attrTypeDef.IsNestedPublic) + 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; } @@ -344,11 +347,11 @@ private TypeSignature ImportTypeSignatureForAttribute(TypeSignature sig) // System.Type must stay as System.Type for attribute blob encoding if (fullName == "System.Type") { - return GetOrCreateTypeReference("System", "Type", "mscorlib").ToTypeSignature(); + return GetOrCreateTypeReference("System", "Type", "mscorlib").ToTypeSignature(false); } // Enum types: import the reference so the blob encoder can resolve the underlying type - TypeDefinition? resolved = typeDefOrRefSig.Type?.Resolve(); + TypeDefinition? resolved = SafeResolve(typeDefOrRefSig.Type); if (resolved != null && resolved.IsEnum) { return ImportTypeReference(typeDefOrRefSig.Type!).ToTypeSignature(typeDefOrRefSig.IsValueType); diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.CustomMappedMembers.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.CustomMappedMembers.cs index dae15cf3f..5cd0013c7 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.CustomMappedMembers.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.CustomMappedMembers.cs @@ -50,7 +50,7 @@ private void ProcessCustomMappedInterfaces(TypeDefinition inputType, TypeDefinit // 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(inputType, impl); + bool isPublic = IsInterfacePubliclyImplemented(classType: inputType, impl, _runtimeContext); mappedInterfaces.Add((impl, interfaceName, mapping, isPublic)); } @@ -127,7 +127,7 @@ private TypeSignature MapCustomMappedTypeArgument(TypeSignature arg) MappedType innerMapping = _mapper.GetMappedType(typeName); (string ns, string name, string asm, _, _) = innerMapping.GetMapping(); TypeReference innerRef = GetOrCreateTypeReference(ns, name, asm); - return innerRef.ToTypeSignature(); + return innerRef.ToTypeSignature(tdrs.IsValueType); } } @@ -298,13 +298,13 @@ void AddMappedProperty(string name, TypeSignature propertyType, bool hasSetter) // MethodImpl for getter (use generic params for declaration signature) TypeSignature implPropertyType = ToGenericParam(propertyType); - MemberReference getterRef = new(mappedInterfaceRef, $"get_{name}", MethodSignature.CreateInstance(implPropertyType)); + 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)) + MethodDefinition setter = new(putMethodName, getAttrs, MethodSignature.CreateInstance(_outputModule.CorLibTypeFactory.Void, [propertyType])) { ImplAttributes = MethodImplAttributes.Runtime | MethodImplAttributes.Managed }; @@ -312,7 +312,7 @@ void AddMappedProperty(string name, TypeSignature propertyType, bool hasSetter) outputType.Methods.Add(setter); prop.Semantics.Add(new MethodSemantics(setter, MethodSemanticsAttributes.Setter)); - MemberReference setterRef = new(mappedInterfaceRef, $"put_{name}", MethodSignature.CreateInstance(_outputModule.CorLibTypeFactory.Void, implPropertyType)); + MemberReference setterRef = new(mappedInterfaceRef, $"put_{name}", MethodSignature.CreateInstance(_outputModule.CorLibTypeFactory.Void, [implPropertyType])); outputType.MethodImplementations.Add(new MethodImplementation(setterRef, setter)); } @@ -332,8 +332,8 @@ TypeSignature GetGenericArg(int index) TypeSignature objectSig = _outputModule.CorLibTypeFactory.Object; TypeSignature stringSig = _outputModule.CorLibTypeFactory.String; - TypeSignature GetTypeRef(string ns, string name, string asm) => - GetOrCreateTypeReference(ns, name, asm).ToTypeSignature(); + 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); @@ -471,7 +471,7 @@ TypeSignature GetGenericTypeRef(string ns, string name, string asm, params TypeS case "IXamlServiceProvider": AddMappedMethod("GetService", - [("type", GetTypeRef("Windows.UI.Xaml.Interop", "TypeName", "Windows.Foundation.UniversalApiContract"), ParameterAttributes.In)], + [("type", GetTypeRef("Windows.UI.Xaml.Interop", "TypeName", "Windows.Foundation.UniversalApiContract", isValueType: true), ParameterAttributes.In)], objectSig); break; @@ -492,7 +492,7 @@ private void AddMappedEvent( { string qualifiedPrefix = mappedInterfaceRef.FullName ?? ""; TypeReference tokenType = GetOrCreateTypeReference("Windows.Foundation", "EventRegistrationToken", "Windows.Foundation.FoundationContract"); - TypeSignature tokenSig = tokenType.ToTypeSignature(); + TypeSignature tokenSig = tokenType.ToTypeSignature(true); ITypeDefOrRef handlerTypeRef = handlerType is TypeDefOrRefSignature tdrs ? tdrs.Type : (handlerType is GenericInstanceTypeSignature gits ? new TypeSpecification(gits) : tokenType); @@ -504,7 +504,7 @@ private void AddMappedEvent( : (MethodAttributes.Private | MethodAttributes.Final | MethodAttributes.Virtual | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.SpecialName); // Add method - MethodDefinition adder = new(addName, attrs, MethodSignature.CreateInstance(tokenSig, handlerType)) + MethodDefinition adder = new(addName, attrs, MethodSignature.CreateInstance(tokenSig, [handlerType])) { ImplAttributes = MethodImplAttributes.Runtime | MethodImplAttributes.Managed }; @@ -512,7 +512,7 @@ private void AddMappedEvent( outputType.Methods.Add(adder); // Remove method - MethodDefinition remover = new(removeName, attrs, MethodSignature.CreateInstance(_outputModule.CorLibTypeFactory.Void, tokenSig)) + MethodDefinition remover = new(removeName, attrs, MethodSignature.CreateInstance(_outputModule.CorLibTypeFactory.Void, [tokenSig])) { ImplAttributes = MethodImplAttributes.Runtime | MethodImplAttributes.Managed }; @@ -530,9 +530,9 @@ private void AddMappedEvent( TypeSignature implHandlerType = handlerType is GenericInstanceTypeSignature handlerGits ? ToOpenGenericFormStatic(handlerGits, _outputModule) : handlerType; - MemberReference addRef = new(mappedInterfaceRef, $"add_{eventName}", MethodSignature.CreateInstance(tokenSig, implHandlerType)); + 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)); + MemberReference removeRef = new(mappedInterfaceRef, $"remove_{eventName}", MethodSignature.CreateInstance(_outputModule.CorLibTypeFactory.Void, [tokenSig])); outputType.MethodImplementations.Add(new MethodImplementation(removeRef, remover)); } @@ -577,8 +577,8 @@ void CollectFromType(TypeDefinition typeDef, TypeSignature[]? genericArgs) // Also collect interfaces inherited by this interface TypeDefinition? interfaceDef = resolvedInterface is TypeSpecification ts2 - ? (ts2.Signature as GenericInstanceTypeSignature)?.GenericType.Resolve() - : resolvedInterface.Resolve(); + ? SafeResolve((ts2.Signature as GenericInstanceTypeSignature)?.GenericType) + : SafeResolve(resolvedInterface); if (interfaceDef != null) { @@ -599,7 +599,7 @@ ts3.Signature is GenericInstanceTypeSignature innerGits ITypeDefOrRef? baseTypeRef = type.BaseType; while (baseTypeRef != null) { - TypeDefinition? baseDef = baseTypeRef.Resolve(); + TypeDefinition? baseDef = SafeResolve(baseTypeRef); if (baseDef == null) { break; @@ -658,11 +658,11 @@ private static GenericInstanceTypeSignature ToOpenGenericFormStatic(GenericInsta /// 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) + private static bool IsInterfacePubliclyImplemented(TypeDefinition classType, InterfaceImplementation impl, RuntimeContext? runtimeContext) { TypeDefinition? interfaceDef = impl.Interface is TypeSpecification ts - ? (ts.Signature as GenericInstanceTypeSignature)?.GenericType.Resolve() - : impl.Interface?.Resolve(); + ? (ts.Signature as GenericInstanceTypeSignature)?.GenericType.Resolve(runtimeContext) + : impl.Interface?.Resolve(runtimeContext); if (interfaceDef == null) { @@ -682,7 +682,7 @@ private static bool IsInterfacePubliclyImplemented(TypeDefinition classType, Int } } - current = current.BaseType?.Resolve(); + current = current.BaseType?.Resolve(runtimeContext); } return false; @@ -749,8 +749,8 @@ private HashSet CollectCustomMappedMemberNames(TypeDefinition inputType) } TypeDefinition? interfaceDef = impl.Interface is TypeSpecification ts - ? (ts.Signature as GenericInstanceTypeSignature)?.GenericType.Resolve() - : impl.Interface.Resolve(); + ? SafeResolve((ts.Signature as GenericInstanceTypeSignature)?.GenericType) + : SafeResolve(impl.Interface); if (interfaceDef == null) { diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Members.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Members.cs index 6f313b353..fc9dfc068 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Members.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Members.cs @@ -218,8 +218,8 @@ private void AddPropertyToType(TypeDefinition outputType, PropertyDefinition inp } MethodSignature setSignature = isStatic - ? MethodSignature.CreateStatic(_outputModule.CorLibTypeFactory.Void, propertyType) - : MethodSignature.CreateInstance(_outputModule.CorLibTypeFactory.Void, propertyType); + ? MethodSignature.CreateStatic(_outputModule.CorLibTypeFactory.Void, [propertyType]) + : MethodSignature.CreateInstance(_outputModule.CorLibTypeFactory.Void, [propertyType]); MethodDefinition setter = new("put_" + inputProperty.Name.Value, attrs, setSignature); if (!isInterfaceParent) @@ -267,12 +267,12 @@ private void AddEventToType(TypeDefinition outputType, EventDefinition inputEven attrs |= MethodAttributes.Virtual | MethodAttributes.NewSlot | MethodAttributes.Final; } - TypeSignature handlerSig = eventType.ToTypeSignature(); - TypeSignature tokenSig = eventRegistrationTokenType.ToTypeSignature(); + TypeSignature handlerSig = eventType.ToTypeSignature(false); + TypeSignature tokenSig = eventRegistrationTokenType.ToTypeSignature(true); MethodSignature addSignature = isStatic - ? MethodSignature.CreateStatic(tokenSig, handlerSig) - : MethodSignature.CreateInstance(tokenSig, handlerSig); + ? MethodSignature.CreateStatic(tokenSig, [handlerSig]) + : MethodSignature.CreateInstance(tokenSig, [handlerSig]); MethodDefinition adder = new("add_" + inputEvent.Name.Value, attrs, addSignature); if (!isInterfaceParent) @@ -300,11 +300,11 @@ private void AddEventToType(TypeDefinition outputType, EventDefinition inputEven attrs |= MethodAttributes.Virtual | MethodAttributes.NewSlot | MethodAttributes.Final; } - TypeSignature tokenSig = eventRegistrationTokenType.ToTypeSignature(); + TypeSignature tokenSig = eventRegistrationTokenType.ToTypeSignature(true); MethodSignature removeSignature = isStatic - ? MethodSignature.CreateStatic(_outputModule.CorLibTypeFactory.Void, tokenSig) - : MethodSignature.CreateInstance(_outputModule.CorLibTypeFactory.Void, tokenSig); + ? MethodSignature.CreateStatic(_outputModule.CorLibTypeFactory.Void, [tokenSig]) + : MethodSignature.CreateInstance(_outputModule.CorLibTypeFactory.Void, [tokenSig]); MethodDefinition remover = new("remove_" + inputEvent.Name.Value, attrs, removeSignature); if (!isInterfaceParent) diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.SynthesizedInterfaces.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.SynthesizedInterfaces.cs index efffb391e..6a12c0596 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.SynthesizedInterfaces.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.SynthesizedInterfaces.cs @@ -43,8 +43,8 @@ private void AddSynthesizedInterfaces(TypeDefinition inputType, TypeDefinition c foreach (InterfaceImplementation impl in allInterfaces) { TypeDefinition? interfaceDef = impl.Interface is TypeSpecification ts - ? (ts.Signature as GenericInstanceTypeSignature)?.GenericType.Resolve() - : impl.Interface?.Resolve(); + ? SafeResolve((ts.Signature as GenericInstanceTypeSignature)?.GenericType) + : SafeResolve(impl.Interface); if (interfaceDef != null) { diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.TypeMapping.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.TypeMapping.cs index 2e1b41b0a..90ecc687c 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.TypeMapping.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.TypeMapping.cs @@ -162,7 +162,7 @@ private ITypeDefOrRef ImportTypeReference(ITypeDefOrRef type) // 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 = typeRef.Resolve(); + TypeDefinition? resolvedType = SafeResolve(typeRef); if (resolvedType != null) { string? winrtAssembly = AssemblyAnalyzer.GetAssemblyForWinRTType(resolvedType); diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs index 19a0d1f1b..55e69cd6a 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.Types.cs @@ -171,8 +171,8 @@ private void AddDelegateType(TypeDefinition inputType) MethodAttributes.Private | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RuntimeSpecialName, MethodSignature.CreateInstance( _outputModule.CorLibTypeFactory.Void, - _outputModule.CorLibTypeFactory.Object, - _outputModule.CorLibTypeFactory.IntPtr)) + [_outputModule.CorLibTypeFactory.Object, + _outputModule.CorLibTypeFactory.IntPtr])) { ImplAttributes = MethodImplAttributes.Runtime | MethodImplAttributes.Managed }; @@ -346,7 +346,7 @@ private void AddClassType(TypeDefinition inputType) 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 = inputType.BaseType.Resolve(); + TypeDefinition? baseTypeDef = SafeResolve(inputType.BaseType); baseType = baseTypeDef != null && baseTypeDef.IsAbstract ? GetOrCreateTypeReference("System", "Object", "mscorlib") : ImportTypeReference(inputType.BaseType); @@ -547,7 +547,7 @@ private void AddExplicitInterfaceImplementations(TypeDefinition inputType, TypeD { TypeReference eventRegistrationTokenType = GetOrCreateTypeReference( "Windows.Foundation", "EventRegistrationToken", "Windows.Foundation.FoundationContract"); - TypeSignature tokenSig = eventRegistrationTokenType.ToTypeSignature(); + TypeSignature tokenSig = eventRegistrationTokenType.ToTypeSignature(true); foreach (MethodDefinition method in inputType.Methods) { diff --git a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs index 2c0688cb7..3a5090e8d 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinmdWriter.cs @@ -21,6 +21,7 @@ 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; @@ -42,6 +43,7 @@ public WinmdWriter( _version = version; _mapper = mapper; _inputModule = inputModule; + _runtimeContext = inputModule.RuntimeContext; // Create the output WinMD module _outputModule = new ModuleDefinition(assemblyName + ".winmd") @@ -64,6 +66,21 @@ public WinmdWriter( }; } + /// + /// 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. /// @@ -129,12 +146,12 @@ public void FinalizeGeneration() if (classInterfaceImpl.Interface is TypeSpecification ts && ts.Signature is GenericInstanceTypeSignature gits) { - interfaceDef = gits.GenericType.Resolve(); + interfaceDef = SafeResolve(gits.GenericType); interfaceGenericArgs = [.. gits.TypeArguments]; } else { - interfaceDef = classInterfaceImpl.Interface?.Resolve(); + 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. @@ -159,8 +176,8 @@ public void FinalizeGeneration() { interfaceDef = inputImpl.Interface is TypeSpecification its && its.Signature is GenericInstanceTypeSignature igits - ? igits.GenericType.Resolve() - : inputImpl.Interface.Resolve(); + ? SafeResolve(igits.GenericType) + : SafeResolve(inputImpl.Interface); resolvedFromInput = interfaceDef != null; break; } @@ -441,7 +458,7 @@ private void AddOverloadAttribute(MethodDefinition method, string overloadName) MemberReference ctor = new(overloadAttrType, ".ctor", MethodSignature.CreateInstance( _outputModule.CorLibTypeFactory.Void, - _outputModule.CorLibTypeFactory.String)); + [_outputModule.CorLibTypeFactory.String])); CustomAttributeSignature sig = new(); sig.FixedArguments.Add(new CustomAttributeArgument(_outputModule.CorLibTypeFactory.String, overloadName)); diff --git a/src/WinRT.WinMD.Generator/Resolvers/PathAssemblyResolver.cs b/src/WinRT.WinMD.Generator/Resolvers/PathAssemblyResolver.cs deleted file mode 100644 index ef84deeb8..000000000 --- a/src/WinRT.WinMD.Generator/Resolvers/PathAssemblyResolver.cs +++ /dev/null @@ -1,119 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Collections.Concurrent; -using System.IO; -using System.Linq; -using AsmResolver.DotNet; -using AsmResolver.DotNet.Serialized; -using AsmResolver.DotNet.Signatures; - -namespace WindowsRuntime.WinMDGenerator.Resolvers; - -/// -/// A custom from a specific set of reference paths. -/// -internal sealed class PathAssemblyResolver : IAssemblyResolver -{ - /// - /// The input .dll paths to load assemblies from. - /// - private readonly string[] _referencePaths; - - /// - /// The cached assemblies. - /// - private readonly ConcurrentDictionary _cache = new(new SignatureComparer()); - - /// - /// Creates a new instance with the specified parameters. - /// - /// The input .dll paths. - public PathAssemblyResolver(string[] referencePaths) - { - _referencePaths = referencePaths; - ReaderParameters = new RuntimeContext(new DotNetRuntimeInfo(".NETCoreApp", new Version(10, 0)), this).DefaultReaderParameters; - } - - /// - /// Gets the instance to use. - /// - public ModuleReaderParameters ReaderParameters { get; } - - /// - public AssemblyDefinition? Resolve(AssemblyDescriptor assembly) - { - // If we already have the assembly in the cache, return it - if (_cache.TryGetValue(assembly, out AssemblyDefinition? cachedDefinition)) - { - return cachedDefinition; - } - - // We can't load an assembly without a name - if (assembly.Name is null) - { - return null; - } - - // Find the first match in our list of reference paths, and load that assembly - foreach (string path in _referencePaths) - { - if (Path.GetFileNameWithoutExtension(path.AsSpan()).SequenceEqual(assembly.Name)) - { - return _cache.GetOrAdd( - key: assembly, - valueFactory: static (_, args) => AssemblyDefinition.FromFile(args.Path, args.Parameters), - factoryArgument: (Path: path, Parameters: ReaderParameters)); - } - } - - // Fallback: search the same directories as existing reference paths for assemblies - // not directly in the reference list. This handles type forwarding scenarios where - // a referenced assembly forwards to another assembly in the same directory. - string targetFileName = assembly.Name + ".dll"; - foreach (string path in _referencePaths) - { - string? dir = Path.GetDirectoryName(path); - if (dir is null) - { - continue; - } - - string candidate = Path.Combine(dir, targetFileName); - if (File.Exists(candidate)) - { - return _cache.GetOrAdd( - key: assembly, - valueFactory: static (_, args) => AssemblyDefinition.FromFile(args.Path, args.Parameters), - factoryArgument: (Path: candidate, Parameters: ReaderParameters)); - } - } - - return null; - } - - /// - public void AddToCache(AssemblyDescriptor descriptor, AssemblyDefinition definition) - { - _ = _cache.TryAdd(descriptor, definition); - } - - /// - public bool RemoveFromCache(AssemblyDescriptor descriptor) - { - return _cache.TryRemove(descriptor, out _); - } - - /// - public bool HasCached(AssemblyDescriptor descriptor) - { - return _cache.ContainsKey(descriptor); - } - - /// - public void ClearCache() - { - _cache.Clear(); - } -} \ No newline at end of file