From 54d14825ef8cf7e6a55afc3872f9e8f02b5843ba Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 9 Jun 2026 12:07:22 -0700 Subject: [PATCH 01/15] Delete dead SignatureComparerExtensions copies in Impl + Projection 'WinRT.Impl.Generator' and 'WinRT.Projection.Generator' each ship a copy of 'SignatureComparerExtensions' that has zero call sites in their own project. The real consumer is 'WinRT.Interop.Generator', which has its own (larger, strict-superset) copy at 'src/WinRT.Interop.Generator/Extensions/SignatureComparerExtensions.cs'. The two dead copies appear to have been left behind during earlier refactors. Delete them. No behavioral change is possible since they had no callers; the Interop copy is untouched and continues to serve its local consumers. Net diff: -141 LOC across 2 files. Both projects build with 0 warnings in Release. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Extensions/SignatureComparerExtensions.cs | 70 ------------------- .../Extensions/SignatureComparerExtensions.cs | 70 ------------------- 2 files changed, 140 deletions(-) delete mode 100644 src/WinRT.Impl.Generator/Extensions/SignatureComparerExtensions.cs delete mode 100644 src/WinRT.Projection.Generator/Extensions/SignatureComparerExtensions.cs diff --git a/src/WinRT.Impl.Generator/Extensions/SignatureComparerExtensions.cs b/src/WinRT.Impl.Generator/Extensions/SignatureComparerExtensions.cs deleted file mode 100644 index c1aa401f6a..0000000000 --- a/src/WinRT.Impl.Generator/Extensions/SignatureComparerExtensions.cs +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using AsmResolver.DotNet.Signatures; - -namespace WindowsRuntime.ImplGenerator; - -/// -/// Extensions for . -/// -internal static class SignatureComparerExtensions -{ -#pragma warning disable IDE0052 // TODO: remove this once Roslyn bug is fixed - /// - /// Backing field for . - /// - private static readonly SignatureComparer IgnoreVersion = new(SignatureComparisonFlags.VersionAgnostic); -#pragma warning restore IDE0052 - - extension(SignatureComparer comparer) - { - /// - /// An immutable default instance of , with . - /// - public static SignatureComparer IgnoreVersion => IgnoreVersion; - - /// - /// Creates an instance for a pair of values. - /// - /// The resulting instance. - public IEqualityComparer<(TypeSignature, TypeSignature)> MakeValueTupleComparer() - { - return new SignatureValueTupleComparer(comparer); - } - } -} - -/// -/// An for a pair of values. -/// -file sealed class SignatureValueTupleComparer : IEqualityComparer<(TypeSignature, TypeSignature)> -{ - /// - /// The wrapped instance used for comparison. - /// - private readonly SignatureComparer _comparer; - - /// - /// Creates a new instance with the specified parameters. - /// - /// The instance to wrap. - public SignatureValueTupleComparer(SignatureComparer comparer) - { - _comparer = comparer; - } - - /// - public bool Equals((TypeSignature, TypeSignature) x, (TypeSignature, TypeSignature) y) - { - return _comparer.Equals(x.Item1, y.Item1) && _comparer.Equals(x.Item2, y.Item2); - } - - /// - public int GetHashCode((TypeSignature, TypeSignature) obj) - { - return HashCode.Combine(_comparer.GetHashCode(obj.Item1), _comparer.GetHashCode(obj.Item2)); - } -} \ No newline at end of file diff --git a/src/WinRT.Projection.Generator/Extensions/SignatureComparerExtensions.cs b/src/WinRT.Projection.Generator/Extensions/SignatureComparerExtensions.cs deleted file mode 100644 index 82670098d4..0000000000 --- a/src/WinRT.Projection.Generator/Extensions/SignatureComparerExtensions.cs +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using AsmResolver.DotNet.Signatures; - -namespace WindowsRuntime.ProjectionGenerator; - -/// -/// Extensions for . -/// -internal static class SignatureComparerExtensions -{ -#pragma warning disable IDE0052 // TODO: remove this once Roslyn bug is fixed - /// - /// Backing field for . - /// - private static readonly SignatureComparer IgnoreVersion = new(SignatureComparisonFlags.VersionAgnostic); -#pragma warning restore IDE0052 - - extension(SignatureComparer comparer) - { - /// - /// An immutable default instance of , with . - /// - public static SignatureComparer IgnoreVersion => IgnoreVersion; - - /// - /// Creates an instance for a pair of values. - /// - /// The resulting instance. - public IEqualityComparer<(TypeSignature, TypeSignature)> MakeValueTupleComparer() - { - return new SignatureValueTupleComparer(comparer); - } - } -} - -/// -/// An for a pair of values. -/// -file sealed class SignatureValueTupleComparer : IEqualityComparer<(TypeSignature, TypeSignature)> -{ - /// - /// The wrapped instance used for comparison. - /// - private readonly SignatureComparer _comparer; - - /// - /// Creates a new instance with the specified parameters. - /// - /// The instance to wrap. - public SignatureValueTupleComparer(SignatureComparer comparer) - { - _comparer = comparer; - } - - /// - public bool Equals((TypeSignature, TypeSignature) x, (TypeSignature, TypeSignature) y) - { - return _comparer.Equals(x.Item1, y.Item1) && _comparer.Equals(x.Item2, y.Item2); - } - - /// - public int GetHashCode((TypeSignature, TypeSignature) obj) - { - return HashCode.Combine(_comparer.GetHashCode(obj.Item1), _comparer.GetHashCode(obj.Item2)); - } -} From e13702511fa49d7004ac38fecbff8cba84660207 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 9 Jun 2026 12:16:54 -0700 Subject: [PATCH 02/15] Add debug-repro orchestration helpers to DebugReproPacker Extend 'DebugReproPacker' with three orchestration helpers that capture the preamble/tail boilerplate that all 5 generators' 'SaveDebugRepro' and 'UnpackDebugRepro' methods used to repeat verbatim: * 'BeginSave(directory, toolName, archiveFileName)' validates the user-provided directory exists (throws the per-tool 'TError.DebugReproDirectoryDoesNotExist' via the shared 'IGeneratorErrorFactory' contract), builds the target '.zip' path and creates a fresh '{toolName}-debug-repro-{Guid}' staging dir. * 'FinalizeSave(tempDirectory, zipPath)' deletes any pre-existing archive, zips the staging dir, and deletes the staging dir. * 'CreateUnpackTempDirectory(toolName)' creates a fresh '{toolName}-debug-repro-unpack-{Guid}' directory. Each '*Generator.DebugRepro.cs' now calls these helpers in place of the inlined boilerplate. Per-tool concerns stay per-tool: subdirectory layout, file-copy strategy, '.rsp' build, and JSON path-map writes are all unchanged. Per-tool exception identity is fully preserved because every 'BeginSave' call binds to the tool's own 'WellKnown*Exceptions' static-abstract factory. Net diff: 6 files, +103 / -140 LOC (-37 LOC). All 5 generators build clean and pass end-to-end debug-repro validation (ref-gen: 8/8 0 diffs, WinMD: 16-byte MVID-only delta, proj-gen: 327/327 0 diffs, impl-gen: 0 byte diffs, interop-gen: smoke OK). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../DebugRepro/DebugReproPacker.cs | 73 +++++++++++++++++++ .../Generation/ImplGenerator.DebugRepro.cs | 34 ++------- .../Generation/InteropGenerator.DebugRepro.cs | 34 ++------- .../ProjectionGenerator.DebugRepro.cs | 34 ++------- ...ReferenceProjectionGenerator.DebugRepro.cs | 34 ++------- .../Generation/WinMDGenerator.DebugRepro.cs | 34 ++------- 6 files changed, 103 insertions(+), 140 deletions(-) diff --git a/src/WinRT.Generator.Core/DebugRepro/DebugReproPacker.cs b/src/WinRT.Generator.Core/DebugRepro/DebugReproPacker.cs index db68eaef15..d706c93b87 100644 --- a/src/WinRT.Generator.Core/DebugRepro/DebugReproPacker.cs +++ b/src/WinRT.Generator.Core/DebugRepro/DebugReproPacker.cs @@ -10,6 +10,7 @@ using System.Text; using System.Text.Json; using System.Threading; +using WindowsRuntime.Generator.Errors; using WindowsRuntime.Generator.Helpers; namespace WindowsRuntime.Generator.DebugRepro; @@ -148,4 +149,76 @@ public static Dictionary ExtractPathMap(ZipArchiveEntry pathMapE // Load the mapping with all the original file paths for the included files return JsonSerializer.Deserialize(stream, GeneratorJsonSerializerContext.Default.DictionaryStringString)!; } + + /// + /// Prepares the staging directory and target archive path for a debug repro save operation. + /// + /// The per-tool error factory used to throw if does not exist. + /// The user-provided directory where the resulting .zip archive will be written. Must already exist. + /// The CLI tool name (e.g. "cswinrtimplgen"), used as the prefix of the staging directory. + /// The file name of the resulting .zip archive (e.g. "impl-debug-repro.zip"). + /// A pair containing the freshly-created staging directory and the absolute path of the target archive. + /// Thrown via if does not exist. + public static (string TempDirectory, string ZipPath) BeginSave( + string debugReproDirectory, + string toolName, + string archiveFileName) + where TError : IGeneratorErrorFactory + { + // The target folder must exist + if (!Directory.Exists(debugReproDirectory)) + { + throw TError.DebugReproDirectoryDoesNotExist(debugReproDirectory); + } + + // Path for the ZIP archive + string zipPath = Path.Combine(debugReproDirectory, archiveFileName); + + // Create a temporary directory to stage files for the ZIP + string tempFolderName = $"{toolName}-debug-repro-{Guid.NewGuid().ToString().ToUpperInvariant()}"; + string tempDirectory = Path.Combine(Path.GetTempPath(), tempFolderName); + + _ = Directory.CreateDirectory(tempDirectory); + + return (tempDirectory, zipPath); + } + + /// + /// Finalizes a debug repro save by zipping the staging directory into the target archive and deleting the staging directory. + /// + /// The staging directory previously returned by . + /// The absolute path of the target .zip archive, previously returned by . + /// + /// If a file already exists at , it is deleted before the new archive is created. + /// + public static void FinalizeSave(string tempDirectory, string zipPath) + { + // Delete the previous file, if it exists + if (File.Exists(zipPath)) + { + File.Delete(zipPath); + } + + // Create the actual .zip file in the target directory + ZipFile.CreateFromDirectory(tempDirectory, zipPath); + + // Clean up the temporary directory + Directory.Delete(tempDirectory, recursive: true); + } + + /// + /// Creates a freshly-named temporary directory for unpacking a debug repro .zip archive. + /// + /// The CLI tool name (e.g. "cswinrtimplgen"), used as the prefix of the directory. + /// The absolute path of the created directory. + public static string CreateUnpackTempDirectory(string toolName) + { + // Create a temporary directory to extract the files from the debug repro + string tempFolderName = $"{toolName}-debug-repro-unpack-{Guid.NewGuid().ToString().ToUpperInvariant()}"; + string tempDirectory = Path.Combine(Path.GetTempPath(), tempFolderName); + + _ = Directory.CreateDirectory(tempDirectory); + + return tempDirectory; + } } diff --git a/src/WinRT.Impl.Generator/Generation/ImplGenerator.DebugRepro.cs b/src/WinRT.Impl.Generator/Generation/ImplGenerator.DebugRepro.cs index 7027c9802d..dea139fcf1 100644 --- a/src/WinRT.Impl.Generator/Generation/ImplGenerator.DebugRepro.cs +++ b/src/WinRT.Impl.Generator/Generation/ImplGenerator.DebugRepro.cs @@ -33,11 +33,7 @@ internal static partial class ImplGenerator /// The path to the resulting response file to use. private static string UnpackDebugRepro(string path, CancellationToken token) { - // Create a temporary directory to extract the files from the debug repro - string tempFolderName = $"cswinrtimplgen-debug-repro-unpack-{Guid.NewGuid().ToString().ToUpperInvariant()}"; - string tempDirectory = Path.Combine(Path.GetTempPath(), tempFolderName); - - _ = Directory.CreateDirectory(tempDirectory); + string tempDirectory = DebugReproPacker.CreateUnpackTempDirectory("cswinrtimplgen"); token.ThrowIfCancellationRequested(); @@ -148,21 +144,13 @@ private static void SaveDebugRepro(ImplGeneratorArgs args) return; } - // The target folder must exist - if (!Directory.Exists(args.DebugReproDirectory)) - { - throw WellKnownImplExceptions.DebugReproDirectoryDoesNotExist(args.DebugReproDirectory); - } - - // Path for the ZIP archive - string zipPath = Path.Combine(args.DebugReproDirectory, "impl-debug-repro.zip"); + (string tempDirectory, string zipPath) = DebugReproPacker.BeginSave( + args.DebugReproDirectory, + toolName: "cswinrtimplgen", + archiveFileName: "impl-debug-repro.zip"); - // Create a temporary directory to stage files for the ZIP - string tempFolderName = $"cswinrtimplgen-debug-repro-{Guid.NewGuid().ToString().ToUpperInvariant()}"; - string tempDirectory = Path.Combine(Path.GetTempPath(), tempFolderName); string referenceDirectory = Path.Combine(tempDirectory, "reference"); - _ = Directory.CreateDirectory(tempDirectory); _ = Directory.CreateDirectory(referenceDirectory); // Map with all the original paths @@ -203,16 +191,6 @@ private static void SaveDebugRepro(ImplGeneratorArgs args) args.Token.ThrowIfCancellationRequested(); - // Delete the previous file, if it exists - if (File.Exists(zipPath)) - { - File.Delete(zipPath); - } - - // Create the actual .zip file in the target directory - ZipFile.CreateFromDirectory(tempDirectory, zipPath); - - // Clean up the temporary directory - Directory.Delete(tempDirectory, recursive: true); + DebugReproPacker.FinalizeSave(tempDirectory, zipPath); } } diff --git a/src/WinRT.Interop.Generator/Generation/InteropGenerator.DebugRepro.cs b/src/WinRT.Interop.Generator/Generation/InteropGenerator.DebugRepro.cs index e5309cf44c..f39a75b95b 100644 --- a/src/WinRT.Interop.Generator/Generation/InteropGenerator.DebugRepro.cs +++ b/src/WinRT.Interop.Generator/Generation/InteropGenerator.DebugRepro.cs @@ -38,11 +38,7 @@ internal partial class InteropGenerator /// The path to the resulting response file to use. private static string UnpackDebugRepro(string path, CancellationToken token) { - // Create a temporary directory to extract the files from the debug repro - string tempFolderName = $"cswinrtinteropgen-debug-repro-unpack-{Guid.NewGuid().ToString().ToUpperInvariant()}"; - string tempDirectory = Path.Combine(Path.GetTempPath(), tempFolderName); - - _ = Directory.CreateDirectory(tempDirectory); + string tempDirectory = DebugReproPacker.CreateUnpackTempDirectory("cswinrtinteropgen"); token.ThrowIfCancellationRequested(); @@ -205,22 +201,14 @@ private static void SaveDebugRepro(InteropGeneratorArgs args) return; } - // The target folder must exist - if (!Directory.Exists(args.DebugReproDirectory)) - { - throw WellKnownInteropExceptions.DebugReproDirectoryDoesNotExist(args.DebugReproDirectory); - } - - // Path for the ZIP archive - string zipPath = Path.Combine(args.DebugReproDirectory, "interop-debug-repro.zip"); + (string tempDirectory, string zipPath) = DebugReproPacker.BeginSave( + args.DebugReproDirectory, + toolName: "cswinrtinteropgen", + archiveFileName: "interop-debug-repro.zip"); - // Create a temporary directory to stage files for the ZIP - string tempFolderName = $"cswinrtinteropgen-debug-repro-{Guid.NewGuid().ToString().ToUpperInvariant()}"; - string tempDirectory = Path.Combine(Path.GetTempPath(), tempFolderName); string referenceDirectory = Path.Combine(tempDirectory, "reference"); string implementationDirectory = Path.Combine(tempDirectory, "implementation"); - _ = Directory.CreateDirectory(tempDirectory); _ = Directory.CreateDirectory(referenceDirectory); _ = Directory.CreateDirectory(implementationDirectory); @@ -282,17 +270,7 @@ private static void SaveDebugRepro(InteropGeneratorArgs args) args.Token.ThrowIfCancellationRequested(); - // Delete the previous file, if it exists - if (File.Exists(zipPath)) - { - File.Delete(zipPath); - } - - // Create the actual .zip file in the target directory - ZipFile.CreateFromDirectory(tempDirectory, zipPath); - - // Clean up the temporary directory - Directory.Delete(tempDirectory, recursive: true); + DebugReproPacker.FinalizeSave(tempDirectory, zipPath); } /// diff --git a/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.DebugRepro.cs b/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.DebugRepro.cs index 22d3816e1a..1e784160d3 100644 --- a/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.DebugRepro.cs +++ b/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.DebugRepro.cs @@ -60,11 +60,7 @@ internal static partial class ProjectionGenerator /// The path to the resulting response file to use. private static string UnpackDebugRepro(string path, CancellationToken token) { - // Create a temporary directory to extract the files from the debug repro - string tempFolderName = $"cswinrtprojectiongen-debug-repro-unpack-{Guid.NewGuid().ToString().ToUpperInvariant()}"; - string tempDirectory = Path.Combine(Path.GetTempPath(), tempFolderName); - - _ = Directory.CreateDirectory(tempDirectory); + string tempDirectory = DebugReproPacker.CreateUnpackTempDirectory("cswinrtprojectiongen"); token.ThrowIfCancellationRequested(); @@ -209,23 +205,15 @@ private static void SaveDebugRepro(ProjectionGeneratorArgs args) return; } - // The target folder must exist - if (!Directory.Exists(args.DebugReproDirectory)) - { - throw WellKnownProjectionGeneratorExceptions.DebugReproDirectoryDoesNotExist(args.DebugReproDirectory); - } - - // Path for the ZIP archive - string zipPath = Path.Combine(args.DebugReproDirectory, "projection-debug-repro.zip"); + (string tempDirectory, string zipPath) = DebugReproPacker.BeginSave( + args.DebugReproDirectory, + toolName: "cswinrtprojectiongen", + archiveFileName: "projection-debug-repro.zip"); - // Create a temporary directory to stage files for the ZIP - string tempFolderName = $"cswinrtprojectiongen-debug-repro-{Guid.NewGuid().ToString().ToUpperInvariant()}"; - string tempDirectory = Path.Combine(Path.GetTempPath(), tempFolderName); string referenceDirectory = Path.Combine(tempDirectory, ReferenceSubfolder); string winmdDirectory = Path.Combine(tempDirectory, WinMDSubfolder); string windowsMetadataDirectory = Path.Combine(tempDirectory, WindowsMetadataSubfolder); - _ = Directory.CreateDirectory(tempDirectory); _ = Directory.CreateDirectory(referenceDirectory); _ = Directory.CreateDirectory(winmdDirectory); _ = Directory.CreateDirectory(windowsMetadataDirectory); @@ -301,16 +289,6 @@ private static void SaveDebugRepro(ProjectionGeneratorArgs args) args.Token.ThrowIfCancellationRequested(); - // Delete the previous file, if it exists - if (File.Exists(zipPath)) - { - File.Delete(zipPath); - } - - // Create the actual .zip file in the target directory - ZipFile.CreateFromDirectory(tempDirectory, zipPath); - - // Clean up the temporary directory - Directory.Delete(tempDirectory, recursive: true); + DebugReproPacker.FinalizeSave(tempDirectory, zipPath); } } diff --git a/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGenerator.DebugRepro.cs b/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGenerator.DebugRepro.cs index 1358d044e9..1c08ac14e1 100644 --- a/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGenerator.DebugRepro.cs +++ b/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGenerator.DebugRepro.cs @@ -33,11 +33,7 @@ internal static partial class ReferenceProjectionGenerator /// The path to the resulting response file to use. private static string UnpackDebugRepro(string path, CancellationToken token) { - // Create a temporary directory to extract the files from the debug repro - string tempFolderName = $"cswinrtprojectionrefgen-debug-repro-unpack-{Guid.NewGuid().ToString().ToUpperInvariant()}"; - string tempDirectory = Path.Combine(Path.GetTempPath(), tempFolderName); - - _ = Directory.CreateDirectory(tempDirectory); + string tempDirectory = DebugReproPacker.CreateUnpackTempDirectory("cswinrtprojectionrefgen"); token.ThrowIfCancellationRequested(); @@ -146,21 +142,13 @@ private static void SaveDebugRepro(ReferenceProjectionGeneratorArgs args) return; } - // The target folder must exist - if (!Directory.Exists(args.DebugReproDirectory)) - { - throw WellKnownReferenceProjectionGeneratorExceptions.DebugReproDirectoryDoesNotExist(args.DebugReproDirectory); - } - - // Path for the ZIP archive - string zipPath = Path.Combine(args.DebugReproDirectory, "ref-projection-debug-repro.zip"); + (string tempDirectory, string zipPath) = DebugReproPacker.BeginSave( + args.DebugReproDirectory, + toolName: "cswinrtprojectionrefgen", + archiveFileName: "ref-projection-debug-repro.zip"); - // Create a temporary directory to stage files for the ZIP - string tempFolderName = $"cswinrtprojectionrefgen-debug-repro-{Guid.NewGuid().ToString().ToUpperInvariant()}"; - string tempDirectory = Path.Combine(Path.GetTempPath(), tempFolderName); string inputDirectory = Path.Combine(tempDirectory, "input"); - _ = Directory.CreateDirectory(tempDirectory); _ = Directory.CreateDirectory(inputDirectory); // Expand all input paths (which may be file paths, directories to recursively scan, or @@ -215,16 +203,6 @@ private static void SaveDebugRepro(ReferenceProjectionGeneratorArgs args) args.Token.ThrowIfCancellationRequested(); - // Delete the previous file, if it exists - if (File.Exists(zipPath)) - { - File.Delete(zipPath); - } - - // Create the actual .zip file in the target directory - ZipFile.CreateFromDirectory(tempDirectory, zipPath); - - // Clean up the temporary directory - Directory.Delete(tempDirectory, recursive: true); + DebugReproPacker.FinalizeSave(tempDirectory, zipPath); } } diff --git a/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.DebugRepro.cs b/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.DebugRepro.cs index 49834c4124..4f2ea5783e 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.DebugRepro.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.DebugRepro.cs @@ -32,11 +32,7 @@ internal static partial class WinMDGenerator /// The path to the resulting response file to use. private static string UnpackDebugRepro(string path, CancellationToken token) { - // Create a temporary directory to extract the files from the debug repro - string tempFolderName = $"cswinrtwinmdgen-debug-repro-unpack-{Guid.NewGuid().ToString().ToUpperInvariant()}"; - string tempDirectory = Path.Combine(Path.GetTempPath(), tempFolderName); - - _ = Directory.CreateDirectory(tempDirectory); + string tempDirectory = DebugReproPacker.CreateUnpackTempDirectory("cswinrtwinmdgen"); token.ThrowIfCancellationRequested(); @@ -159,21 +155,13 @@ private static void SaveDebugRepro(WinMDGeneratorArgs args) return; } - // The target folder must exist - if (!Directory.Exists(args.DebugReproDirectory)) - { - throw WellKnownWinMDExceptions.DebugReproDirectoryDoesNotExist(args.DebugReproDirectory); - } - - // Path for the ZIP archive - string zipPath = Path.Combine(args.DebugReproDirectory, "winmd-debug-repro.zip"); + (string tempDirectory, string zipPath) = DebugReproPacker.BeginSave( + args.DebugReproDirectory, + toolName: "cswinrtwinmdgen", + archiveFileName: "winmd-debug-repro.zip"); - // Create a temporary directory to stage files for the ZIP - string tempFolderName = $"cswinrtwinmdgen-debug-repro-{Guid.NewGuid().ToString().ToUpperInvariant()}"; - string tempDirectory = Path.Combine(Path.GetTempPath(), tempFolderName); string referenceDirectory = Path.Combine(tempDirectory, "reference"); - _ = Directory.CreateDirectory(tempDirectory); _ = Directory.CreateDirectory(referenceDirectory); // Map with all the original paths @@ -214,16 +202,6 @@ private static void SaveDebugRepro(WinMDGeneratorArgs args) args.Token.ThrowIfCancellationRequested(); - // Delete the previous file, if it exists - if (File.Exists(zipPath)) - { - File.Delete(zipPath); - } - - // Create the actual .zip file in the target directory - ZipFile.CreateFromDirectory(tempDirectory, zipPath); - - // Clean up the temporary directory - Directory.Delete(tempDirectory, recursive: true); + DebugReproPacker.FinalizeSave(tempDirectory, zipPath); } } From df72d73e3c5855ace408709cee43f110dfeee211 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 9 Jun 2026 12:24:07 -0700 Subject: [PATCH 03/15] Move RuntimeContextExtensions.LoadModule to Core 'Impl', 'Interop', and 'WinMD' each shipped their own copy of 'RuntimeContextExtensions.LoadModule', with bodies that are character-for-character identical (only the receiver parameter name differed). Impl had only the 'PEImage' overload, Interop had only the 'string' overload, WinMD had both. Consolidate to a single file under 'WinRT.Generator.Core' that carries both overloads. All three consumer projects pick up the overload they already used; no behavioral change. This adds an 'AsmResolver.DotNet' package reference to 'WinRT.Generator.Core' (all 5 generators already reference AsmResolver directly). Net diff: 6 files, +63 / -126 LOC (-63 LOC). All 5 generators build with 0 warnings and pass end-to-end debug-repro validation (ref-gen: 8/8 0 diffs, WinMD: 16-byte MVID-only delta, proj-gen: 327/327 0 diffs, impl-gen: 0 byte diffs, interop-gen: smoke OK). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Extensions/RuntimeContextExtensions.cs | 2 +- .../WinRT.Generator.Core.csproj | 4 ++ .../Extensions/RuntimeContextExtensions.cs | 40 ------------------- .../Generation/ImplGenerator.cs | 1 + .../Extensions/RuntimeContextExtensions.cs | 39 ------------------ .../Generation/InteropGenerator.Discover.cs | 1 + .../Generation/WinMDGenerator.Discover.cs | 1 + 7 files changed, 8 insertions(+), 80 deletions(-) rename src/{WinRT.WinMD.Generator => WinRT.Generator.Core}/Extensions/RuntimeContextExtensions.cs (98%) delete mode 100644 src/WinRT.Impl.Generator/Extensions/RuntimeContextExtensions.cs delete mode 100644 src/WinRT.Interop.Generator/Extensions/RuntimeContextExtensions.cs diff --git a/src/WinRT.WinMD.Generator/Extensions/RuntimeContextExtensions.cs b/src/WinRT.Generator.Core/Extensions/RuntimeContextExtensions.cs similarity index 98% rename from src/WinRT.WinMD.Generator/Extensions/RuntimeContextExtensions.cs rename to src/WinRT.Generator.Core/Extensions/RuntimeContextExtensions.cs index 8794f0c543..7123d56211 100644 --- a/src/WinRT.WinMD.Generator/Extensions/RuntimeContextExtensions.cs +++ b/src/WinRT.Generator.Core/Extensions/RuntimeContextExtensions.cs @@ -7,7 +7,7 @@ #pragma warning disable IDE0046 -namespace WindowsRuntime.WinMDGenerator; +namespace WindowsRuntime.Generator.Extensions; /// /// Extensions for the type. diff --git a/src/WinRT.Generator.Core/WinRT.Generator.Core.csproj b/src/WinRT.Generator.Core/WinRT.Generator.Core.csproj index 55ac42ef9a..e2c8a96a3f 100644 --- a/src/WinRT.Generator.Core/WinRT.Generator.Core.csproj +++ b/src/WinRT.Generator.Core/WinRT.Generator.Core.csproj @@ -29,4 +29,8 @@ strict true + + + + diff --git a/src/WinRT.Impl.Generator/Extensions/RuntimeContextExtensions.cs b/src/WinRT.Impl.Generator/Extensions/RuntimeContextExtensions.cs deleted file mode 100644 index 55b50ba06f..0000000000 --- a/src/WinRT.Impl.Generator/Extensions/RuntimeContextExtensions.cs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using AsmResolver.DotNet; -using AsmResolver.PE; - -#pragma warning disable IDE0046 - -namespace WindowsRuntime.ImplGenerator; - -/// -/// Extensions for the type. -/// -internal static class RuntimeContextExtensions -{ - extension(RuntimeContext signature) - { - /// - /// Loads a .NET module into the context from the provided input image. - /// - /// The image containing the .NET metadata. - /// The module. - /// Occurs when the image does not contain a valid .NET metadata directory. - public ModuleDefinition LoadModule(PEImage peImage) - { - AssemblyDefinition assemblyDefinition = signature.LoadAssembly(peImage); - - // 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; - } - } -} \ No newline at end of file diff --git a/src/WinRT.Impl.Generator/Generation/ImplGenerator.cs b/src/WinRT.Impl.Generator/Generation/ImplGenerator.cs index a6e2e389a7..da8c3be7a9 100644 --- a/src/WinRT.Impl.Generator/Generation/ImplGenerator.cs +++ b/src/WinRT.Impl.Generator/Generation/ImplGenerator.cs @@ -19,6 +19,7 @@ using ConsoleAppFramework; using WindowsRuntime.Generator; using WindowsRuntime.Generator.Errors; +using WindowsRuntime.Generator.Extensions; using WindowsRuntime.Generator.Parsing; using WindowsRuntime.ImplGenerator.Errors; using WindowsRuntime.ImplGenerator.References; diff --git a/src/WinRT.Interop.Generator/Extensions/RuntimeContextExtensions.cs b/src/WinRT.Interop.Generator/Extensions/RuntimeContextExtensions.cs deleted file mode 100644 index 05b9c3833f..0000000000 --- a/src/WinRT.Interop.Generator/Extensions/RuntimeContextExtensions.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using AsmResolver.DotNet; - -#pragma warning disable IDE0046 - -namespace WindowsRuntime.InteropGenerator; - -/// -/// 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; - } - } -} \ No newline at end of file diff --git a/src/WinRT.Interop.Generator/Generation/InteropGenerator.Discover.cs b/src/WinRT.Interop.Generator/Generation/InteropGenerator.Discover.cs index 1608a1ed40..c2444f365a 100644 --- a/src/WinRT.Interop.Generator/Generation/InteropGenerator.Discover.cs +++ b/src/WinRT.Interop.Generator/Generation/InteropGenerator.Discover.cs @@ -11,6 +11,7 @@ using AsmResolver.DotNet.Signatures; using AsmResolver.PE; using WindowsRuntime.Generator.Errors; +using WindowsRuntime.Generator.Extensions; using WindowsRuntime.InteropGenerator.Discovery; using WindowsRuntime.InteropGenerator.Errors; using WindowsRuntime.InteropGenerator.Models; diff --git a/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.Discover.cs b/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.Discover.cs index ea927ce18b..86ab36b8ec 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.Discover.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.Discover.cs @@ -6,6 +6,7 @@ using System.Linq; using AsmResolver.DotNet; using AsmResolver.PE; +using WindowsRuntime.Generator.Extensions; using WindowsRuntime.WinMDGenerator.Discovery; using WindowsRuntime.WinMDGenerator.Errors; From 8540da228f5c2afbaa0ed4e67a693d7cd69c8a14 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 9 Jun 2026 12:35:54 -0700 Subject: [PATCH 04/15] Add GeneratorPhaseRunner to wrap per-phase try/catch + log Every generator's 'Run' method historically opens with a single 'GeneratorHost.Prepare' call (which handles the shared unpack/parse/save preamble) and then proceeds through a series of phases (discovery, processing, emit, ...). Each phase was wrapped in an identical: try { ConsoleApp.Log("..."); DoPhase(args); } catch (Exception e) when (!e.IsWellKnown) { throw new UnhandledXxxException("phase-name", e); } That is now expressed as a single 'runner.RunPhase' call. The 'GeneratorPhaseRunner' readonly struct captures the per-tool 'wrapUnhandled' + 'log' delegates once (it is returned bound to them by 'GeneratorHost.Prepare') and offers four overloads ('Action' / 'Func', each with or without a log message). All four invariably route through the per-tool 'wrapUnhandled' delegate so each tool keeps throwing its own 'UnhandledXxxException' with the right phase name. 'GeneratorHost.Prepare' now returns '(TArgs Args, GeneratorPhaseRunner Runner)' instead of just 'TArgs'. The 5 'Run' methods deconstruct the tuple and use the runner for each phase. Impl's 'LoadOutputModule(args, out runtimeContext, out outputModule)' becomes a tuple-return 'LoadOutputModule(args) -> (RuntimeContext, ModuleDefinition)' so it fits the 'Func' overload cleanly. 'ThrowIfCancellationRequested' calls stay per-tool at the call sites (the pattern isn't uniform: some phases use 'args.Token', WinMD uses 'token', and the last phase typically has no check). ReferenceProjection's second phase wraps to a well-known 'CsWinRTProcessError' rather than the 'Unhandled' factory, so it's left as an inline try/catch. Net diff across modified files: -84 LOC. Including the new 'GeneratorPhaseRunner.cs' (~123 LOC, mostly XML docs), the overall file diff is +39 LOC but with a much lower density of error-handling boilerplate at the per-tool call sites. All 5 generators build with 0 warnings and pass end-to-end debug-repro validation (ref-gen: 8/8 0 diffs, WinMD: 16-byte MVID-only delta, proj-gen: 327/327 0 diffs, impl-gen: 0 byte diffs, interop-gen: smoke OK). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/WinRT.Generator.Core/GeneratorHost.cs | 18 ++- .../GeneratorPhaseRunner.cs | 126 ++++++++++++++++++ .../Generation/ImplGenerator.cs | 71 +++------- .../Generation/InteropGenerator.cs | 41 ++---- .../Generation/ProjectionGenerator.cs | 57 +++----- .../ReferenceProjectionGenerator.cs | 19 +-- .../Generation/WinMDGenerator.cs | 34 ++--- 7 files changed, 204 insertions(+), 162 deletions(-) create mode 100644 src/WinRT.Generator.Core/GeneratorPhaseRunner.cs diff --git a/src/WinRT.Generator.Core/GeneratorHost.cs b/src/WinRT.Generator.Core/GeneratorHost.cs index ba60c250f7..94b1d09b13 100644 --- a/src/WinRT.Generator.Core/GeneratorHost.cs +++ b/src/WinRT.Generator.Core/GeneratorHost.cs @@ -21,6 +21,14 @@ namespace WindowsRuntime.Generator; /// encapsulates that preamble. Each generator's Run now starts with a /// single call to it; the per-tool unpack / save / parse logic is supplied via delegates so behavior /// stays identical (same log messages, same exception phases, same per-tool unhandled exception type). +/// +/// The same Run methods then proceed through a series of phases (loading, processing, emit, ...), +/// each wrapped in an identical try/catch that re-throws as the per-tool +/// Unhandled*Exception. additionally returns a +/// bound to the same per-tool wrapUnhandled and log +/// delegates so each phase can be expressed as a single +/// call instead of a hand-written try/catch. +/// /// internal static class GeneratorHost { @@ -36,8 +44,11 @@ internal static class GeneratorHost /// Wraps an unexpected exception into the per-tool Unhandled*Exception with the given phase name. /// Logs a progress message to the user (typically ConsoleApp.Log from ConsoleAppFramework). /// The token for the operation. - /// The parsed instance. - public static TArgs Prepare( + /// + /// A pair containing the parsed instance and a + /// pre-bound to and for use by subsequent phases. + /// + public static (TArgs Args, GeneratorPhaseRunner Runner) Prepare( string inputFilePath, string toolName, Func unpackDebugRepro, @@ -109,7 +120,8 @@ public static TArgs Prepare( args.Token.ThrowIfCancellationRequested(); - return args; + return (args, new GeneratorPhaseRunner(wrapUnhandled, log)); } } + diff --git a/src/WinRT.Generator.Core/GeneratorPhaseRunner.cs b/src/WinRT.Generator.Core/GeneratorPhaseRunner.cs new file mode 100644 index 0000000000..8e71fb5e3d --- /dev/null +++ b/src/WinRT.Generator.Core/GeneratorPhaseRunner.cs @@ -0,0 +1,126 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using WindowsRuntime.Generator.Errors; + +namespace WindowsRuntime.Generator; + +/// +/// Runs the body of a single generator phase, wrapping unexpected exceptions in the per-tool +/// Unhandled*Exception and optionally logging a progress message before the body runs. +/// +/// +/// Each generator's Run method historically wrapped every phase (loading, processing, emit, ...) +/// in an identical try/catch that re-threw as the per-tool Unhandled*Exception +/// (with the phase name as the constructor argument). captures +/// the per-tool wrapUnhandled and log delegates once (it is returned bound to them by +/// ) and lets each phase call site collapse to a single +/// (or overload) invocation. Per-tool exception identity +/// is fully preserved because the original wrapUnhandled delegate is invoked unchanged. +/// +internal readonly struct GeneratorPhaseRunner +{ + /// + /// The per-tool wrapUnhandled delegate. + /// + private readonly Func _wrapUnhandled; + + /// + /// The per-tool progress logger. + /// + private readonly Action _log; + + /// + /// Creates a new bound to the given per-tool delegates. + /// + /// Wraps an unexpected exception into the per-tool Unhandled*Exception with the given phase name. + /// Logs a progress message to the user (typically ConsoleApp.Log from ConsoleAppFramework). + internal GeneratorPhaseRunner(Func wrapUnhandled, Action log) + { + _wrapUnhandled = wrapUnhandled; + _log = log; + } + + /// + /// Runs , wrapping any unexpected exception in the per-tool + /// Unhandled*Exception with as the phase tag. + /// + /// The phase name used by the per-tool Unhandled*Exception. + /// The body of the phase to run. + public void RunPhase(string phaseName, Action body) + { + try + { + body(); + } + catch (Exception e) when (!e.IsWellKnown) + { + throw _wrapUnhandled(phaseName, e); + } + } + + /// + /// Logs and then runs , wrapping any + /// unexpected exception in the per-tool Unhandled*Exception with + /// as the phase tag. + /// + /// The phase name used by the per-tool Unhandled*Exception. + /// The progress message to log before the body runs. + /// The body of the phase to run. + public void RunPhase(string phaseName, string logMessage, Action body) + { + try + { + _log(logMessage); + body(); + } + catch (Exception e) when (!e.IsWellKnown) + { + throw _wrapUnhandled(phaseName, e); + } + } + + /// + /// Runs and returns its result, wrapping any unexpected exception + /// in the per-tool Unhandled*Exception with as the phase tag. + /// + /// The result type of . + /// The phase name used by the per-tool Unhandled*Exception. + /// The body of the phase to run. + /// The value returned by . + public T RunPhase(string phaseName, Func body) + { + try + { + return body(); + } + catch (Exception e) when (!e.IsWellKnown) + { + throw _wrapUnhandled(phaseName, e); + } + } + + /// + /// Logs , then runs and returns its result, + /// wrapping any unexpected exception in the per-tool Unhandled*Exception with + /// as the phase tag. + /// + /// The result type of . + /// The phase name used by the per-tool Unhandled*Exception. + /// The progress message to log before the body runs. + /// The body of the phase to run. + /// The value returned by . + public T RunPhase(string phaseName, string logMessage, Func body) + { + try + { + _log(logMessage); + return body(); + } + catch (Exception e) when (!e.IsWellKnown) + { + throw _wrapUnhandled(phaseName, e); + } + } +} diff --git a/src/WinRT.Impl.Generator/Generation/ImplGenerator.cs b/src/WinRT.Impl.Generator/Generation/ImplGenerator.cs index da8c3be7a9..1a6149aaee 100644 --- a/src/WinRT.Impl.Generator/Generation/ImplGenerator.cs +++ b/src/WinRT.Impl.Generator/Generation/ImplGenerator.cs @@ -69,7 +69,7 @@ internal static partial class ImplGenerator /// The token for the operation. public static void Run([Argument] string inputFilePath, CancellationToken token) { - ImplGeneratorArgs args = GeneratorHost.Prepare( + (ImplGeneratorArgs args, GeneratorPhaseRunner runner) = GeneratorHost.Prepare( inputFilePath: inputFilePath, toolName: "cswinrtimplgen", unpackDebugRepro: UnpackDebugRepro, @@ -79,67 +79,38 @@ public static void Run([Argument] string inputFilePath, CancellationToken token) log: ConsoleApp.Log, token: token); - RuntimeContext runtimeContext; - ModuleDefinition outputModule; - // Initialize the assembly resolver and load the output module - try - { - LoadOutputModule(args, out runtimeContext, out outputModule); - } - catch (Exception e) when (!e.IsWellKnown) - { - throw new UnhandledImplException("loading", e); - } + (RuntimeContext runtimeContext, ModuleDefinition outputModule) = runner.RunPhase( + phaseName: "loading", + body: () => LoadOutputModule(args)); args.Token.ThrowIfCancellationRequested(); - ModuleDefinition implModule; - // Define the impl module to emit - try - { - implModule = DefineImplModule(runtimeContext, outputModule); - } - catch (Exception e) when (!e.IsWellKnown) - { - throw new UnhandledImplException("loading", e); - } + ModuleDefinition implModule = runner.RunPhase( + phaseName: "loading", + body: () => DefineImplModule(runtimeContext, outputModule)); args.Token.ThrowIfCancellationRequested(); // Emit all necessary IL code in the impl module - try + runner.RunPhase(phaseName: "generation", body: () => { EmitAssemblyAttributes(outputModule, implModule); EmitTypeForwards(outputModule, implModule); - } - catch (Exception e) when (!e.IsWellKnown) - { - throw new UnhandledImplException("generation", e); - } + }); args.Token.ThrowIfCancellationRequested(); // Write the module to disk with all the generated contents - try - { - WriteImplModuleToDisk(args, outputModule, implModule); - } - catch (Exception e) when (!e.IsWellKnown) - { - throw new UnhandledImplException("emit", e); - } + runner.RunPhase( + phaseName: "emit", + body: () => WriteImplModuleToDisk(args, outputModule, implModule)); // Signs the module on disk, if needed - try - { - SignImplModuleOnDisk(args, outputModule); - } - catch (Exception e) when (!e.IsWellKnown) - { - throw new UnhandledImplException("sign", e); - } + runner.RunPhase( + phaseName: "sign", + body: () => SignImplModuleOnDisk(args, outputModule)); // Notify the user that generation was successful ConsoleApp.Log($"Impl code generated -> {Path.Combine(args.GeneratedAssemblyDirectory, implModule.Name!)}"); @@ -149,12 +120,8 @@ public static void Run([Argument] string inputFilePath, CancellationToken token) /// Loads the output assembly being produced. /// /// The arguments for this invocation. - /// The instance in use. - /// The loaded for the output assembly. - private static void LoadOutputModule( - ImplGeneratorArgs args, - out RuntimeContext runtimeContext, - out ModuleDefinition outputModule) + /// The instance in use and the loaded for the output assembly. + private static (RuntimeContext RuntimeContext, ModuleDefinition OutputModule) LoadOutputModule(ImplGeneratorArgs args) { PEImage outputAssemblyImage; @@ -179,12 +146,12 @@ private static void LoadOutputModule( PathAssemblyResolver assemblyResolver = new(args.ReferenceAssemblyPaths); // Initialize the runtime context (this will be reused to allow caching) - runtimeContext = new RuntimeContext(targetRuntime, assemblyResolver); + RuntimeContext runtimeContext = new(targetRuntime, assemblyResolver); // Try to load the .dll at the current path try { - outputModule = runtimeContext.LoadModule(outputAssemblyImage); + return (runtimeContext, runtimeContext.LoadModule(outputAssemblyImage)); } catch (Exception e) { diff --git a/src/WinRT.Interop.Generator/Generation/InteropGenerator.cs b/src/WinRT.Interop.Generator/Generation/InteropGenerator.cs index 44693e753d..8e3631caf5 100644 --- a/src/WinRT.Interop.Generator/Generation/InteropGenerator.cs +++ b/src/WinRT.Interop.Generator/Generation/InteropGenerator.cs @@ -1,12 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; using System.IO; using System.Threading; using ConsoleAppFramework; using WindowsRuntime.Generator; -using WindowsRuntime.Generator.Errors; using WindowsRuntime.Generator.Parsing; using WindowsRuntime.InteropGenerator.Errors; using WindowsRuntime.InteropGenerator.References; @@ -25,7 +23,7 @@ internal static partial class InteropGenerator /// The token for the operation. public static void Run([Argument] string inputFilePath, CancellationToken token) { - InteropGeneratorArgs args = GeneratorHost.Prepare( + (InteropGeneratorArgs args, GeneratorPhaseRunner runner) = GeneratorHost.Prepare( inputFilePath: inputFilePath, toolName: "cswinrtinteropgen", unpackDebugRepro: UnpackDebugRepro, @@ -35,36 +33,19 @@ public static void Run([Argument] string inputFilePath, CancellationToken token) log: ConsoleApp.Log, token: token); - InteropGeneratorDiscoveryState discoveryState; - - // Wrap the actual logic, to ensure that we're only ever throwing an exception that will result - // in either graceful cancellation, or a well formatted error message. The 'ConsoleApp' code is - // taking care of passing the exception 'ToString()' result to the output buffer, so we want all - // exceptions that can reach that path to have our custom formatting implementation there. - try - { - ConsoleApp.Log($"Processing {args.ReferenceAssemblyPaths.Length + args.ImplementationAssemblyPaths.Length + 1} module(s)"); - - discoveryState = Discover(args); - } - catch (Exception e) when (!e.IsWellKnown) - { - throw new UnhandledInteropException("discovery", e); - } + // Discover the types to process + InteropGeneratorDiscoveryState discoveryState = runner.RunPhase( + phaseName: "discovery", + logMessage: $"Processing {args.ReferenceAssemblyPaths.Length + args.ImplementationAssemblyPaths.Length + 1} module(s)", + body: () => Discover(args)); args.Token.ThrowIfCancellationRequested(); - // Same thing for the emit phase - try - { - ConsoleApp.Log("Generating interop code"); - - Emit(args, discoveryState); - } - catch (Exception e) when (!e.IsWellKnown) - { - throw new UnhandledInteropException("emit", e); - } + // Emit the resulting interop assembly + runner.RunPhase( + phaseName: "emit", + logMessage: "Generating interop code", + body: () => Emit(args, discoveryState)); // Notify the user that generation was successful ConsoleApp.Log($"Interop code generated -> {Path.Combine(args.GeneratedAssemblyDirectory, InteropNames.WindowsRuntimeInteropDllName)}"); diff --git a/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.cs b/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.cs index 4844f07463..658d491144 100644 --- a/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.cs +++ b/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.cs @@ -1,12 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; using System.IO; using System.Threading; using ConsoleAppFramework; using WindowsRuntime.Generator; -using WindowsRuntime.Generator.Errors; using WindowsRuntime.Generator.Parsing; using WindowsRuntime.ProjectionGenerator.Errors; @@ -24,7 +22,7 @@ internal static partial class ProjectionGenerator /// The token for the operation. public static void Run([Argument] string inputFilePath, CancellationToken token) { - ProjectionGeneratorArgs args = GeneratorHost.Prepare( + (ProjectionGeneratorArgs args, GeneratorPhaseRunner runner) = GeneratorHost.Prepare( inputFilePath: inputFilePath, toolName: "cswinrtprojectiongen", unpackDebugRepro: UnpackDebugRepro, @@ -34,27 +32,18 @@ public static void Run([Argument] string inputFilePath, CancellationToken token) log: ConsoleApp.Log, token: token); - ProjectionGeneratorProcessingState processingState; - - // Process all .winmd references - try - { - // Show the appropriate message to inform users of what this generator is doing, - // based on the input arguments. If we don't have precompiled projections, this - // tool might run up to 3 times during builds, so this helps make things clearer. - ConsoleApp.Log(args switch + // Process all .winmd references. Show the appropriate message to inform users of what this + // generator is doing, based on the input arguments. If we don't have precompiled projections, + // this tool might run up to 3 times during builds, so this helps make things clearer. + ProjectionGeneratorProcessingState processingState = runner.RunPhase( + phaseName: "processing", + logMessage: args switch { { WindowsSdkOnly: true, WindowsUIXamlProjection: false } => "Processing Windows SDK .winmd references", { WindowsSdkOnly: true, WindowsUIXamlProjection: true } => "Processing 'Windows.UI.Xaml' .winmd references", _ => $"Processing {args.WinMDPaths.Length} .winmd reference(s)" - }); - - processingState = ProcessReferences(args); - } - catch (Exception e) when (!e.IsWellKnown) - { - throw new UnhandledProjectionGeneratorException("processing", e); - } + }, + body: () => ProcessReferences(args)); args.Token.ThrowIfCancellationRequested(); @@ -66,30 +55,18 @@ public static void Run([Argument] string inputFilePath, CancellationToken token) } // Invoke the projection writer (in-process) to generate the projection sources - try - { - ConsoleApp.Log("Generating projection code"); - - GenerateSources(processingState); - } - catch (Exception e) when (!e.IsWellKnown) - { - throw new UnhandledProjectionGeneratorException("source-generation", e); - } + runner.RunPhase( + phaseName: "source-generation", + logMessage: "Generating projection code", + body: () => GenerateSources(processingState)); args.Token.ThrowIfCancellationRequested(); // Invoke Roslyn to compile the generated sources into 'WinRT.Projection.dll' - try - { - ConsoleApp.Log("Compiling projection code"); - - Emit(args, processingState); - } - catch (Exception e) when (!e.IsWellKnown) - { - throw new UnhandledProjectionGeneratorException("emit", e); - } + runner.RunPhase( + phaseName: "emit", + logMessage: "Compiling projection code", + body: () => Emit(args, processingState)); // Notify the user that generation was successful ConsoleApp.Log($"Projection code generated -> {Path.Combine(args.GeneratedAssemblyDirectory, args.AssemblyName)}.dll"); diff --git a/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGenerator.cs b/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGenerator.cs index 990f39a1ef..505f2d4abe 100644 --- a/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGenerator.cs +++ b/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGenerator.cs @@ -29,7 +29,7 @@ internal static partial class ReferenceProjectionGenerator /// The token for the operation. public static void Run([Argument] string inputFilePath, CancellationToken token) { - ReferenceProjectionGeneratorArgs args = GeneratorHost.Prepare( + (ReferenceProjectionGeneratorArgs args, GeneratorPhaseRunner runner) = GeneratorHost.Prepare( inputFilePath: inputFilePath, toolName: "cswinrtprojectionrefgen", unpackDebugRepro: UnpackDebugRepro, @@ -46,20 +46,15 @@ public static void Run([Argument] string inputFilePath, CancellationToken token) } // Build the writer options from the parsed arguments - ProjectionWriterOptions options; - - try - { - options = BuildWriterOptions(args); - } - catch (Exception e) when (!e.IsWellKnown) - { - throw new UnhandledReferenceProjectionGeneratorException("processing", e); - } + ProjectionWriterOptions options = runner.RunPhase( + phaseName: "processing", + body: () => BuildWriterOptions(args)); args.Token.ThrowIfCancellationRequested(); - // Invoke the projection writer (in-process) to generate the projection sources + // Invoke the projection writer (in-process) to generate the projection sources. We can't + // route this through the shared 'runner.RunPhase' helper because we wrap the exception + // into a well-known 'CsWinRTProcessError' rather than the per-tool 'Unhandled' factory. try { ConsoleApp.Log($"Generating reference projection sources -> {options.OutputFolder}"); diff --git a/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.cs b/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.cs index 8b850ce782..293858a395 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.cs @@ -1,11 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; using System.Threading; using ConsoleAppFramework; using WindowsRuntime.Generator; -using WindowsRuntime.Generator.Errors; using WindowsRuntime.Generator.Parsing; using WindowsRuntime.WinMDGenerator.Errors; @@ -39,7 +37,7 @@ internal static partial class WinMDGenerator /// The token for the operation. public static void Run([Argument] string inputFilePath, CancellationToken token) { - WinMDGeneratorArgs args = GeneratorHost.Prepare( + (WinMDGeneratorArgs args, GeneratorPhaseRunner runner) = GeneratorHost.Prepare( inputFilePath: inputFilePath, toolName: "cswinrtwinmdgen", unpackDebugRepro: UnpackDebugRepro, @@ -50,32 +48,18 @@ public static void Run([Argument] string inputFilePath, CancellationToken token) token: token); // Discover the types to process - WinMDGeneratorDiscoveryState discoveryState; - - try - { - ConsoleApp.Log($"Processing assembly: '{System.IO.Path.GetFileName(args.InputAssemblyPath)}'"); - - discoveryState = Discover(args); - } - catch (Exception e) when (!e.IsWellKnown) - { - throw new UnhandledWinMDException("discovery", e); - } + WinMDGeneratorDiscoveryState discoveryState = runner.RunPhase( + phaseName: "discovery", + logMessage: $"Processing assembly: '{System.IO.Path.GetFileName(args.InputAssemblyPath)}'", + body: () => Discover(args)); token.ThrowIfCancellationRequested(); // Generate and write the .winmd file - try - { - ConsoleApp.Log($"Defining {discoveryState.PublicTypes.Count} authored type(s)"); - - Generate(args, discoveryState); - } - catch (Exception e) when (!e.IsWellKnown) - { - throw new UnhandledWinMDException("generation", e); - } + runner.RunPhase( + phaseName: "generation", + logMessage: $"Defining {discoveryState.PublicTypes.Count} authored type(s)", + body: () => Generate(args, discoveryState)); ConsoleApp.Log($"Windows Runtime assembly (.winmd) generated -> {args.OutputWinmdPath}"); } From d7864f11aa4ec702feb31b2d351d54ac096ed979 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 9 Jun 2026 12:44:45 -0700 Subject: [PATCH 05/15] Centralize IGeneratorErrorFactory message strings in WellKnownGeneratorMessages Each per-tool 'WellKnown*Exceptions' factory implements the same 6 logical errors from 'IGeneratorErrorFactory' with byte-identical message text (5 of 6 strings are character-for-character identical across all 5 tools; the sixth, 'ResponseFileReadError', only varies by the embedded tool name). Move the message templates to a new shared 'WellKnownGeneratorMessages' class in 'WinRT.Generator.Core' and have each per-tool factory call through to it. Per-tool error ID prefixes (e.g. 'CSWINRTIMPLGEN0001' vs 'CSWINRTINTEROPGEN0028'), per-tool concrete exception types ('WellKnownImplException' / 'WellKnownInteropException' / ...) and per-tool numeric IDs are all unchanged. While here, replace the per-tool '' doc on each of these 6 methods with ''. The per-tool summaries were byte-identical to the interface summaries, so this is no info loss; it also avoids future drift. Net diff: 6 files (5 modified + 1 new), -60 LOC across the per-tool factories and +70 LOC for the new central messages file. The real win is the divergence-protection: any future message tweak now lives in one place. All 5 generators build clean and pass end-to-end debug-repro validation (ref-gen: 8/8 0 diffs, WinMD: 16-byte MVID-only delta, proj-gen: 327/327 0 diffs, impl-gen: 0 byte diffs, interop-gen: smoke OK). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Errors/WellKnownGeneratorMessages.cs | 71 +++++++++++++++++++ .../Errors/WellKnownImplExceptions.cs | 36 ++++------ .../Errors/WellKnownInteropExceptions.cs | 36 ++++------ .../WellKnownProjectionGeneratorExceptions.cs | 36 ++++------ ...nReferenceProjectionGeneratorExceptions.cs | 36 ++++------ .../Errors/WellKnownWinMDExceptions.cs | 36 ++++------ 6 files changed, 131 insertions(+), 120 deletions(-) create mode 100644 src/WinRT.Generator.Core/Errors/WellKnownGeneratorMessages.cs diff --git a/src/WinRT.Generator.Core/Errors/WellKnownGeneratorMessages.cs b/src/WinRT.Generator.Core/Errors/WellKnownGeneratorMessages.cs new file mode 100644 index 0000000000..8404f0a29b --- /dev/null +++ b/src/WinRT.Generator.Core/Errors/WellKnownGeneratorMessages.cs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace WindowsRuntime.Generator.Errors; + +/// +/// Shared message templates for the well-known logical errors defined by . +/// +/// +/// Each per-tool WellKnown*Exceptions factory uses these helpers to format its own +/// instance of every method, ensuring that the message +/// text stays identical across all generators while the per-tool error ID prefix (e.g. +/// CSWINRTIMPLGEN) and concrete exception type are still chosen per-tool. +/// +internal static class WellKnownGeneratorMessages +{ + /// + /// Builds the message for . + /// + /// The CLI tool name embedded in the message (e.g. "cswinrtimplgen"). + /// The formatted error message. + public static string ResponseFileReadError(string toolName) + { + return $"Failed to read the response file to run '{toolName}'."; + } + + /// + /// Builds the message for . + /// + /// The name of the response-file argument that failed to parse. + /// The formatted error message. + public static string ResponseFileArgumentParsingError(string argumentName) + { + return $"Failed to parse argument '{argumentName}' from response file."; + } + + /// + /// The message for . + /// + public const string MalformedResponseFile = "The response file is malformed and contains invalid content."; + + /// + /// Builds the message for . + /// + /// The directory path that does not exist. + /// The formatted error message. + public static string DebugReproDirectoryDoesNotExist(string path) + { + return $"The debug repro directory '{path}' does not exist."; + } + + /// + /// Builds the message for . + /// + /// The debug-repro file entry path that has no mapping. + /// The formatted error message. + public static string DebugReproMissingFileEntryMapping(string path) + { + return $"The debug repro file entry with path '{path}' is missing its assembly path mapping."; + } + + /// + /// Builds the message for . + /// + /// The debug-repro file entry path that was not recognized. + /// The formatted error message. + public static string DebugReproUnrecognizedFileEntry(string path) + { + return $"The debug repro file entry with path '{path}' was not recognized."; + } +} diff --git a/src/WinRT.Impl.Generator/Errors/WellKnownImplExceptions.cs b/src/WinRT.Impl.Generator/Errors/WellKnownImplExceptions.cs index 960ee08993..ef3a92458c 100644 --- a/src/WinRT.Impl.Generator/Errors/WellKnownImplExceptions.cs +++ b/src/WinRT.Impl.Generator/Errors/WellKnownImplExceptions.cs @@ -23,28 +23,22 @@ private WellKnownImplExceptions() { } - /// - /// Some exception was thrown when trying to read the response file. - /// + /// public static Exception ResponseFileReadError(Exception exception) { - return Exception(1, "Failed to read the response file to run 'cswinrtimplgen'.", exception); + return Exception(1, WellKnownGeneratorMessages.ResponseFileReadError("cswinrtimplgen"), exception); } - /// - /// Failed to parse an argument from the response file. - /// + /// public static Exception ResponseFileArgumentParsingError(string argumentName, Exception? exception = null) { - return Exception(2, $"Failed to parse argument '{argumentName}' from response file.", exception); + return Exception(2, WellKnownGeneratorMessages.ResponseFileArgumentParsingError(argumentName), exception); } - /// - /// The input response file is malformed. - /// + /// public static Exception MalformedResponseFile() { - return Exception(3, "The response file is malformed and contains invalid content."); + return Exception(3, WellKnownGeneratorMessages.MalformedResponseFile); } /// @@ -103,28 +97,22 @@ public static Exception SignDllError(Exception exception) return Exception(10, "Failed to sign the impl .dll on disk.", exception); } - /// - /// The debug repro directory does not exist. - /// + /// public static Exception DebugReproDirectoryDoesNotExist(string path) { - return Exception(11, $"The debug repro directory '{path}' does not exist."); + return Exception(11, WellKnownGeneratorMessages.DebugReproDirectoryDoesNotExist(path)); } - /// - /// The debug repro contains a file entry that has no mapping. - /// + /// public static Exception DebugReproMissingFileEntryMapping(string path) { - return Exception(12, $"The debug repro file entry with path '{path}' is missing its assembly path mapping."); + return Exception(12, WellKnownGeneratorMessages.DebugReproMissingFileEntryMapping(path)); } - /// - /// The debug repro contains a file entry that was not recognized. - /// + /// public static Exception DebugReproUnrecognizedFileEntry(string path) { - return Exception(13, $"The debug repro file entry with path '{path}' was not recognized."); + return Exception(13, WellKnownGeneratorMessages.DebugReproUnrecognizedFileEntry(path)); } /// diff --git a/src/WinRT.Interop.Generator/Errors/WellKnownInteropExceptions.cs b/src/WinRT.Interop.Generator/Errors/WellKnownInteropExceptions.cs index 3f9e6c721f..6043cc31fd 100644 --- a/src/WinRT.Interop.Generator/Errors/WellKnownInteropExceptions.cs +++ b/src/WinRT.Interop.Generator/Errors/WellKnownInteropExceptions.cs @@ -284,36 +284,28 @@ public static WellKnownInteropException IDictionary2TypeCodeGenerationError(Type return Exception(27, $"Failed to generate marshalling code for 'IDictionary' type '{dictionaryType}'.", exception); } - /// - /// Some exception was thrown when trying to read the response file. - /// + /// public static WellKnownInteropException ResponseFileReadError(Exception exception) { - return Exception(28, "Failed to read the response file to run 'cswinrtinteropgen'.", exception); + return Exception(28, WellKnownGeneratorMessages.ResponseFileReadError("cswinrtinteropgen"), exception); } - /// - /// Failed to parse an argument from the response file. - /// + /// public static WellKnownInteropException ResponseFileArgumentParsingError(string argumentName, Exception? exception = null) { - return Exception(29, $"Failed to parse argument '{argumentName}' from response file.", exception); + return Exception(29, WellKnownGeneratorMessages.ResponseFileArgumentParsingError(argumentName), exception); } - /// - /// The input response file is malformed. - /// + /// public static WellKnownInteropException MalformedResponseFile() { - return Exception(30, "The response file is malformed and contains invalid content."); + return Exception(30, WellKnownGeneratorMessages.MalformedResponseFile); } - /// - /// The debug repro directory does not exist. - /// + /// public static WellKnownInteropException DebugReproDirectoryDoesNotExist(string path) { - return Exception(31, $"The debug repro directory '{path}' does not exist."); + return Exception(31, WellKnownGeneratorMessages.DebugReproDirectoryDoesNotExist(path)); } /// @@ -788,20 +780,16 @@ public static WellKnownInteropException ReservedDllOriginalPathMismatchFromDebug return Exception(88, $"The reserved '{dllName}' assembly has a mismatching path with the item supplied via '$(ReferencePath)': the debug repro cannot be generated."); } - /// - /// The debug repro contains a file entry that has no mapping. - /// + /// public static WellKnownInteropException DebugReproMissingFileEntryMapping(string path) { - return Exception(89, $"The debug repro file entry with path '{path}' is missing its assembly path mapping."); + return Exception(89, WellKnownGeneratorMessages.DebugReproMissingFileEntryMapping(path)); } - /// - /// The debug repro contains a file entry that was not recognized. - /// + /// public static WellKnownInteropException DebugReproUnrecognizedFileEntry(string path) { - return Exception(90, $"The debug repro file entry with path '{path}' was not recognized."); + return Exception(90, WellKnownGeneratorMessages.DebugReproUnrecognizedFileEntry(path)); } /// diff --git a/src/WinRT.Projection.Generator/Errors/WellKnownProjectionGeneratorExceptions.cs b/src/WinRT.Projection.Generator/Errors/WellKnownProjectionGeneratorExceptions.cs index 5a6c98cc04..4bde0b5e20 100644 --- a/src/WinRT.Projection.Generator/Errors/WellKnownProjectionGeneratorExceptions.cs +++ b/src/WinRT.Projection.Generator/Errors/WellKnownProjectionGeneratorExceptions.cs @@ -26,28 +26,22 @@ private WellKnownProjectionGeneratorExceptions() { } - /// - /// Some exception was thrown when trying to read the response file. - /// + /// public static Exception ResponseFileReadError(Exception exception) { - return Exception(1, "Failed to read the response file to run 'cswinrtprojectiongen'.", exception); + return Exception(1, WellKnownGeneratorMessages.ResponseFileReadError("cswinrtprojectiongen"), exception); } - /// - /// Failed to parse an argument from the response file. - /// + /// public static Exception ResponseFileArgumentParsingError(string argumentName, Exception? exception = null) { - return Exception(2, $"Failed to parse argument '{argumentName}' from response file.", exception); + return Exception(2, WellKnownGeneratorMessages.ResponseFileArgumentParsingError(argumentName), exception); } - /// - /// The input response file is malformed. - /// + /// public static Exception MalformedResponseFile() { - return Exception(3, "The response file is malformed and contains invalid content."); + return Exception(3, WellKnownGeneratorMessages.MalformedResponseFile); } /// @@ -100,28 +94,22 @@ public static Exception CsWinRTProcessError(int exitCode, Exception exception) return Exception(8, $"The projection writer failed during source generation (exit code {exitCode}).", exception); } - /// - /// The debug repro directory does not exist. - /// + /// public static Exception DebugReproDirectoryDoesNotExist(string path) { - return Exception(9, $"The debug repro directory '{path}' does not exist."); + return Exception(9, WellKnownGeneratorMessages.DebugReproDirectoryDoesNotExist(path)); } - /// - /// The debug repro contains a file entry that has no mapping. - /// + /// public static Exception DebugReproMissingFileEntryMapping(string path) { - return Exception(10, $"The debug repro file entry with path '{path}' is missing its assembly path mapping."); + return Exception(10, WellKnownGeneratorMessages.DebugReproMissingFileEntryMapping(path)); } - /// - /// The debug repro contains a file entry that was not recognized. - /// + /// public static Exception DebugReproUnrecognizedFileEntry(string path) { - return Exception(11, $"The debug repro file entry with path '{path}' was not recognized."); + return Exception(11, WellKnownGeneratorMessages.DebugReproUnrecognizedFileEntry(path)); } /// diff --git a/src/WinRT.Projection.Ref.Generator/Errors/WellKnownReferenceProjectionGeneratorExceptions.cs b/src/WinRT.Projection.Ref.Generator/Errors/WellKnownReferenceProjectionGeneratorExceptions.cs index b5ae41ee1b..9e232becc0 100644 --- a/src/WinRT.Projection.Ref.Generator/Errors/WellKnownReferenceProjectionGeneratorExceptions.cs +++ b/src/WinRT.Projection.Ref.Generator/Errors/WellKnownReferenceProjectionGeneratorExceptions.cs @@ -23,28 +23,22 @@ private WellKnownReferenceProjectionGeneratorExceptions() { } - /// - /// Some exception was thrown when trying to read the response file. - /// + /// public static Exception ResponseFileReadError(Exception exception) { - return Exception(1, "Failed to read the response file to run 'cswinrtprojectionrefgen'.", exception); + return Exception(1, WellKnownGeneratorMessages.ResponseFileReadError("cswinrtprojectionrefgen"), exception); } - /// - /// Failed to parse an argument from the response file. - /// + /// public static Exception ResponseFileArgumentParsingError(string argumentName, Exception? exception = null) { - return Exception(2, $"Failed to parse argument '{argumentName}' from response file.", exception); + return Exception(2, WellKnownGeneratorMessages.ResponseFileArgumentParsingError(argumentName), exception); } - /// - /// The input response file is malformed. - /// + /// public static Exception MalformedResponseFile() { - return Exception(3, "The response file is malformed and contains invalid content."); + return Exception(3, WellKnownGeneratorMessages.MalformedResponseFile); } /// @@ -63,28 +57,22 @@ public static Exception CsWinRTProcessError(Exception exception) return Exception(5, "The projection writer failed during source generation.", exception); } - /// - /// The debug repro directory does not exist. - /// + /// public static Exception DebugReproDirectoryDoesNotExist(string path) { - return Exception(6, $"The debug repro directory '{path}' does not exist."); + return Exception(6, WellKnownGeneratorMessages.DebugReproDirectoryDoesNotExist(path)); } - /// - /// The debug repro contains a file entry that has no mapping. - /// + /// public static Exception DebugReproMissingFileEntryMapping(string path) { - return Exception(7, $"The debug repro file entry with path '{path}' is missing its assembly path mapping."); + return Exception(7, WellKnownGeneratorMessages.DebugReproMissingFileEntryMapping(path)); } - /// - /// The debug repro contains a file entry that was not recognized. - /// + /// public static Exception DebugReproUnrecognizedFileEntry(string path) { - return Exception(8, $"The debug repro file entry with path '{path}' was not recognized."); + return Exception(8, WellKnownGeneratorMessages.DebugReproUnrecognizedFileEntry(path)); } /// diff --git a/src/WinRT.WinMD.Generator/Errors/WellKnownWinMDExceptions.cs b/src/WinRT.WinMD.Generator/Errors/WellKnownWinMDExceptions.cs index 58a0678f00..3ec0e57344 100644 --- a/src/WinRT.WinMD.Generator/Errors/WellKnownWinMDExceptions.cs +++ b/src/WinRT.WinMD.Generator/Errors/WellKnownWinMDExceptions.cs @@ -23,28 +23,22 @@ private WellKnownWinMDExceptions() { } - /// - /// Some exception was thrown when trying to read the response file. - /// + /// public static Exception ResponseFileReadError(Exception exception) { - return Exception(1, "Failed to read the response file to run 'cswinrtwinmdgen'.", exception); + return Exception(1, WellKnownGeneratorMessages.ResponseFileReadError("cswinrtwinmdgen"), exception); } - /// - /// The input response file is malformed. - /// + /// public static Exception MalformedResponseFile() { - return Exception(2, "The response file is malformed and contains invalid content."); + return Exception(2, WellKnownGeneratorMessages.MalformedResponseFile); } - /// - /// 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); + return Exception(3, WellKnownGeneratorMessages.ResponseFileArgumentParsingError(argumentName), exception); } /// @@ -79,28 +73,22 @@ public static Exception InputAssemblyRuntimeVersionNotFound(string path) return Exception(7, $"Failed to probe the .NET runtime version from the input assembly '{path}'."); } - /// - /// The debug repro directory does not exist. - /// + /// public static Exception DebugReproDirectoryDoesNotExist(string path) { - return Exception(8, $"The debug repro directory '{path}' does not exist."); + return Exception(8, WellKnownGeneratorMessages.DebugReproDirectoryDoesNotExist(path)); } - /// - /// The debug repro contains a file entry that has no mapping. - /// + /// public static Exception DebugReproMissingFileEntryMapping(string path) { - return Exception(9, $"The debug repro file entry with path '{path}' is missing its assembly path mapping."); + return Exception(9, WellKnownGeneratorMessages.DebugReproMissingFileEntryMapping(path)); } - /// - /// The debug repro contains a file entry that was not recognized. - /// + /// public static Exception DebugReproUnrecognizedFileEntry(string path) { - return Exception(10, $"The debug repro file entry with path '{path}' was not recognized."); + return Exception(10, WellKnownGeneratorMessages.DebugReproUnrecognizedFileEntry(path)); } /// From bc2c32e53632c12c4ad2a78851d6194d4e4ec9b3 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 9 Jun 2026 12:50:56 -0700 Subject: [PATCH 06/15] Consolidate MvidGenerator + IncrementalHashExtensions in Core Both Impl and Interop shipped their own 'MvidGenerator' with complementary overloads ('CreateMvid(Guid, Guid)' for Impl; 'CreateMvid(params IEnumerable)' for Interop). Interop's overload also depended on a local 'IncrementalHashExtensions.AppendData(Stream)' extension whose only consumer was that one method. Move both 'MvidGenerator' overloads into a single file in 'WinRT.Generator.Core' and relocate 'IncrementalHashExtensions' alongside it. Each consumer continues to call the exact overload it already used, with no behavioral change. The impl-gen end-to-end output is byte-identical (257536 bytes, 0 byte diffs) which proves the 'Guid+Guid' MVID computation is preserved exactly. Net diff: 5 files changed, 2 new + 3 deleted. All 5 generators build with 0 warnings and pass end-to-end debug-repro validation (ref-gen: 8/8 0 diffs, WinMD: 16-byte MVID-only delta, proj-gen: 327/327 0 diffs, impl-gen: 0 byte diffs, interop-gen: smoke OK). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Extensions/IncrementalHashExtensions.cs | 4 +-- .../Helpers/MvidGenerator.cs | 26 +++++++++++++- .../Generation/ImplGenerator.cs | 1 + .../Helpers/MvidGenerator.cs | 36 ------------------- .../Generation/InteropGenerator.Emit.cs | 2 +- 5 files changed, 29 insertions(+), 40 deletions(-) rename src/{WinRT.Interop.Generator => WinRT.Generator.Core}/Extensions/IncrementalHashExtensions.cs (95%) rename src/{WinRT.Interop.Generator => WinRT.Generator.Core}/Helpers/MvidGenerator.cs (57%) delete mode 100644 src/WinRT.Impl.Generator/Helpers/MvidGenerator.cs diff --git a/src/WinRT.Interop.Generator/Extensions/IncrementalHashExtensions.cs b/src/WinRT.Generator.Core/Extensions/IncrementalHashExtensions.cs similarity index 95% rename from src/WinRT.Interop.Generator/Extensions/IncrementalHashExtensions.cs rename to src/WinRT.Generator.Core/Extensions/IncrementalHashExtensions.cs index ed75d20673..1db570f31a 100644 --- a/src/WinRT.Interop.Generator/Extensions/IncrementalHashExtensions.cs +++ b/src/WinRT.Generator.Core/Extensions/IncrementalHashExtensions.cs @@ -5,7 +5,7 @@ using System.IO; using System.Security.Cryptography; -namespace WindowsRuntime.InteropGenerator; +namespace WindowsRuntime.Generator.Extensions; /// /// Extensions for the type. @@ -34,4 +34,4 @@ public void AppendData(Stream stream) ArrayPool.Shared.Return(buffer); } } -} \ No newline at end of file +} diff --git a/src/WinRT.Interop.Generator/Helpers/MvidGenerator.cs b/src/WinRT.Generator.Core/Helpers/MvidGenerator.cs similarity index 57% rename from src/WinRT.Interop.Generator/Helpers/MvidGenerator.cs rename to src/WinRT.Generator.Core/Helpers/MvidGenerator.cs index d738973660..200483f064 100644 --- a/src/WinRT.Interop.Generator/Helpers/MvidGenerator.cs +++ b/src/WinRT.Generator.Core/Helpers/MvidGenerator.cs @@ -6,14 +6,38 @@ using System.IO; using System.Linq; using System.Security.Cryptography; +using WindowsRuntime.Generator.Extensions; -namespace WindowsRuntime.InteropGenerator.Helpers; +namespace WindowsRuntime.Generator.Helpers; /// /// A generator for MVIDs for .NET modules. /// internal static class MvidGenerator { + /// + /// Generates a deterministic MVID based on two input IIDs. + /// + /// The first IID to combine. + /// The second IID to combine. + /// The resulting MVID. + public static Guid CreateMvid(Guid left, Guid right) + { + Span input = stackalloc byte[32]; + + // Write the two IIDs in sequence + _ = left.TryWriteBytes(input, bigEndian: true, out _); + _ = right.TryWriteBytes(input[16..], bigEndian: true, out _); + + Span hash = stackalloc byte[SHA1.HashSizeInBytes]; + + // Hash the two IIDs together (the order matters) + _ = SHA1.HashData(input, hash); + + // Create the final MVID from the first 16 bytes of the hash + return new(hash[..16]); + } + /// /// Generates a deterministic MVID based on a set of input assemblies. /// diff --git a/src/WinRT.Impl.Generator/Generation/ImplGenerator.cs b/src/WinRT.Impl.Generator/Generation/ImplGenerator.cs index 1a6149aaee..41ef267294 100644 --- a/src/WinRT.Impl.Generator/Generation/ImplGenerator.cs +++ b/src/WinRT.Impl.Generator/Generation/ImplGenerator.cs @@ -20,6 +20,7 @@ using WindowsRuntime.Generator; using WindowsRuntime.Generator.Errors; using WindowsRuntime.Generator.Extensions; +using WindowsRuntime.Generator.Helpers; using WindowsRuntime.Generator.Parsing; using WindowsRuntime.ImplGenerator.Errors; using WindowsRuntime.ImplGenerator.References; diff --git a/src/WinRT.Impl.Generator/Helpers/MvidGenerator.cs b/src/WinRT.Impl.Generator/Helpers/MvidGenerator.cs deleted file mode 100644 index 4a87706e0c..0000000000 --- a/src/WinRT.Impl.Generator/Helpers/MvidGenerator.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Security.Cryptography; - -namespace WindowsRuntime.ImplGenerator; - -/// -/// A generator for MVIDs for .NET modules. -/// -internal static class MvidGenerator -{ - /// - /// Generates a deterministic MVID based on two input IIDs. - /// - /// The first IID to combine. - /// The second IID to combine. - /// The resulting MVID. - public static Guid CreateMvid(Guid left, Guid right) - { - Span input = stackalloc byte[32]; - - // Write the two IIDs in sequence - _ = left.TryWriteBytes(input, bigEndian: true, out _); - _ = right.TryWriteBytes(input[16..], bigEndian: true, out _); - - Span hash = stackalloc byte[SHA1.HashSizeInBytes]; - - // Hash the two IIDs together (the order matters) - _ = SHA1.HashData(input, hash); - - // Create the final MVID from the first 16 bytes of the hash - return new(hash[..16]); - } -} diff --git a/src/WinRT.Interop.Generator/Generation/InteropGenerator.Emit.cs b/src/WinRT.Interop.Generator/Generation/InteropGenerator.Emit.cs index 24da3772da..012efd323f 100644 --- a/src/WinRT.Interop.Generator/Generation/InteropGenerator.Emit.cs +++ b/src/WinRT.Interop.Generator/Generation/InteropGenerator.Emit.cs @@ -10,10 +10,10 @@ using AsmResolver.DotNet.Signatures; using WindowsRuntime.Generator; using WindowsRuntime.Generator.Errors; +using WindowsRuntime.Generator.Helpers; using WindowsRuntime.InteropGenerator.Builders; using WindowsRuntime.InteropGenerator.Errors; using WindowsRuntime.InteropGenerator.Fixups; -using WindowsRuntime.InteropGenerator.Helpers; using WindowsRuntime.InteropGenerator.Models; using WindowsRuntime.InteropGenerator.References; using WindowsRuntime.InteropGenerator.Rewriters; From c839e750b7a11863270a15235ca5b1fb2b664277 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 9 Jun 2026 13:02:14 -0700 Subject: [PATCH 07/15] Consolidate WellKnownPublicKeys + WellKnownPublicKeyTokens in Core The 160-byte CsWinRT 3.0 strong-name public keys lived in several inconsistent places across the generators: * 'ImplValues.PublicKeyData' (Impl) - the SDK projection key, used 3x, but the type/property name suggested it was a generic 'PublicKey'. * 'InteropValues.WindowsSdkProjectionPublicKey[Data]' (Interop) - the same SDK projection key, dead code (0 callers). * 'InteropValues.CsWinRTPublicKey[Data]' (Interop) - the real CsWinRT runtime key, used 1x. * 'ProjectionGenerator.Emit.CsWinRTPublicKey' (Projection) - the SDK projection key again, used 1x, but misnamed as the CsWinRT key. The const-string forms ('PublicKey', 'CsWinRTPublicKey', 'WindowsSdkProjectionPublicKey') had 0 callers anywhere - leftover dead code. The same 'B5FC90E7...' byte sequence appeared 3 times, which is exactly the kind of duplication that risks accidental drift. Consolidate into a single 'WellKnownPublicKeys' in 'WinRT.Generator.Core': * 'WindowsSdkProjection' - the precompiled SDK projection assembly key (used by Impl forwarder refs and by Projection's Roslyn delay-signing). * 'CsWinRT' - the real CsWinRT 3.0 runtime release key (used by Interop when emitting refs to 'WinRT.Runtime.dll'). Each live caller is repointed; Projection wraps the 'byte[]' as 'ImmutableArray' on the fly via 'ImmutableCollectionsMarshal' for Roslyn's API. The dead-code declarations are deleted, including the now-empty 'ImplValues' and 'InteropValues' files entirely. The public key tokens ('Interop\WellKnownPublicKeyTokens.cs' with 6 entries + 'WinMD\WellKnownPublicKeyTokens.cs' with 1 entry) are unioned into 'WinRT.Generator.Core\References\WellKnownPublicKeyTokens.cs' (7 entries total: MSCorLib + the 6 from Interop). Net diff: 10 modified files + 2 new + 4 deleted (16 files total), -22 LOC. Removes the dead 'WindowsSdkProjectionPublicKey[Data]' declarations from 'InteropValues' (~31 LOC), fixes the 'PublicKey'/'CsWinRTPublicKey'/'WindowsSdkProjection' naming inconsistency, and centralizes the canonical byte sequences in one file. The byte-identical impl-gen output is the strongest possible confirmation that the consolidated keys match the originals exactly. All 5 generators build with 0 warnings and pass end-to-end debug-repro validation (ref-gen: 8/8 0 diffs, WinMD: 16-byte MVID-only delta, proj-gen: 327/327 0 diffs, impl-gen: 0 byte diffs, interop-gen: smoke OK). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../References/WellKnownPublicKeyTokens.cs | 22 +++++++++----- .../References/WellKnownPublicKeys.cs | 24 +++++++++++++++ .../Generation/ImplGenerator.cs | 8 ++--- .../References/ImplValues.cs | 20 ------------- .../Generation/InteropGenerator.Discover.cs | 3 +- .../References/InteropReferences.cs | 1 + .../References/InteropValues.cs | 30 ------------------- .../Generation/ProjectionGenerator.Emit.cs | 10 ++----- .../References/WellKnownPublicKeyTokens.cs | 15 ---------- .../Writers/WinMDWriter.TypeMapping.cs | 2 +- .../Writers/WinMDWriter.cs | 2 +- 11 files changed, 50 insertions(+), 87 deletions(-) rename src/{WinRT.Interop.Generator => WinRT.Generator.Core}/References/WellKnownPublicKeyTokens.cs (60%) create mode 100644 src/WinRT.Generator.Core/References/WellKnownPublicKeys.cs delete mode 100644 src/WinRT.Impl.Generator/References/ImplValues.cs delete mode 100644 src/WinRT.Interop.Generator/References/InteropValues.cs delete mode 100644 src/WinRT.WinMD.Generator/References/WellKnownPublicKeyTokens.cs diff --git a/src/WinRT.Interop.Generator/References/WellKnownPublicKeyTokens.cs b/src/WinRT.Generator.Core/References/WellKnownPublicKeyTokens.cs similarity index 60% rename from src/WinRT.Interop.Generator/References/WellKnownPublicKeyTokens.cs rename to src/WinRT.Generator.Core/References/WellKnownPublicKeyTokens.cs index a9f34cc3da..7247409794 100644 --- a/src/WinRT.Interop.Generator/References/WellKnownPublicKeyTokens.cs +++ b/src/WinRT.Generator.Core/References/WellKnownPublicKeyTokens.cs @@ -1,35 +1,41 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -namespace WindowsRuntime.InteropGenerator.References; +namespace WindowsRuntime.Generator.References; /// -/// Well known public key tokens. +/// Well known public key tokens for the framework and CsWinRT assemblies referenced by the +/// CsWinRT CLI generators. /// internal static class WellKnownPublicKeyTokens { /// - /// The public key data for System.Memory.dll. + /// The public key token for mscorlib (b77a5c561934e089). + /// + public static readonly byte[] MSCorLib = [0xB7, 0x7A, 0x5C, 0x56, 0x19, 0x34, 0xE0, 0x89]; + + /// + /// The public key token for System.Memory.dll. /// public static readonly byte[] SystemMemory = [0xCC, 0x7B, 0x13, 0xFF, 0xCD, 0x2D, 0xDD, 0x51]; /// - /// The public key data for System.ObjectModel.dll. + /// The public key token for System.ObjectModel.dll. /// public static readonly byte[] SystemObjectModel = [0xB0, 0x3F, 0x5F, 0x7F, 0x11, 0xD5, 0x0A, 0x3A]; /// - /// The public key data for System.Runtime.InteropServices.dll. + /// The public key token for System.Runtime.InteropServices.dll. /// public static readonly byte[] SystemRuntimeInteropServices = [0xB0, 0x3F, 0x5F, 0x7F, 0x11, 0xD5, 0x0A, 0x3A]; /// - /// The public key data for System.Numerics.Vectors.dll. + /// The public key token for System.Numerics.Vectors.dll. /// public static readonly byte[] SystemNumericsVectors = [0xB0, 0x3F, 0x5F, 0x7F, 0x11, 0xD5, 0x0A, 0x3A]; /// - /// The public key data for System.Threading.dll. + /// The public key token for System.Threading.dll. /// public static readonly byte[] SystemThreading = [0xB0, 0x3F, 0x5F, 0x7F, 0x11, 0xD5, 0x0A, 0x3A]; @@ -37,4 +43,4 @@ internal static class WellKnownPublicKeyTokens /// The public key token for CsWinRT assemblies (31bf3856ad364e35). /// public static readonly byte[] CsWinRT = [0x31, 0xBF, 0x38, 0x56, 0xAD, 0x36, 0x4E, 0x35]; -} \ No newline at end of file +} diff --git a/src/WinRT.Generator.Core/References/WellKnownPublicKeys.cs b/src/WinRT.Generator.Core/References/WellKnownPublicKeys.cs new file mode 100644 index 0000000000..73e159760b --- /dev/null +++ b/src/WinRT.Generator.Core/References/WellKnownPublicKeys.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace WindowsRuntime.Generator.References; + +/// +/// The 160-byte strong-name public keys consumed by the CsWinRT CLI generators when emitting +/// assembly references or signing generated assemblies. +/// +internal static class WellKnownPublicKeys +{ + /// + /// The public key used by the precompiled Windows SDK projection assemblies (WinRT.Sdk.Projection.dll, + /// WinRT.Sdk.Xaml.Projection.dll) and the merged projection assembly (WinRT.Projection.dll) that + /// the projection generator produces at app build time. The impl generator stamps this same key on the + /// AssemblyReference entries it emits in the forwarder .dll. + /// + public static readonly byte[] WindowsSdkProjection = [0x00, 0x24, 0x00, 0x00, 0x04, 0x80, 0x00, 0x00, 0x94, 0x00, 0x00, 0x00, 0x06, 0x02, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x52, 0x53, 0x41, 0x31, 0x00, 0x04, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0xB5, 0xFC, 0x90, 0xE7, 0x02, 0x7F, 0x67, 0x87, 0x1E, 0x77, 0x3A, 0x8F, 0xDE, 0x89, 0x38, 0xC8, 0x1D, 0xD4, 0x02, 0xBA, 0x65, 0xB9, 0x20, 0x1D, 0x60, 0x59, 0x3E, 0x96, 0xC4, 0x92, 0x65, 0x1E, 0x88, 0x9C, 0xC1, 0x3F, 0x14, 0x15, 0xEB, 0xB5, 0x3F, 0xAC, 0x11, 0x31, 0xAE, 0x0B, 0xD3, 0x33, 0xC5, 0xEE, 0x60, 0x21, 0x67, 0x2D, 0x97, 0x18, 0xEA, 0x31, 0xA8, 0xAE, 0xBD, 0x0D, 0xA0, 0x07, 0x2F, 0x25, 0xD8, 0x7D, 0xBA, 0x6F, 0xC9, 0x0F, 0xFD, 0x59, 0x8E, 0xD4, 0xDA, 0x35, 0xE4, 0x4C, 0x39, 0x8C, 0x45, 0x43, 0x07, 0xE8, 0xE3, 0x3B, 0x84, 0x26, 0x14, 0x3D, 0xAE, 0xC9, 0xF5, 0x96, 0x83, 0x6F, 0x97, 0xC8, 0xF7, 0x47, 0x50, 0xE5, 0x97, 0x5C, 0x64, 0xE2, 0x18, 0x9F, 0x45, 0xDE, 0xF4, 0x6B, 0x2A, 0x2B, 0x12, 0x47, 0xAD, 0xC3, 0x65, 0x2B, 0xF5, 0xC3, 0x08, 0x05, 0x5D, 0xA9]; + + /// + /// The public key for the real CsWinRT 3.0 runtime release assemblies (e.g. WinRT.Runtime.dll). + /// + public static readonly byte[] CsWinRT = [0x00, 0x24, 0x00, 0x00, 0x04, 0x80, 0x00, 0x00, 0x94, 0x00, 0x00, 0x00, 0x06, 0x02, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x52, 0x53, 0x41, 0x31, 0x00, 0x04, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x65, 0x32, 0x43, 0xC2, 0xEC, 0x7B, 0x83, 0x00, 0x92, 0x93, 0xB0, 0xE7, 0x98, 0x19, 0x3A, 0x8B, 0xB1, 0x03, 0x90, 0xB5, 0xA1, 0x2A, 0x3F, 0x2D, 0x5C, 0x58, 0x28, 0x2D, 0x71, 0x29, 0xDA, 0xD2, 0xD0, 0xC0, 0x12, 0x75, 0x53, 0x29, 0xC1, 0xA4, 0x51, 0x73, 0xE1, 0xAC, 0x9B, 0x8B, 0x4C, 0x0A, 0x4E, 0x07, 0x82, 0x30, 0xBD, 0xD5, 0xE8, 0xCE, 0x39, 0x32, 0x08, 0x3B, 0x09, 0x89, 0x2D, 0x82, 0x8E, 0x4B, 0x18, 0xAF, 0x00, 0xCE, 0x74, 0x6B, 0xCC, 0x99, 0x4D, 0xAD, 0x06, 0xAC, 0xEC, 0x3E, 0x69, 0x79, 0x3E, 0x75, 0xF8, 0xD2, 0xCC, 0x8E, 0x77, 0xF4, 0x46, 0x68, 0x55, 0x0C, 0xA9, 0xB8, 0x3D, 0xD6, 0x48, 0x2D, 0xA9, 0xAA, 0x4D, 0x2E, 0x8B, 0xCF, 0x2E, 0x17, 0x93, 0xCF, 0x84, 0xC4, 0x95, 0x34, 0x6B, 0x1B, 0x99, 0xEB, 0x99, 0x76, 0x7F, 0xB4, 0x12, 0x46, 0x67, 0x2E, 0x7C, 0xF0]; +} diff --git a/src/WinRT.Impl.Generator/Generation/ImplGenerator.cs b/src/WinRT.Impl.Generator/Generation/ImplGenerator.cs index 41ef267294..10b86b510c 100644 --- a/src/WinRT.Impl.Generator/Generation/ImplGenerator.cs +++ b/src/WinRT.Impl.Generator/Generation/ImplGenerator.cs @@ -22,8 +22,8 @@ using WindowsRuntime.Generator.Extensions; using WindowsRuntime.Generator.Helpers; using WindowsRuntime.Generator.Parsing; +using WindowsRuntime.Generator.References; using WindowsRuntime.ImplGenerator.Errors; -using WindowsRuntime.ImplGenerator.References; namespace WindowsRuntime.ImplGenerator.Generation; @@ -245,7 +245,7 @@ private static void EmitTypeForwards(ModuleDefinition inputModule, ModuleDefinit // will always have a version number equal or higher than this, so it will load correctly. AssemblyReference sdkProjectionAssembly = new("WinRT.Sdk.Projection"u8, new Version(0, 0, 0, 0)) { - PublicKeyOrToken = ImplValues.PublicKeyData, + PublicKeyOrToken = WellKnownPublicKeys.WindowsSdkProjection, HasPublicKey = true }; @@ -253,7 +253,7 @@ private static void EmitTypeForwards(ModuleDefinition inputModule, ModuleDefinit // This is only used when the option to use Windows UI Xaml projections is enabled. AssemblyReference sdkXamlProjectionAssembly = new("WinRT.Sdk.Xaml.Projection"u8, new Version(0, 0, 0, 0)) { - PublicKeyOrToken = ImplValues.PublicKeyData, + PublicKeyOrToken = WellKnownPublicKeys.WindowsSdkProjection, HasPublicKey = true }; @@ -261,7 +261,7 @@ private static void EmitTypeForwards(ModuleDefinition inputModule, ModuleDefinit // Unlike the implementation .dll for the Windows SDK however, this .dll is created on the fly. AssemblyReference projectionAssembly = new("WinRT.Projection"u8, new Version(0, 0, 0, 0)) { - PublicKeyOrToken = ImplValues.PublicKeyData, + PublicKeyOrToken = WellKnownPublicKeys.WindowsSdkProjection, HasPublicKey = true }; diff --git a/src/WinRT.Impl.Generator/References/ImplValues.cs b/src/WinRT.Impl.Generator/References/ImplValues.cs deleted file mode 100644 index e29b8828de..0000000000 --- a/src/WinRT.Impl.Generator/References/ImplValues.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace WindowsRuntime.ImplGenerator.References; - -/// -/// Well known impl values (constants). -/// -internal static class ImplValues -{ - /// - /// The public key for CsWinRT assemblies. - /// - public const string PublicKey = "0024000004800000940000000602000000240000525341310004000001000100B5FC90E7027F67871E773A8FDE8938C81DD402BA65B9201D60593E96C492651E889CC13F1415EBB53FAC1131AE0BD333C5EE6021672D9718EA31A8AEBD0DA0072F25D87DBA6FC90FFD598ED4DA35E44C398C454307E8E33B8426143DAEC9F596836F97C8F74750E5975C64E2189F45DEF46B2A2B1247ADC3652BF5C308055DA9"; - - /// - /// The public key data for CsWinRT assemblies. - /// - public static readonly byte[] PublicKeyData = [0x00, 0x24, 0x00, 0x00, 0x04, 0x80, 0x00, 0x00, 0x94, 0x00, 0x00, 0x00, 0x06, 0x02, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x52, 0x53, 0x41, 0x31, 0x00, 0x04, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0xB5, 0xFC, 0x90, 0xE7, 0x02, 0x7F, 0x67, 0x87, 0x1E, 0x77, 0x3A, 0x8F, 0xDE, 0x89, 0x38, 0xC8, 0x1D, 0xD4, 0x02, 0xBA, 0x65, 0xB9, 0x20, 0x1D, 0x60, 0x59, 0x3E, 0x96, 0xC4, 0x92, 0x65, 0x1E, 0x88, 0x9C, 0xC1, 0x3F, 0x14, 0x15, 0xEB, 0xB5, 0x3F, 0xAC, 0x11, 0x31, 0xAE, 0x0B, 0xD3, 0x33, 0xC5, 0xEE, 0x60, 0x21, 0x67, 0x2D, 0x97, 0x18, 0xEA, 0x31, 0xA8, 0xAE, 0xBD, 0x0D, 0xA0, 0x07, 0x2F, 0x25, 0xD8, 0x7D, 0xBA, 0x6F, 0xC9, 0x0F, 0xFD, 0x59, 0x8E, 0xD4, 0xDA, 0x35, 0xE4, 0x4C, 0x39, 0x8C, 0x45, 0x43, 0x07, 0xE8, 0xE3, 0x3B, 0x84, 0x26, 0x14, 0x3D, 0xAE, 0xC9, 0xF5, 0x96, 0x83, 0x6F, 0x97, 0xC8, 0xF7, 0x47, 0x50, 0xE5, 0x97, 0x5C, 0x64, 0xE2, 0x18, 0x9F, 0x45, 0xDE, 0xF4, 0x6B, 0x2A, 0x2B, 0x12, 0x47, 0xAD, 0xC3, 0x65, 0x2B, 0xF5, 0xC3, 0x08, 0x05, 0x5D, 0xA9]; -} \ No newline at end of file diff --git a/src/WinRT.Interop.Generator/Generation/InteropGenerator.Discover.cs b/src/WinRT.Interop.Generator/Generation/InteropGenerator.Discover.cs index c2444f365a..42760ba3c2 100644 --- a/src/WinRT.Interop.Generator/Generation/InteropGenerator.Discover.cs +++ b/src/WinRT.Interop.Generator/Generation/InteropGenerator.Discover.cs @@ -12,6 +12,7 @@ using AsmResolver.PE; using WindowsRuntime.Generator.Errors; using WindowsRuntime.Generator.Extensions; +using WindowsRuntime.Generator.References; using WindowsRuntime.InteropGenerator.Discovery; using WindowsRuntime.InteropGenerator.Errors; using WindowsRuntime.InteropGenerator.Models; @@ -557,7 +558,7 @@ private static InteropReferences CreateDiscoveryInteropReferences(ModuleDefiniti AssemblyReference windowsRuntimeAssembly = new("WinRT.Runtime"u8, windowsRuntimeVersion) { // Set the public keys, as it's needed to ensure references compare as equals as expected - PublicKeyOrToken = InteropValues.CsWinRTPublicKeyData, + PublicKeyOrToken = WellKnownPublicKeys.CsWinRT, HasPublicKey = true }; diff --git a/src/WinRT.Interop.Generator/References/InteropReferences.cs b/src/WinRT.Interop.Generator/References/InteropReferences.cs index fb8e570927..77c1e2ed9d 100644 --- a/src/WinRT.Interop.Generator/References/InteropReferences.cs +++ b/src/WinRT.Interop.Generator/References/InteropReferences.cs @@ -4,6 +4,7 @@ using System; using AsmResolver.DotNet; using AsmResolver.DotNet.Signatures; +using WindowsRuntime.Generator.References; using WindowsRuntime.InteropGenerator.Factories; #pragma warning disable IDE0032 diff --git a/src/WinRT.Interop.Generator/References/InteropValues.cs b/src/WinRT.Interop.Generator/References/InteropValues.cs deleted file mode 100644 index a74b9748d5..0000000000 --- a/src/WinRT.Interop.Generator/References/InteropValues.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace WindowsRuntime.InteropGenerator.References; - -/// -/// Well known interop values (constants). -/// -internal static class InteropValues -{ - /// - /// The public key for CsWinRT assemblies. - /// - public const string CsWinRTPublicKey = "0024000004800000940000000602000000240000525341310004000001000100653243C2EC7B83009293B0E798193A8BB10390B5A12A3F2D5C58282D7129DAD2D0C012755329C1A45173E1AC9B8B4C0A4E078230BDD5E8CE3932083B09892D828E4B18AF00CE746BCC994DAD06ACEC3E69793E75F8D2CC8E77F44668550CA9B83DD6482DA9AA4D2E8BCF2E1793CF84C495346B1B99EB99767FB41246672E7CF0"; - - /// - /// The public key for Windows SDK projection assemblies. - /// - public const string WindowsSdkProjectionPublicKey = "0024000004800000940000000602000000240000525341310004000001000100B5FC90E7027F67871E773A8FDE8938C81DD402BA65B9201D60593E96C492651E889CC13F1415EBB53FAC1131AE0BD333C5EE6021672D9718EA31A8AEBD0DA0072F25D87DBA6FC90FFD598ED4DA35E44C398C454307E8E33B8426143DAEC9F596836F97C8F74750E5975C64E2189F45DEF46B2A2B1247ADC3652BF5C308055DA9"; - - /// - /// The public key data for CsWinRT assemblies. - /// - public static readonly byte[] CsWinRTPublicKeyData = [0x00, 0x24, 0x00, 0x00, 0x04, 0x80, 0x00, 0x00, 0x94, 0x00, 0x00, 0x00, 0x06, 0x02, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x52, 0x53, 0x41, 0x31, 0x00, 0x04, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x65, 0x32, 0x43, 0xC2, 0xEC, 0x7B, 0x83, 0x00, 0x92, 0x93, 0xB0, 0xE7, 0x98, 0x19, 0x3A, 0x8B, 0xB1, 0x03, 0x90, 0xB5, 0xA1, 0x2A, 0x3F, 0x2D, 0x5C, 0x58, 0x28, 0x2D, 0x71, 0x29, 0xDA, 0xD2, 0xD0, 0xC0, 0x12, 0x75, 0x53, 0x29, 0xC1, 0xA4, 0x51, 0x73, 0xE1, 0xAC, 0x9B, 0x8B, 0x4C, 0x0A, 0x4E, 0x07, 0x82, 0x30, 0xBD, 0xD5, 0xE8, 0xCE, 0x39, 0x32, 0x08, 0x3B, 0x09, 0x89, 0x2D, 0x82, 0x8E, 0x4B, 0x18, 0xAF, 0x00, 0xCE, 0x74, 0x6B, 0xCC, 0x99, 0x4D, 0xAD, 0x06, 0xAC, 0xEC, 0x3E, 0x69, 0x79, 0x3E, 0x75, 0xF8, 0xD2, 0xCC, 0x8E, 0x77, 0xF4, 0x46, 0x68, 0x55, 0x0C, 0xA9, 0xB8, 0x3D, 0xD6, 0x48, 0x2D, 0xA9, 0xAA, 0x4D, 0x2E, 0x8B, 0xCF, 0x2E, 0x17, 0x93, 0xCF, 0x84, 0xC4, 0x95, 0x34, 0x6B, 0x1B, 0x99, 0xEB, 0x99, 0x76, 0x7F, 0xB4, 0x12, 0x46, 0x67, 0x2E, 0x7C, 0xF0]; - - /// - /// The public key data for Windows SDK projection assemblies. - /// - public static readonly byte[] WindowsSdkProjectionPublicKeyData = [0x00, 0x24, 0x00, 0x00, 0x04, 0x80, 0x00, 0x00, 0x94, 0x00, 0x00, 0x00, 0x06, 0x02, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x52, 0x53, 0x41, 0x31, 0x00, 0x04, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0xB5, 0xFC, 0x90, 0xE7, 0x02, 0x7F, 0x67, 0x87, 0x1E, 0x77, 0x3A, 0x8F, 0xDE, 0x89, 0x38, 0xC8, 0x1D, 0xD4, 0x02, 0xBA, 0x65, 0xB9, 0x20, 0x1D, 0x60, 0x59, 0x3E, 0x96, 0xC4, 0x92, 0x65, 0x1E, 0x88, 0x9C, 0xC1, 0x3F, 0x14, 0x15, 0xEB, 0xB5, 0x3F, 0xAC, 0x11, 0x31, 0xAE, 0x0B, 0xD3, 0x33, 0xC5, 0xEE, 0x60, 0x21, 0x67, 0x2D, 0x97, 0x18, 0xEA, 0x31, 0xA8, 0xAE, 0xBD, 0x0D, 0xA0, 0x07, 0x2F, 0x25, 0xD8, 0x7D, 0xBA, 0x6F, 0xC9, 0x0F, 0xFD, 0x59, 0x8E, 0xD4, 0xDA, 0x35, 0xE4, 0x4C, 0x39, 0x8C, 0x45, 0x43, 0x07, 0xE8, 0xE3, 0x3B, 0x84, 0x26, 0x14, 0x3D, 0xAE, 0xC9, 0xF5, 0x96, 0x83, 0x6F, 0x97, 0xC8, 0xF7, 0x47, 0x50, 0xE5, 0x97, 0x5C, 0x64, 0xE2, 0x18, 0x9F, 0x45, 0xDE, 0xF4, 0x6B, 0x2A, 0x2B, 0x12, 0x47, 0xAD, 0xC3, 0x65, 0x2B, 0xF5, 0xC3, 0x08, 0x05, 0x5D, 0xA9]; -} \ No newline at end of file diff --git a/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.Emit.cs b/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.Emit.cs index 71ad974fbe..506a93f8f8 100644 --- a/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.Emit.cs +++ b/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.Emit.cs @@ -3,13 +3,14 @@ using System; using System.Collections.Generic; -using System.Collections.Immutable; using System.IO; +using System.Runtime.InteropServices; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Emit; using Microsoft.CodeAnalysis.Text; using WindowsRuntime.Generator.Errors; +using WindowsRuntime.Generator.References; using WindowsRuntime.ProjectionGenerator.Errors; namespace WindowsRuntime.ProjectionGenerator.Generation; @@ -17,11 +18,6 @@ namespace WindowsRuntime.ProjectionGenerator.Generation; /// internal partial class ProjectionGenerator { - /// - /// The public key for CsWinRT assemblies, used for delay signing projection DLLs. - /// - private static readonly ImmutableArray CsWinRTPublicKey = [0x00, 0x24, 0x00, 0x00, 0x04, 0x80, 0x00, 0x00, 0x94, 0x00, 0x00, 0x00, 0x06, 0x02, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x52, 0x53, 0x41, 0x31, 0x00, 0x04, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0xB5, 0xFC, 0x90, 0xE7, 0x02, 0x7F, 0x67, 0x87, 0x1E, 0x77, 0x3A, 0x8F, 0xDE, 0x89, 0x38, 0xC8, 0x1D, 0xD4, 0x02, 0xBA, 0x65, 0xB9, 0x20, 0x1D, 0x60, 0x59, 0x3E, 0x96, 0xC4, 0x92, 0x65, 0x1E, 0x88, 0x9C, 0xC1, 0x3F, 0x14, 0x15, 0xEB, 0xB5, 0x3F, 0xAC, 0x11, 0x31, 0xAE, 0x0B, 0xD3, 0x33, 0xC5, 0xEE, 0x60, 0x21, 0x67, 0x2D, 0x97, 0x18, 0xEA, 0x31, 0xA8, 0xAE, 0xBD, 0x0D, 0xA0, 0x07, 0x2F, 0x25, 0xD8, 0x7D, 0xBA, 0x6F, 0xC9, 0x0F, 0xFD, 0x59, 0x8E, 0xD4, 0xDA, 0x35, 0xE4, 0x4C, 0x39, 0x8C, 0x45, 0x43, 0x07, 0xE8, 0xE3, 0x3B, 0x84, 0x26, 0x14, 0x3D, 0xAE, 0xC9, 0xF5, 0x96, 0x83, 0x6F, 0x97, 0xC8, 0xF7, 0x47, 0x50, 0xE5, 0x97, 0x5C, 0x64, 0xE2, 0x18, 0x9F, 0x45, 0xDE, 0xF4, 0x6B, 0x2A, 0x2B, 0x12, 0x47, 0xAD, 0xC3, 0x65, 0x2B, 0xF5, 0xC3, 0x08, 0x05, 0x5D, 0xA9]; - /// /// Runs the emit logic for the generator. /// @@ -68,7 +64,7 @@ private static void Emit(ProjectionGeneratorArgs args, ProjectionGeneratorProces allowUnsafe: true, optimizationLevel: OptimizationLevel.Release, deterministic: true, - cryptoPublicKey: CsWinRTPublicKey, + cryptoPublicKey: ImmutableCollectionsMarshal.AsImmutableArray(WellKnownPublicKeys.WindowsSdkProjection), delaySign: true, generalDiagnosticOption: ReportDiagnostic.Info)); } diff --git a/src/WinRT.WinMD.Generator/References/WellKnownPublicKeyTokens.cs b/src/WinRT.WinMD.Generator/References/WellKnownPublicKeyTokens.cs deleted file mode 100644 index 77897462dc..0000000000 --- a/src/WinRT.WinMD.Generator/References/WellKnownPublicKeyTokens.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace WindowsRuntime.WinMDGenerator.References; - -/// -/// Well known public key tokens. -/// -internal static class WellKnownPublicKeyTokens -{ - /// - /// The public key data for mscorlib. - /// - public static readonly byte[] MSCorLib = [0xB7, 0x7A, 0x5C, 0x56, 0x19, 0x34, 0xE0, 0x89]; -} \ No newline at end of file diff --git a/src/WinRT.WinMD.Generator/Writers/WinMDWriter.TypeMapping.cs b/src/WinRT.WinMD.Generator/Writers/WinMDWriter.TypeMapping.cs index 4412429c39..07a492aa10 100644 --- a/src/WinRT.WinMD.Generator/Writers/WinMDWriter.TypeMapping.cs +++ b/src/WinRT.WinMD.Generator/Writers/WinMDWriter.TypeMapping.cs @@ -6,9 +6,9 @@ using AsmResolver.DotNet; using AsmResolver.DotNet.Signatures; using AsmResolver.PE.DotNet.Metadata.Tables; +using WindowsRuntime.Generator.References; using WindowsRuntime.InteropGenerator.References; using WindowsRuntime.WinMDGenerator.Models; -using WindowsRuntime.WinMDGenerator.References; using AssemblyAttributes = AsmResolver.PE.DotNet.Metadata.Tables.AssemblyAttributes; #pragma warning disable IDE0072 diff --git a/src/WinRT.WinMD.Generator/Writers/WinMDWriter.cs b/src/WinRT.WinMD.Generator/Writers/WinMDWriter.cs index bd6ee8f52e..e7af3b363f 100644 --- a/src/WinRT.WinMD.Generator/Writers/WinMDWriter.cs +++ b/src/WinRT.WinMD.Generator/Writers/WinMDWriter.cs @@ -4,11 +4,11 @@ using System; using System.Collections.Generic; using AsmResolver.DotNet; +using WindowsRuntime.Generator.References; using WindowsRuntime.InteropGenerator.References; using WindowsRuntime.WinMDGenerator.Errors; using WindowsRuntime.WinMDGenerator.Helpers; using WindowsRuntime.WinMDGenerator.Models; -using WindowsRuntime.WinMDGenerator.References; using AssemblyAttributes = AsmResolver.PE.DotNet.Metadata.Tables.AssemblyAttributes; #pragma warning disable IDE0046 From 989332678682e2a20220c1b17830f2e1676ebe00 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 9 Jun 2026 13:19:26 -0700 Subject: [PATCH 08/15] Make GeneratorPhaseRunner generic in TArgs and capture the args itself 'GeneratorHost.Prepare' previously returned a tuple '(TArgs Args, GeneratorPhaseRunner Runner)', and each per-tool 'Run' method captured the 'args' local in a closure for every phase body ('runner.RunPhase("phase", () => DoPhase(args, ...))'). Make 'GeneratorPhaseRunner' generic in 'TArgs', have it capture the parsed args directly, and pass them to every body delegate. The 'RunPhase' overloads now take 'Action' / 'Func' instead of 'Action' / 'Func'. Per-tool exception identity is fully preserved because the per-tool 'wrapUnhandled' delegate is still invoked unchanged. Phase bodies that only need 'args' now bind to a method group (e.g. 'body: Discover' instead of 'body: () => Discover(args)') and allocate zero closures per invocation. Five phases across the five generators benefit from this: WinMD 'Discover', Interop 'Discover', Projection 'ProcessReferences', ProjectionRef 'BuildWriterOptions', and Impl 'LoadOutputModule'. The remaining eight phases (which need additional state besides 'args') still use lambdas, but with 'args' as an explicit parameter rather than a captured local. Rename 'GeneratorHost.Prepare' to 'GeneratorHost.CreateRunner' since the method now exclusively returns the runner (no longer a preamble-only operation that needs a separate "prepare" verb). Net diff: 7 files, +83 / -67 LOC. All 5 generators build with 0 warnings and pass end-to-end debug-repro validation (ref-gen: 8/8 0 diffs, WinMD: 16-byte MVID-only delta, proj-gen: 327/327 0 diffs, impl-gen: 0 byte diffs, interop-gen: smoke OK). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/WinRT.Generator.Core/GeneratorHost.cs | 27 +++++----- .../GeneratorPhaseRunner.cs | 53 ++++++++++++------- .../Generation/ImplGenerator.cs | 20 +++---- .../Generation/InteropGenerator.cs | 12 ++--- .../Generation/ProjectionGenerator.cs | 18 +++---- .../ReferenceProjectionGenerator.cs | 10 ++-- .../Generation/WinMDGenerator.cs | 10 ++-- 7 files changed, 83 insertions(+), 67 deletions(-) diff --git a/src/WinRT.Generator.Core/GeneratorHost.cs b/src/WinRT.Generator.Core/GeneratorHost.cs index 94b1d09b13..e8f543eeaf 100644 --- a/src/WinRT.Generator.Core/GeneratorHost.cs +++ b/src/WinRT.Generator.Core/GeneratorHost.cs @@ -18,22 +18,24 @@ namespace WindowsRuntime.Generator; /// Parse the response file into a per-tool args record. /// If DebugReproDirectory is set and we are not already replaying, save a debug repro of the current invocation. /// -/// encapsulates that preamble. Each generator's Run now starts with a -/// single call to it; the per-tool unpack / save / parse logic is supplied via delegates so behavior -/// stays identical (same log messages, same exception phases, same per-tool unhandled exception type). +/// encapsulates that preamble. Each generator's Run now starts with +/// a single call to it; the per-tool unpack / save / parse logic is supplied via delegates so behavior stays +/// identical (same log messages, same exception phases, same per-tool unhandled exception type). /// /// The same Run methods then proceed through a series of phases (loading, processing, emit, ...), /// each wrapped in an identical try/catch that re-throws as the per-tool -/// Unhandled*Exception. additionally returns a -/// bound to the same per-tool wrapUnhandled and log -/// delegates so each phase can be expressed as a single -/// call instead of a hand-written try/catch. +/// Unhandled*Exception. returns a +/// bound to the parsed args plus the same per-tool +/// wrapUnhandled and log delegates, so each phase can be expressed as a single +/// call instead of a +/// hand-written try/catch. /// /// internal static class GeneratorHost { /// - /// Runs the shared unpack → parse → save preamble for a CsWinRT CLI generator. + /// Runs the shared unpack → parse → save preamble for a CsWinRT CLI generator and returns a + /// ready to drive the remaining per-tool phases. /// /// The per-tool args record (must implement ). /// The input file path (response file or debug-repro .zip). @@ -45,10 +47,10 @@ internal static class GeneratorHost /// Logs a progress message to the user (typically ConsoleApp.Log from ConsoleAppFramework). /// The token for the operation. /// - /// A pair containing the parsed instance and a - /// pre-bound to and for use by subsequent phases. + /// A pre-bound to the parsed args, + /// and for use by subsequent phases. /// - public static (TArgs Args, GeneratorPhaseRunner Runner) Prepare( + public static GeneratorPhaseRunner CreateRunner( string inputFilePath, string toolName, Func unpackDebugRepro, @@ -120,8 +122,9 @@ public static (TArgs Args, GeneratorPhaseRunner Runner) Prepare( args.Token.ThrowIfCancellationRequested(); - return (args, new GeneratorPhaseRunner(wrapUnhandled, log)); + return new GeneratorPhaseRunner(args, wrapUnhandled, log); } } + diff --git a/src/WinRT.Generator.Core/GeneratorPhaseRunner.cs b/src/WinRT.Generator.Core/GeneratorPhaseRunner.cs index 8e71fb5e3d..cc151243d6 100644 --- a/src/WinRT.Generator.Core/GeneratorPhaseRunner.cs +++ b/src/WinRT.Generator.Core/GeneratorPhaseRunner.cs @@ -10,16 +10,21 @@ namespace WindowsRuntime.Generator; /// Runs the body of a single generator phase, wrapping unexpected exceptions in the per-tool /// Unhandled*Exception and optionally logging a progress message before the body runs. /// +/// The per-tool args record (must implement ). /// /// Each generator's Run method historically wrapped every phase (loading, processing, emit, ...) /// in an identical try/catch that re-threw as the per-tool Unhandled*Exception -/// (with the phase name as the constructor argument). captures -/// the per-tool wrapUnhandled and log delegates once (it is returned bound to them by -/// ) and lets each phase call site collapse to a single -/// (or overload) invocation. Per-tool exception identity -/// is fully preserved because the original wrapUnhandled delegate is invoked unchanged. +/// (with the phase name as the constructor argument). captures +/// the parsed instance plus the per-tool wrapUnhandled and log +/// delegates (it is returned bound to all three by ) and +/// lets each phase call site collapse to a single (or +/// overload) invocation. The captured is forwarded to every body delegate, +/// so phases that only depend on it can be expressed as a static lambda (or method group) with +/// zero per-call allocations. Per-tool exception identity is fully preserved because the original +/// wrapUnhandled delegate is invoked unchanged. /// -internal readonly struct GeneratorPhaseRunner +internal readonly struct GeneratorPhaseRunner + where TArgs : IGeneratorArgs { /// /// The per-tool wrapUnhandled delegate. @@ -32,27 +37,34 @@ internal readonly struct GeneratorPhaseRunner private readonly Action _log; /// - /// Creates a new bound to the given per-tool delegates. + /// Creates a new bound to the given args and per-tool delegates. /// + /// The parsed per-tool args, forwarded to every body. /// Wraps an unexpected exception into the per-tool Unhandled*Exception with the given phase name. /// Logs a progress message to the user (typically ConsoleApp.Log from ConsoleAppFramework). - internal GeneratorPhaseRunner(Func wrapUnhandled, Action log) + internal GeneratorPhaseRunner(TArgs args, Func wrapUnhandled, Action log) { + Args = args; _wrapUnhandled = wrapUnhandled; _log = log; } + /// + /// Gets the parsed per-tool args, forwarded to every body. + /// + public TArgs Args { get; } + /// /// Runs , wrapping any unexpected exception in the per-tool /// Unhandled*Exception with as the phase tag. /// /// The phase name used by the per-tool Unhandled*Exception. - /// The body of the phase to run. - public void RunPhase(string phaseName, Action body) + /// The body of the phase to run. The captured is forwarded as its argument. + public void RunPhase(string phaseName, Action body) { try { - body(); + body(Args); } catch (Exception e) when (!e.IsWellKnown) { @@ -67,13 +79,13 @@ public void RunPhase(string phaseName, Action body) /// /// The phase name used by the per-tool Unhandled*Exception. /// The progress message to log before the body runs. - /// The body of the phase to run. - public void RunPhase(string phaseName, string logMessage, Action body) + /// The body of the phase to run. The captured is forwarded as its argument. + public void RunPhase(string phaseName, string logMessage, Action body) { try { _log(logMessage); - body(); + body(Args); } catch (Exception e) when (!e.IsWellKnown) { @@ -87,13 +99,13 @@ public void RunPhase(string phaseName, string logMessage, Action body) /// /// The result type of . /// The phase name used by the per-tool Unhandled*Exception. - /// The body of the phase to run. + /// The body of the phase to run. The captured is forwarded as its argument. /// The value returned by . - public T RunPhase(string phaseName, Func body) + public T RunPhase(string phaseName, Func body) { try { - return body(); + return body(Args); } catch (Exception e) when (!e.IsWellKnown) { @@ -109,14 +121,14 @@ public T RunPhase(string phaseName, Func body) /// The result type of . /// The phase name used by the per-tool Unhandled*Exception. /// The progress message to log before the body runs. - /// The body of the phase to run. + /// The body of the phase to run. The captured is forwarded as its argument. /// The value returned by . - public T RunPhase(string phaseName, string logMessage, Func body) + public T RunPhase(string phaseName, string logMessage, Func body) { try { _log(logMessage); - return body(); + return body(Args); } catch (Exception e) when (!e.IsWellKnown) { @@ -124,3 +136,4 @@ public T RunPhase(string phaseName, string logMessage, Func body) } } } + diff --git a/src/WinRT.Impl.Generator/Generation/ImplGenerator.cs b/src/WinRT.Impl.Generator/Generation/ImplGenerator.cs index 10b86b510c..e855b52c90 100644 --- a/src/WinRT.Impl.Generator/Generation/ImplGenerator.cs +++ b/src/WinRT.Impl.Generator/Generation/ImplGenerator.cs @@ -70,7 +70,7 @@ internal static partial class ImplGenerator /// The token for the operation. public static void Run([Argument] string inputFilePath, CancellationToken token) { - (ImplGeneratorArgs args, GeneratorPhaseRunner runner) = GeneratorHost.Prepare( + GeneratorPhaseRunner runner = GeneratorHost.CreateRunner( inputFilePath: inputFilePath, toolName: "cswinrtimplgen", unpackDebugRepro: UnpackDebugRepro, @@ -83,38 +83,38 @@ public static void Run([Argument] string inputFilePath, CancellationToken token) // Initialize the assembly resolver and load the output module (RuntimeContext runtimeContext, ModuleDefinition outputModule) = runner.RunPhase( phaseName: "loading", - body: () => LoadOutputModule(args)); + body: LoadOutputModule); - args.Token.ThrowIfCancellationRequested(); + runner.Args.Token.ThrowIfCancellationRequested(); // Define the impl module to emit ModuleDefinition implModule = runner.RunPhase( phaseName: "loading", - body: () => DefineImplModule(runtimeContext, outputModule)); + body: _ => DefineImplModule(runtimeContext, outputModule)); - args.Token.ThrowIfCancellationRequested(); + runner.Args.Token.ThrowIfCancellationRequested(); // Emit all necessary IL code in the impl module - runner.RunPhase(phaseName: "generation", body: () => + runner.RunPhase(phaseName: "generation", body: _ => { EmitAssemblyAttributes(outputModule, implModule); EmitTypeForwards(outputModule, implModule); }); - args.Token.ThrowIfCancellationRequested(); + runner.Args.Token.ThrowIfCancellationRequested(); // Write the module to disk with all the generated contents runner.RunPhase( phaseName: "emit", - body: () => WriteImplModuleToDisk(args, outputModule, implModule)); + body: args => WriteImplModuleToDisk(args, outputModule, implModule)); // Signs the module on disk, if needed runner.RunPhase( phaseName: "sign", - body: () => SignImplModuleOnDisk(args, outputModule)); + body: args => SignImplModuleOnDisk(args, outputModule)); // Notify the user that generation was successful - ConsoleApp.Log($"Impl code generated -> {Path.Combine(args.GeneratedAssemblyDirectory, implModule.Name!)}"); + ConsoleApp.Log($"Impl code generated -> {Path.Combine(runner.Args.GeneratedAssemblyDirectory, implModule.Name!)}"); } /// diff --git a/src/WinRT.Interop.Generator/Generation/InteropGenerator.cs b/src/WinRT.Interop.Generator/Generation/InteropGenerator.cs index 8e3631caf5..b2298b6089 100644 --- a/src/WinRT.Interop.Generator/Generation/InteropGenerator.cs +++ b/src/WinRT.Interop.Generator/Generation/InteropGenerator.cs @@ -23,7 +23,7 @@ internal static partial class InteropGenerator /// The token for the operation. public static void Run([Argument] string inputFilePath, CancellationToken token) { - (InteropGeneratorArgs args, GeneratorPhaseRunner runner) = GeneratorHost.Prepare( + GeneratorPhaseRunner runner = GeneratorHost.CreateRunner( inputFilePath: inputFilePath, toolName: "cswinrtinteropgen", unpackDebugRepro: UnpackDebugRepro, @@ -36,18 +36,18 @@ public static void Run([Argument] string inputFilePath, CancellationToken token) // Discover the types to process InteropGeneratorDiscoveryState discoveryState = runner.RunPhase( phaseName: "discovery", - logMessage: $"Processing {args.ReferenceAssemblyPaths.Length + args.ImplementationAssemblyPaths.Length + 1} module(s)", - body: () => Discover(args)); + logMessage: $"Processing {runner.Args.ReferenceAssemblyPaths.Length + runner.Args.ImplementationAssemblyPaths.Length + 1} module(s)", + body: Discover); - args.Token.ThrowIfCancellationRequested(); + runner.Args.Token.ThrowIfCancellationRequested(); // Emit the resulting interop assembly runner.RunPhase( phaseName: "emit", logMessage: "Generating interop code", - body: () => Emit(args, discoveryState)); + body: args => Emit(args, discoveryState)); // Notify the user that generation was successful - ConsoleApp.Log($"Interop code generated -> {Path.Combine(args.GeneratedAssemblyDirectory, InteropNames.WindowsRuntimeInteropDllName)}"); + ConsoleApp.Log($"Interop code generated -> {Path.Combine(runner.Args.GeneratedAssemblyDirectory, InteropNames.WindowsRuntimeInteropDllName)}"); } } \ No newline at end of file diff --git a/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.cs b/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.cs index 658d491144..6c9f164f65 100644 --- a/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.cs +++ b/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.cs @@ -22,7 +22,7 @@ internal static partial class ProjectionGenerator /// The token for the operation. public static void Run([Argument] string inputFilePath, CancellationToken token) { - (ProjectionGeneratorArgs args, GeneratorPhaseRunner runner) = GeneratorHost.Prepare( + GeneratorPhaseRunner runner = GeneratorHost.CreateRunner( inputFilePath: inputFilePath, toolName: "cswinrtprojectiongen", unpackDebugRepro: UnpackDebugRepro, @@ -37,15 +37,15 @@ public static void Run([Argument] string inputFilePath, CancellationToken token) // this tool might run up to 3 times during builds, so this helps make things clearer. ProjectionGeneratorProcessingState processingState = runner.RunPhase( phaseName: "processing", - logMessage: args switch + logMessage: runner.Args switch { { WindowsSdkOnly: true, WindowsUIXamlProjection: false } => "Processing Windows SDK .winmd references", { WindowsSdkOnly: true, WindowsUIXamlProjection: true } => "Processing 'Windows.UI.Xaml' .winmd references", - _ => $"Processing {args.WinMDPaths.Length} .winmd reference(s)" + _ => $"Processing {runner.Args.WinMDPaths.Length} .winmd reference(s)" }, - body: () => ProcessReferences(args)); + body: ProcessReferences); - args.Token.ThrowIfCancellationRequested(); + runner.Args.Token.ThrowIfCancellationRequested(); // If no types were found to project (e.g., component mode with no component references), // skip the source generation and emit phases entirely (no .dll will be produced at all). @@ -58,17 +58,17 @@ public static void Run([Argument] string inputFilePath, CancellationToken token) runner.RunPhase( phaseName: "source-generation", logMessage: "Generating projection code", - body: () => GenerateSources(processingState)); + body: _ => GenerateSources(processingState)); - args.Token.ThrowIfCancellationRequested(); + runner.Args.Token.ThrowIfCancellationRequested(); // Invoke Roslyn to compile the generated sources into 'WinRT.Projection.dll' runner.RunPhase( phaseName: "emit", logMessage: "Compiling projection code", - body: () => Emit(args, processingState)); + body: args => Emit(args, processingState)); // Notify the user that generation was successful - ConsoleApp.Log($"Projection code generated -> {Path.Combine(args.GeneratedAssemblyDirectory, args.AssemblyName)}.dll"); + ConsoleApp.Log($"Projection code generated -> {Path.Combine(runner.Args.GeneratedAssemblyDirectory, runner.Args.AssemblyName)}.dll"); } } \ No newline at end of file diff --git a/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGenerator.cs b/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGenerator.cs index 505f2d4abe..ad6d877859 100644 --- a/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGenerator.cs +++ b/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGenerator.cs @@ -29,7 +29,7 @@ internal static partial class ReferenceProjectionGenerator /// The token for the operation. public static void Run([Argument] string inputFilePath, CancellationToken token) { - (ReferenceProjectionGeneratorArgs args, GeneratorPhaseRunner runner) = GeneratorHost.Prepare( + GeneratorPhaseRunner runner = GeneratorHost.CreateRunner( inputFilePath: inputFilePath, toolName: "cswinrtprojectionrefgen", unpackDebugRepro: UnpackDebugRepro, @@ -40,17 +40,17 @@ public static void Run([Argument] string inputFilePath, CancellationToken token) token: token); // Validate the target framework. CsWinRT 3.0 requires .NET 10 or later. - if (!string.IsNullOrEmpty(args.TargetFramework) && !args.TargetFramework.StartsWith("net10.0", StringComparison.Ordinal)) + if (!string.IsNullOrEmpty(runner.Args.TargetFramework) && !runner.Args.TargetFramework.StartsWith("net10.0", StringComparison.Ordinal)) { - throw WellKnownReferenceProjectionGeneratorExceptions.UnsupportedTargetFramework(args.TargetFramework); + throw WellKnownReferenceProjectionGeneratorExceptions.UnsupportedTargetFramework(runner.Args.TargetFramework); } // Build the writer options from the parsed arguments ProjectionWriterOptions options = runner.RunPhase( phaseName: "processing", - body: () => BuildWriterOptions(args)); + body: BuildWriterOptions); - args.Token.ThrowIfCancellationRequested(); + runner.Args.Token.ThrowIfCancellationRequested(); // Invoke the projection writer (in-process) to generate the projection sources. We can't // route this through the shared 'runner.RunPhase' helper because we wrap the exception diff --git a/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.cs b/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.cs index 293858a395..298a1c683e 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.cs @@ -37,7 +37,7 @@ internal static partial class WinMDGenerator /// The token for the operation. public static void Run([Argument] string inputFilePath, CancellationToken token) { - (WinMDGeneratorArgs args, GeneratorPhaseRunner runner) = GeneratorHost.Prepare( + GeneratorPhaseRunner runner = GeneratorHost.CreateRunner( inputFilePath: inputFilePath, toolName: "cswinrtwinmdgen", unpackDebugRepro: UnpackDebugRepro, @@ -50,8 +50,8 @@ public static void Run([Argument] string inputFilePath, CancellationToken token) // Discover the types to process WinMDGeneratorDiscoveryState discoveryState = runner.RunPhase( phaseName: "discovery", - logMessage: $"Processing assembly: '{System.IO.Path.GetFileName(args.InputAssemblyPath)}'", - body: () => Discover(args)); + logMessage: $"Processing assembly: '{System.IO.Path.GetFileName(runner.Args.InputAssemblyPath)}'", + body: Discover); token.ThrowIfCancellationRequested(); @@ -59,8 +59,8 @@ public static void Run([Argument] string inputFilePath, CancellationToken token) runner.RunPhase( phaseName: "generation", logMessage: $"Defining {discoveryState.PublicTypes.Count} authored type(s)", - body: () => Generate(args, discoveryState)); + body: args => Generate(args, discoveryState)); - ConsoleApp.Log($"Windows Runtime assembly (.winmd) generated -> {args.OutputWinmdPath}"); + ConsoleApp.Log($"Windows Runtime assembly (.winmd) generated -> {runner.Args.OutputWinmdPath}"); } } \ No newline at end of file From 9e815647e919ece0a6690caa76b3ab97744e42ba Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 9 Jun 2026 14:06:06 -0700 Subject: [PATCH 09/15] Simplify response-file parsing and messages Refactor response-file parsing and related generator messaging: - Consolidate and simplify WellKnownGeneratorMessages (make ResponseFileReadError a constant and clean up XML docs) and update per-tool exception factories to use the new message form. - Restructure ResponseFileParser: split line parsing into a map builder and a Populate method, adjust DynamicallyAccessedMembers annotations, remove nullable-inspection/NullabilityInfo usage, and simplify default-application logic. - Tidy ResponseFileBuilder formatting and annotations; minor comment and formatting cleanups. - Remove an unused PathExtensions.Normalize(ReadOnlySpan) overload and add small GeneratorHost/GeneratorPhaseRunner code cleanups (target-typed new, pragma for warnings, reorder assignments). These changes reduce duplication, simplify trimming/reflection annotations for the linker, and clarify parsing behavior without changing external behavior. --- .../DebugRepro/DebugReproPacker.cs | 10 +- .../Errors/IGeneratorErrorFactory.cs | 14 +-- .../Errors/WellKnownGeneratorMessages.cs | 35 ++----- .../Extensions/PathExtensions.cs | 15 --- src/WinRT.Generator.Core/GeneratorHost.cs | 27 +---- .../GeneratorPhaseRunner.cs | 44 ++------- src/WinRT.Generator.Core/IGeneratorArgs.cs | 6 +- .../Parsing/ResponseFileBuilder.cs | 44 ++------- .../Parsing/ResponseFileParser.cs | 98 ++++--------------- .../References/WellKnownPublicKeyTokens.cs | 5 +- .../References/WellKnownPublicKeys.cs | 6 +- .../Errors/WellKnownImplExceptions.cs | 2 +- .../Errors/WellKnownInteropExceptions.cs | 2 +- .../WellKnownProjectionGeneratorExceptions.cs | 2 +- ...nReferenceProjectionGeneratorExceptions.cs | 2 +- .../Errors/WellKnownWinMDExceptions.cs | 2 +- 16 files changed, 63 insertions(+), 251 deletions(-) diff --git a/src/WinRT.Generator.Core/DebugRepro/DebugReproPacker.cs b/src/WinRT.Generator.Core/DebugRepro/DebugReproPacker.cs index d706c93b87..ba0da678e9 100644 --- a/src/WinRT.Generator.Core/DebugRepro/DebugReproPacker.cs +++ b/src/WinRT.Generator.Core/DebugRepro/DebugReproPacker.cs @@ -19,14 +19,8 @@ namespace WindowsRuntime.Generator.DebugRepro; /// Leaf helpers shared across the CsWinRT CLI generators for packaging and unpacking debug repros. /// /// -/// Each generator's SaveDebugRepro/UnpackDebugRepro still owns the high-level orchestration -/// (which input categories exist, which subfolders are used, how the response file is re-stitched). This -/// type only owns the small, identical-across-tools building blocks: -/// -/// Hashing the original file path into a stable, collision-free file name. -/// Copying files (or a single file) into a destination directory using the hashed names. -/// Serializing / deserializing the "hashed name → original path" mapping as JSON inside the repro archive. -/// +/// Each generator still owns the high-level orchestration (which input categories exist, which subfolders are +/// used, how the response file is re-stitched). This type only owns identical helpers that can be reused. /// internal static class DebugReproPacker { diff --git a/src/WinRT.Generator.Core/Errors/IGeneratorErrorFactory.cs b/src/WinRT.Generator.Core/Errors/IGeneratorErrorFactory.cs index 21f31ef861..8906ab8c63 100644 --- a/src/WinRT.Generator.Core/Errors/IGeneratorErrorFactory.cs +++ b/src/WinRT.Generator.Core/Errors/IGeneratorErrorFactory.cs @@ -9,18 +9,10 @@ namespace WindowsRuntime.Generator.Errors; /// Routes shared logical errors through the per-tool well-known exception factory. /// /// -/// /// Shared infrastructure (response-file parsing, debug-repro packing, etc.) is generic over an -/// implementation of this interface and reaches the per-tool factory through its static abstract -/// members. This preserves per-tool exception identity exactly: each factory continues to assign its -/// own numeric error IDs, format its own messages (including embedded tool names), and construct -/// its own concrete subtype. -/// -/// -/// Implementations must be sealed (not static) so they can participate in the -/// static abstract interface contract. The implementing type is not meant to be instantiated; -/// it is used as a type parameter to dispatch the factory call. -/// +/// implementation of this interface to preserve per-tool exception identity exactly: each factory +/// continues to assign its own numeric error IDs, format its own messages (including embedded tool names), +/// and construct its own concrete subtype. /// internal interface IGeneratorErrorFactory { diff --git a/src/WinRT.Generator.Core/Errors/WellKnownGeneratorMessages.cs b/src/WinRT.Generator.Core/Errors/WellKnownGeneratorMessages.cs index 8404f0a29b..03e5bee363 100644 --- a/src/WinRT.Generator.Core/Errors/WellKnownGeneratorMessages.cs +++ b/src/WinRT.Generator.Core/Errors/WellKnownGeneratorMessages.cs @@ -14,56 +14,35 @@ namespace WindowsRuntime.Generator.Errors; /// internal static class WellKnownGeneratorMessages { - /// - /// Builds the message for . - /// - /// The CLI tool name embedded in the message (e.g. "cswinrtimplgen"). - /// The formatted error message. - public static string ResponseFileReadError(string toolName) - { - return $"Failed to read the response file to run '{toolName}'."; - } + /// + public const string ResponseFileReadError = "Failed to read the response file (e.g. it may be missing or not accessible)."; - /// - /// Builds the message for . - /// + /// /// The name of the response-file argument that failed to parse. - /// The formatted error message. public static string ResponseFileArgumentParsingError(string argumentName) { return $"Failed to parse argument '{argumentName}' from response file."; } - /// - /// The message for . - /// + /// public const string MalformedResponseFile = "The response file is malformed and contains invalid content."; - /// - /// Builds the message for . - /// + /// /// The directory path that does not exist. - /// The formatted error message. public static string DebugReproDirectoryDoesNotExist(string path) { return $"The debug repro directory '{path}' does not exist."; } - /// - /// Builds the message for . - /// + /// /// The debug-repro file entry path that has no mapping. - /// The formatted error message. public static string DebugReproMissingFileEntryMapping(string path) { return $"The debug repro file entry with path '{path}' is missing its assembly path mapping."; } - /// - /// Builds the message for . - /// + /// /// The debug-repro file entry path that was not recognized. - /// The formatted error message. public static string DebugReproUnrecognizedFileEntry(string path) { return $"The debug repro file entry with path '{path}' was not recognized."; diff --git a/src/WinRT.Generator.Core/Extensions/PathExtensions.cs b/src/WinRT.Generator.Core/Extensions/PathExtensions.cs index b4afd3b725..fcb37d6130 100644 --- a/src/WinRT.Generator.Core/Extensions/PathExtensions.cs +++ b/src/WinRT.Generator.Core/Extensions/PathExtensions.cs @@ -36,21 +36,6 @@ internal static class PathExtensions return path?.Replace('\\', '/'); } - /// - public static ReadOnlySpan Normalize(ReadOnlySpan path) - { - if (OperatingSystem.IsWindows()) - { - return path; - } - - char[] buffer = new char[path.Length]; - - path.Replace(buffer, '\\', '/'); - - return buffer; - } - /// /// Checks whether a given path represents a file or folder contained within a folder with a given name. /// diff --git a/src/WinRT.Generator.Core/GeneratorHost.cs b/src/WinRT.Generator.Core/GeneratorHost.cs index e8f543eeaf..27df49e2d1 100644 --- a/src/WinRT.Generator.Core/GeneratorHost.cs +++ b/src/WinRT.Generator.Core/GeneratorHost.cs @@ -11,30 +11,10 @@ namespace WindowsRuntime.Generator; /// /// Shared Run entry-point scaffold for the CsWinRT CLI generators. /// -/// -/// Each generator's Run method historically opened with an identical preamble: -/// -/// If the input file path looks like a .zip, unpack a debug repro and re-route to the extracted .rsp. -/// Parse the response file into a per-tool args record. -/// If DebugReproDirectory is set and we are not already replaying, save a debug repro of the current invocation. -/// -/// encapsulates that preamble. Each generator's Run now starts with -/// a single call to it; the per-tool unpack / save / parse logic is supplied via delegates so behavior stays -/// identical (same log messages, same exception phases, same per-tool unhandled exception type). -/// -/// The same Run methods then proceed through a series of phases (loading, processing, emit, ...), -/// each wrapped in an identical try/catch that re-throws as the per-tool -/// Unhandled*Exception. returns a -/// bound to the parsed args plus the same per-tool -/// wrapUnhandled and log delegates, so each phase can be expressed as a single -/// call instead of a -/// hand-written try/catch. -/// -/// internal static class GeneratorHost { /// - /// Runs the shared unpack → parse → save preamble for a CsWinRT CLI generator and returns a + /// Runs the shared unpack, parse, save preamble for a CsWinRT CLI generator and returns a /// ready to drive the remaining per-tool phases. /// /// The per-tool args record (must implement ). @@ -122,9 +102,6 @@ public static GeneratorPhaseRunner CreateRunner( args.Token.ThrowIfCancellationRequested(); - return new GeneratorPhaseRunner(args, wrapUnhandled, log); + return new(args, wrapUnhandled, log); } } - - - diff --git a/src/WinRT.Generator.Core/GeneratorPhaseRunner.cs b/src/WinRT.Generator.Core/GeneratorPhaseRunner.cs index cc151243d6..af038e4428 100644 --- a/src/WinRT.Generator.Core/GeneratorPhaseRunner.cs +++ b/src/WinRT.Generator.Core/GeneratorPhaseRunner.cs @@ -4,6 +4,8 @@ using System; using WindowsRuntime.Generator.Errors; +#pragma warning disable CS1573 + namespace WindowsRuntime.Generator; /// @@ -11,18 +13,6 @@ namespace WindowsRuntime.Generator; /// Unhandled*Exception and optionally logging a progress message before the body runs. /// /// The per-tool args record (must implement ). -/// -/// Each generator's Run method historically wrapped every phase (loading, processing, emit, ...) -/// in an identical try/catch that re-threw as the per-tool Unhandled*Exception -/// (with the phase name as the constructor argument). captures -/// the parsed instance plus the per-tool wrapUnhandled and log -/// delegates (it is returned bound to all three by ) and -/// lets each phase call site collapse to a single (or -/// overload) invocation. The captured is forwarded to every body delegate, -/// so phases that only depend on it can be expressed as a static lambda (or method group) with -/// zero per-call allocations. Per-tool exception identity is fully preserved because the original -/// wrapUnhandled delegate is invoked unchanged. -/// internal readonly struct GeneratorPhaseRunner where TArgs : IGeneratorArgs { @@ -44,9 +34,10 @@ internal readonly struct GeneratorPhaseRunner /// Logs a progress message to the user (typically ConsoleApp.Log from ConsoleAppFramework). internal GeneratorPhaseRunner(TArgs args, Func wrapUnhandled, Action log) { - Args = args; _wrapUnhandled = wrapUnhandled; _log = log; + + Args = args; } /// @@ -72,19 +63,14 @@ public void RunPhase(string phaseName, Action body) } } - /// - /// Logs and then runs , wrapping any - /// unexpected exception in the per-tool Unhandled*Exception with - /// as the phase tag. - /// - /// The phase name used by the per-tool Unhandled*Exception. + /// /// The progress message to log before the body runs. - /// The body of the phase to run. The captured is forwarded as its argument. public void RunPhase(string phaseName, string logMessage, Action body) { try { _log(logMessage); + body(Args); } catch (Exception e) when (!e.IsWellKnown) @@ -93,13 +79,7 @@ public void RunPhase(string phaseName, string logMessage, Action body) } } - /// - /// Runs and returns its result, wrapping any unexpected exception - /// in the per-tool Unhandled*Exception with as the phase tag. - /// - /// The result type of . - /// The phase name used by the per-tool Unhandled*Exception. - /// The body of the phase to run. The captured is forwarded as its argument. + /// /// The value returned by . public T RunPhase(string phaseName, Func body) { @@ -113,21 +93,15 @@ public T RunPhase(string phaseName, Func body) } } - /// - /// Logs , then runs and returns its result, - /// wrapping any unexpected exception in the per-tool Unhandled*Exception with - /// as the phase tag. - /// - /// The result type of . - /// The phase name used by the per-tool Unhandled*Exception. + /// /// The progress message to log before the body runs. - /// The body of the phase to run. The captured is forwarded as its argument. /// The value returned by . public T RunPhase(string phaseName, string logMessage, Func body) { try { _log(logMessage); + return body(Args); } catch (Exception e) when (!e.IsWellKnown) diff --git a/src/WinRT.Generator.Core/IGeneratorArgs.cs b/src/WinRT.Generator.Core/IGeneratorArgs.cs index 34c2a1be87..f6bf0ed5c8 100644 --- a/src/WinRT.Generator.Core/IGeneratorArgs.cs +++ b/src/WinRT.Generator.Core/IGeneratorArgs.cs @@ -6,14 +6,12 @@ namespace WindowsRuntime.Generator; /// -/// Common surface implemented by every per-tool args record (e.g. ImplGeneratorArgs, -/// InteropGeneratorArgs) so the shared entry-point scaffold -/// can dispatch through a couple of well-known properties. +/// Common surface implemented by every per-tool arguments record. /// internal interface IGeneratorArgs { /// - /// Gets the token for the operation. + /// Gets the cancellation token for the generator invocation. /// CancellationToken Token { get; } diff --git a/src/WinRT.Generator.Core/Parsing/ResponseFileBuilder.cs b/src/WinRT.Generator.Core/Parsing/ResponseFileBuilder.cs index 00e2699e39..fa90f69710 100644 --- a/src/WinRT.Generator.Core/Parsing/ResponseFileBuilder.cs +++ b/src/WinRT.Generator.Core/Parsing/ResponseFileBuilder.cs @@ -8,51 +8,22 @@ using System.Reflection; using System.Runtime.CompilerServices; using System.Text; -using System.Threading; using WindowsRuntime.Generator.Attributes; namespace WindowsRuntime.Generator.Parsing; /// -/// Formats an args record into a response file by reflecting over its -/// []-annotated public properties. +/// Helper to format argument objects into response files. /// -/// -/// -/// Properties are emitted in declaration order (the order returned by ), -/// one name]]>value per line. The output round-trips cleanly through -/// . -/// -/// -/// Per-property handling: -/// -/// -typed properties are skipped (they have no CLI surface). -/// Properties without are skipped. -/// values are skipped (they round-trip as "missing"). -/// Empty [] values are skipped (they round-trip as "missing" -/// to the optional-array default, matching the previous per-tool emit behavior). -/// values that are on properties without -/// are skipped (they -/// round-trip to the optional-bool default, matching the previous per-tool emit behavior). -/// All other values are formatted using : -/// strings emit as-is, arrays emit as comma-joined, primitives use . -/// -/// -/// internal static class ResponseFileBuilder { - /// The required dynamic-access annotation for the TArgs type parameter. - private const DynamicallyAccessedMemberTypes ArgsAccessKinds = - DynamicallyAccessedMemberTypes.PublicProperties; - /// - /// Formats as a response file string suitable for - /// . + /// Formats as a response file string. /// /// The strongly-typed args record. /// The args instance to format. /// The formatted response file text. - public static string Format<[DynamicallyAccessedMembers(ArgsAccessKinds)] TArgs>(TArgs args) + public static string Format<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] TArgs>(TArgs args) where TArgs : class { StringBuilder builder = new(); @@ -83,8 +54,8 @@ internal static class ResponseFileBuilder } // Optional booleans default to 'false' on parse, so emitting "name false" would be - // redundant. Required booleans always emit (both 'true' and 'false') so the parser sees - // them and doesn't reject them as missing-required. + // redundant. Required booleans always emit (both 'true' and 'false') so the parser + // sees them and doesn't reject them as missing-required. bool isRequired = property.GetCustomAttribute() is not null; if (value is false && !isRequired) @@ -112,13 +83,14 @@ private static string FormatValue(object value) return s; } + // String values are formatted as comma-separated lists, without brackets or spaces if (value is string[] array) { return string.Join(',', array); } - // All other primitive values use invariant culture, matching the parser's invariant - // 'Convert.ChangeType' so the round-trip is deterministic. + // All other primitive values use invariant culture, matching the parser's + // invariant 'Convert.ChangeType' so the round-trip is deterministic. return Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty; } } diff --git a/src/WinRT.Generator.Core/Parsing/ResponseFileParser.cs b/src/WinRT.Generator.Core/Parsing/ResponseFileParser.cs index f635d005de..25a2bc7a6b 100644 --- a/src/WinRT.Generator.Core/Parsing/ResponseFileParser.cs +++ b/src/WinRT.Generator.Core/Parsing/ResponseFileParser.cs @@ -16,52 +16,25 @@ namespace WindowsRuntime.Generator.Parsing; /// -/// Parses a response file into a strongly-typed args record by reflecting over its -/// []-annotated public properties. +/// Helper to parse response files into argument objects. /// -/// -/// -/// The shape of the response file is one name]]>value per line, with optional -/// blank lines (the MSBuild ToolTask may emit them). The first token of each non-blank line -/// is the CLI flag name; everything after the first space (right-trimmed) is the value. -/// -/// -/// Per-property handling: -/// -/// -typed properties are set from the parser's token -/// parameter (they have no ). -/// Properties without are skipped. -/// Required properties (those with ) throw -/// -/// if the value is missing or fails to parse. -/// Optional properties default to the if -/// is present, otherwise to default(T) (with -/// [] defaulting to an empty array). -/// Value coercion is handled by -/// for primitives, with ; arrays use -/// with comma separator. -/// -/// -/// internal static class ResponseFileParser { - /// The required dynamic-access annotation for the TArgs type parameter. - private const DynamicallyAccessedMemberTypes ArgsAccessKinds = - DynamicallyAccessedMemberTypes.PublicProperties; - /// /// Parses an instance of from a response file at the given path. /// /// - /// The path may be prefixed with @ (matching MSBuild's default escaping for ToolTask - /// response files), which is stripped before reading the file. + /// The path may be prefixed with @ (matching MSBuild's default escaping + /// for ToolTask response files), which is stripped before reading the file. /// /// The strongly-typed args record. Must have a public parameterless constructor surface (only public properties are inspected via reflection). /// The per-tool well-known exception factory used to route parsing errors. /// The path to the response file (optionally prefixed with @). /// The cancellation token for the operation. /// The populated instance. - public static TArgs Parse<[DynamicallyAccessedMembers(ArgsAccessKinds)] TArgs, TErr>(string path, CancellationToken token) + public static TArgs Parse<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.AllConstructors | DynamicallyAccessedMemberTypes.PublicProperties)] TArgs, TErr>( + string path, + CancellationToken token) where TArgs : class where TErr : IGeneratorErrorFactory { @@ -84,7 +57,9 @@ internal static class ResponseFileParser throw TErr.ResponseFileReadError(e); } - return ParseLines(lines, token); + Dictionary argsMap = BuildArgsMap(lines); + + return Populate(argsMap, token); } /// @@ -95,25 +70,14 @@ internal static class ResponseFileParser /// The stream containing the response file content. /// The cancellation token for the operation. /// The populated instance. - public static TArgs Parse<[DynamicallyAccessedMembers(ArgsAccessKinds)] TArgs, TErr>(Stream stream, CancellationToken token) + public static TArgs Parse<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.AllConstructors | DynamicallyAccessedMemberTypes.PublicProperties)] TArgs, TErr>( + Stream stream, + CancellationToken token) where TArgs : class where TErr : IGeneratorErrorFactory { - return ParseLines(File.ReadAllLines(stream), token); - } + string[] lines = File.ReadAllLines(stream); - /// - /// Parses an instance of from the pre-split lines of a response file. - /// - /// The strongly-typed args record. - /// The per-tool well-known exception factory used to route parsing errors. - /// The lines read from the response file. - /// The cancellation token for the operation. - /// The populated instance. - public static TArgs ParseLines<[DynamicallyAccessedMembers(ArgsAccessKinds)] TArgs, TErr>(string[] lines, CancellationToken token) - where TArgs : class - where TErr : IGeneratorErrorFactory - { Dictionary argsMap = BuildArgsMap(lines); return Populate(argsMap, token); @@ -164,20 +128,16 @@ private static Dictionary BuildArgsMap(string[] lines) } /// - /// Constructs via - /// (to bypass enforcement at construction time) and then - /// populates each public property by reflecting on its CLI attribute, nullability, default value, and type. + /// Populates an arguments object with the provided parsed values. /// /// The strongly-typed args record. /// The per-tool well-known exception factory used to route parsing errors. /// The pre-built name-to-value map. /// The cancellation token, assigned to any -typed property on . /// The populated instance. - [UnconditionalSuppressMessage( - "Trimming", - "IL2087:'type' argument does not satisfy 'DynamicallyAccessedMembersAttribute' in call to target method.", - Justification = "GetUninitializedObject does not invoke any constructor; it allocates an instance of TArgs without running any user code, so the PublicConstructors/NonPublicConstructors annotation is not required.")] - private static TArgs Populate<[DynamicallyAccessedMembers(ArgsAccessKinds)] TArgs, TErr>(Dictionary argsMap, CancellationToken token) + private static TArgs Populate<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.AllConstructors | DynamicallyAccessedMemberTypes.PublicProperties)] TArgs, TErr>( + Dictionary argsMap, + CancellationToken token) where TArgs : class where TErr : IGeneratorErrorFactory { @@ -186,8 +146,6 @@ private static Dictionary BuildArgsMap(string[] lines) // response file values, with explicit defaults applied for non-required properties. TArgs instance = (TArgs)RuntimeHelpers.GetUninitializedObject(typeof(TArgs)); - NullabilityInfoContext nullabilityContext = new(); - foreach (PropertyInfo property in typeof(TArgs).GetProperties(BindingFlags.Public | BindingFlags.Instance)) { Type propertyType = property.PropertyType; @@ -214,7 +172,7 @@ private static Dictionary BuildArgsMap(string[] lines) if (!hasValue) { - ApplyDefault(instance, property, propertyType, isRequired, nullabilityContext); + ApplyDefault(instance, property, propertyType, isRequired); continue; } @@ -233,7 +191,7 @@ private static Dictionary BuildArgsMap(string[] lines) } else { - ApplyDefault(instance, property, propertyType, isRequired: false, nullabilityContext); + ApplyDefault(instance, property, propertyType, isRequired: false); } } @@ -248,13 +206,11 @@ private static Dictionary BuildArgsMap(string[] lines) /// The property being set. /// The property's type. /// Whether the property has . - /// The shared for nullable-reference inspection. private static void ApplyDefault( object instance, PropertyInfo property, Type propertyType, - bool isRequired, - NullabilityInfoContext nullabilityContext) + bool isRequired) where TErr : IGeneratorErrorFactory { if (isRequired) @@ -263,7 +219,7 @@ private static void ApplyDefault( } // '[DefaultValue("…")]' takes precedence: it lets per-tool args express initializer-style - // defaults that 'GetUninitializedObject' would otherwise skip. + // defaults that 'GetUninitializedObject' would otherwise skip (initializers aren't needed). DefaultValueAttribute? defaultValueAttribute = property.GetCustomAttribute(); if (defaultValueAttribute is not null) @@ -280,18 +236,6 @@ private static void ApplyDefault( return; } - - // Nullable reference types default to null (matching 'GetNullableStringArgument'). For value - // types and non-nullable reference types we leave 'default(T)' from 'GetUninitializedObject'. - if (!propertyType.IsValueType) - { - NullabilityInfo nullability = nullabilityContext.Create(property); - - if (nullability.ReadState == NullabilityState.Nullable) - { - property.SetValue(instance, null); - } - } } /// diff --git a/src/WinRT.Generator.Core/References/WellKnownPublicKeyTokens.cs b/src/WinRT.Generator.Core/References/WellKnownPublicKeyTokens.cs index 7247409794..0942d8ec00 100644 --- a/src/WinRT.Generator.Core/References/WellKnownPublicKeyTokens.cs +++ b/src/WinRT.Generator.Core/References/WellKnownPublicKeyTokens.cs @@ -4,13 +4,12 @@ namespace WindowsRuntime.Generator.References; /// -/// Well known public key tokens for the framework and CsWinRT assemblies referenced by the -/// CsWinRT CLI generators. +/// Well-known public key tokens for assemblies used by the CsWinRT generators. /// internal static class WellKnownPublicKeyTokens { /// - /// The public key token for mscorlib (b77a5c561934e089). + /// The public key token for mscorlib. /// public static readonly byte[] MSCorLib = [0xB7, 0x7A, 0x5C, 0x56, 0x19, 0x34, 0xE0, 0x89]; diff --git a/src/WinRT.Generator.Core/References/WellKnownPublicKeys.cs b/src/WinRT.Generator.Core/References/WellKnownPublicKeys.cs index 73e159760b..1c755b9d73 100644 --- a/src/WinRT.Generator.Core/References/WellKnownPublicKeys.cs +++ b/src/WinRT.Generator.Core/References/WellKnownPublicKeys.cs @@ -4,16 +4,14 @@ namespace WindowsRuntime.Generator.References; /// -/// The 160-byte strong-name public keys consumed by the CsWinRT CLI generators when emitting -/// assembly references or signing generated assemblies. +/// Well-known public keys for assemblies used by the CsWinRT generators. /// internal static class WellKnownPublicKeys { /// /// The public key used by the precompiled Windows SDK projection assemblies (WinRT.Sdk.Projection.dll, /// WinRT.Sdk.Xaml.Projection.dll) and the merged projection assembly (WinRT.Projection.dll) that - /// the projection generator produces at app build time. The impl generator stamps this same key on the - /// AssemblyReference entries it emits in the forwarder .dll. + /// the projection generator produces at app build time. /// public static readonly byte[] WindowsSdkProjection = [0x00, 0x24, 0x00, 0x00, 0x04, 0x80, 0x00, 0x00, 0x94, 0x00, 0x00, 0x00, 0x06, 0x02, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x52, 0x53, 0x41, 0x31, 0x00, 0x04, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0xB5, 0xFC, 0x90, 0xE7, 0x02, 0x7F, 0x67, 0x87, 0x1E, 0x77, 0x3A, 0x8F, 0xDE, 0x89, 0x38, 0xC8, 0x1D, 0xD4, 0x02, 0xBA, 0x65, 0xB9, 0x20, 0x1D, 0x60, 0x59, 0x3E, 0x96, 0xC4, 0x92, 0x65, 0x1E, 0x88, 0x9C, 0xC1, 0x3F, 0x14, 0x15, 0xEB, 0xB5, 0x3F, 0xAC, 0x11, 0x31, 0xAE, 0x0B, 0xD3, 0x33, 0xC5, 0xEE, 0x60, 0x21, 0x67, 0x2D, 0x97, 0x18, 0xEA, 0x31, 0xA8, 0xAE, 0xBD, 0x0D, 0xA0, 0x07, 0x2F, 0x25, 0xD8, 0x7D, 0xBA, 0x6F, 0xC9, 0x0F, 0xFD, 0x59, 0x8E, 0xD4, 0xDA, 0x35, 0xE4, 0x4C, 0x39, 0x8C, 0x45, 0x43, 0x07, 0xE8, 0xE3, 0x3B, 0x84, 0x26, 0x14, 0x3D, 0xAE, 0xC9, 0xF5, 0x96, 0x83, 0x6F, 0x97, 0xC8, 0xF7, 0x47, 0x50, 0xE5, 0x97, 0x5C, 0x64, 0xE2, 0x18, 0x9F, 0x45, 0xDE, 0xF4, 0x6B, 0x2A, 0x2B, 0x12, 0x47, 0xAD, 0xC3, 0x65, 0x2B, 0xF5, 0xC3, 0x08, 0x05, 0x5D, 0xA9]; diff --git a/src/WinRT.Impl.Generator/Errors/WellKnownImplExceptions.cs b/src/WinRT.Impl.Generator/Errors/WellKnownImplExceptions.cs index ef3a92458c..ab965be045 100644 --- a/src/WinRT.Impl.Generator/Errors/WellKnownImplExceptions.cs +++ b/src/WinRT.Impl.Generator/Errors/WellKnownImplExceptions.cs @@ -26,7 +26,7 @@ private WellKnownImplExceptions() /// public static Exception ResponseFileReadError(Exception exception) { - return Exception(1, WellKnownGeneratorMessages.ResponseFileReadError("cswinrtimplgen"), exception); + return Exception(1, WellKnownGeneratorMessages.ResponseFileReadError, exception); } /// diff --git a/src/WinRT.Interop.Generator/Errors/WellKnownInteropExceptions.cs b/src/WinRT.Interop.Generator/Errors/WellKnownInteropExceptions.cs index 6043cc31fd..6df46e720a 100644 --- a/src/WinRT.Interop.Generator/Errors/WellKnownInteropExceptions.cs +++ b/src/WinRT.Interop.Generator/Errors/WellKnownInteropExceptions.cs @@ -287,7 +287,7 @@ public static WellKnownInteropException IDictionary2TypeCodeGenerationError(Type /// public static WellKnownInteropException ResponseFileReadError(Exception exception) { - return Exception(28, WellKnownGeneratorMessages.ResponseFileReadError("cswinrtinteropgen"), exception); + return Exception(28, WellKnownGeneratorMessages.ResponseFileReadError, exception); } /// diff --git a/src/WinRT.Projection.Generator/Errors/WellKnownProjectionGeneratorExceptions.cs b/src/WinRT.Projection.Generator/Errors/WellKnownProjectionGeneratorExceptions.cs index 4bde0b5e20..4ac475f319 100644 --- a/src/WinRT.Projection.Generator/Errors/WellKnownProjectionGeneratorExceptions.cs +++ b/src/WinRT.Projection.Generator/Errors/WellKnownProjectionGeneratorExceptions.cs @@ -29,7 +29,7 @@ private WellKnownProjectionGeneratorExceptions() /// public static Exception ResponseFileReadError(Exception exception) { - return Exception(1, WellKnownGeneratorMessages.ResponseFileReadError("cswinrtprojectiongen"), exception); + return Exception(1, WellKnownGeneratorMessages.ResponseFileReadError, exception); } /// diff --git a/src/WinRT.Projection.Ref.Generator/Errors/WellKnownReferenceProjectionGeneratorExceptions.cs b/src/WinRT.Projection.Ref.Generator/Errors/WellKnownReferenceProjectionGeneratorExceptions.cs index 9e232becc0..aa3d8014d6 100644 --- a/src/WinRT.Projection.Ref.Generator/Errors/WellKnownReferenceProjectionGeneratorExceptions.cs +++ b/src/WinRT.Projection.Ref.Generator/Errors/WellKnownReferenceProjectionGeneratorExceptions.cs @@ -26,7 +26,7 @@ private WellKnownReferenceProjectionGeneratorExceptions() /// public static Exception ResponseFileReadError(Exception exception) { - return Exception(1, WellKnownGeneratorMessages.ResponseFileReadError("cswinrtprojectionrefgen"), exception); + return Exception(1, WellKnownGeneratorMessages.ResponseFileReadError, exception); } /// diff --git a/src/WinRT.WinMD.Generator/Errors/WellKnownWinMDExceptions.cs b/src/WinRT.WinMD.Generator/Errors/WellKnownWinMDExceptions.cs index 3ec0e57344..d958c0c7a9 100644 --- a/src/WinRT.WinMD.Generator/Errors/WellKnownWinMDExceptions.cs +++ b/src/WinRT.WinMD.Generator/Errors/WellKnownWinMDExceptions.cs @@ -26,7 +26,7 @@ private WellKnownWinMDExceptions() /// public static Exception ResponseFileReadError(Exception exception) { - return Exception(1, WellKnownGeneratorMessages.ResponseFileReadError("cswinrtwinmdgen"), exception); + return Exception(1, WellKnownGeneratorMessages.ResponseFileReadError, exception); } /// From ac53fb5068942c3274677326365614d06c2afb43 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 9 Jun 2026 14:28:46 -0700 Subject: [PATCH 10/15] Standardize generator naming and runner calls Rename the UnhandledGeneratorException property from GeneratorDescription to GeneratorName and update the standardized ToString message to use the new name ("The CsWinRT {GeneratorName} generator..."). Adjust all per-tool unhandled exception types to provide GeneratorName values. Remove explicit generic type args from several GeneratorHost.CreateRunner calls (relying on inference). Also apply minor XML-doc and cref cleanups (simplify exception/type cref qualifications and projection writer cref) and a small doc fix in WindowsRuntimeExceptionExtensions. --- .../Errors/UnhandledGeneratorException.cs | 9 ++++----- .../Errors/UnhandledImplException.cs | 2 +- src/WinRT.Impl.Generator/Generation/ImplGenerator.cs | 2 +- .../Errors/UnhandledInteropException.cs | 2 +- .../Generation/InteropGenerator.cs | 2 +- .../References/WellKnownInterfaceIIDs.cs | 2 +- .../Errors/UnhandledProjectionGeneratorException.cs | 2 +- .../Generation/ProjectionGenerator.cs | 2 +- .../Generation/ProjectionGeneratorProcessingState.cs | 4 ++-- .../UnhandledReferenceProjectionGeneratorException.cs | 2 +- .../Generation/ReferenceProjectionGenerator.cs | 2 +- .../Properties/WindowsRuntimeExceptionExtensions.cs | 2 +- .../Errors/UnhandledWinMDException.cs | 2 +- src/WinRT.WinMD.Generator/Generation/WinMDGenerator.cs | 2 +- 14 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/WinRT.Generator.Core/Errors/UnhandledGeneratorException.cs b/src/WinRT.Generator.Core/Errors/UnhandledGeneratorException.cs index 929afaccc9..3b51d8ad6b 100644 --- a/src/WinRT.Generator.Core/Errors/UnhandledGeneratorException.cs +++ b/src/WinRT.Generator.Core/Errors/UnhandledGeneratorException.cs @@ -10,7 +10,7 @@ namespace WindowsRuntime.Generator.Errors; /// /// /// Each per-tool unhandled exception inherits from this type and provides its -/// and so that the standardized +/// and so that the standardized /// message remains tool-specific. /// /// The phase that failed. @@ -24,16 +24,15 @@ internal abstract class UnhandledGeneratorException(string phase, Exception exce protected abstract string ErrorPrefix { get; } /// - /// Gets the description of the generator used in the standard message - /// (e.g. "impl generator", "interop generator", "WinMD generator"). + /// Gets the name of the generator used in the standard message. /// - protected abstract string GeneratorDescription { get; } + protected abstract string GeneratorName { get; } /// public override string ToString() { return - $"""error {ErrorPrefix}9999: The CsWinRT {GeneratorDescription} failed with an unhandled exception """ + + $"""error {ErrorPrefix}9999: The CsWinRT {GeneratorName} 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."""; diff --git a/src/WinRT.Impl.Generator/Errors/UnhandledImplException.cs b/src/WinRT.Impl.Generator/Errors/UnhandledImplException.cs index 4d1f987fba..af5926b763 100644 --- a/src/WinRT.Impl.Generator/Errors/UnhandledImplException.cs +++ b/src/WinRT.Impl.Generator/Errors/UnhandledImplException.cs @@ -17,5 +17,5 @@ internal sealed class UnhandledImplException(string phase, Exception exception) protected override string ErrorPrefix => WellKnownImplExceptions.ErrorPrefix; /// - protected override string GeneratorDescription => "impl generator"; + protected override string GeneratorName => "impl"; } diff --git a/src/WinRT.Impl.Generator/Generation/ImplGenerator.cs b/src/WinRT.Impl.Generator/Generation/ImplGenerator.cs index e855b52c90..cf6e97c87f 100644 --- a/src/WinRT.Impl.Generator/Generation/ImplGenerator.cs +++ b/src/WinRT.Impl.Generator/Generation/ImplGenerator.cs @@ -70,7 +70,7 @@ internal static partial class ImplGenerator /// The token for the operation. public static void Run([Argument] string inputFilePath, CancellationToken token) { - GeneratorPhaseRunner runner = GeneratorHost.CreateRunner( + GeneratorPhaseRunner runner = GeneratorHost.CreateRunner( inputFilePath: inputFilePath, toolName: "cswinrtimplgen", unpackDebugRepro: UnpackDebugRepro, diff --git a/src/WinRT.Interop.Generator/Errors/UnhandledInteropException.cs b/src/WinRT.Interop.Generator/Errors/UnhandledInteropException.cs index c4b0ee30bf..972d473b14 100644 --- a/src/WinRT.Interop.Generator/Errors/UnhandledInteropException.cs +++ b/src/WinRT.Interop.Generator/Errors/UnhandledInteropException.cs @@ -17,5 +17,5 @@ internal sealed class UnhandledInteropException(string phase, Exception exceptio protected override string ErrorPrefix => WellKnownInteropExceptions.ErrorPrefix; /// - protected override string GeneratorDescription => "interop generator"; + protected override string GeneratorName => "interop"; } diff --git a/src/WinRT.Interop.Generator/Generation/InteropGenerator.cs b/src/WinRT.Interop.Generator/Generation/InteropGenerator.cs index b2298b6089..9b51554ae9 100644 --- a/src/WinRT.Interop.Generator/Generation/InteropGenerator.cs +++ b/src/WinRT.Interop.Generator/Generation/InteropGenerator.cs @@ -23,7 +23,7 @@ internal static partial class InteropGenerator /// The token for the operation. public static void Run([Argument] string inputFilePath, CancellationToken token) { - GeneratorPhaseRunner runner = GeneratorHost.CreateRunner( + GeneratorPhaseRunner runner = GeneratorHost.CreateRunner( inputFilePath: inputFilePath, toolName: "cswinrtinteropgen", unpackDebugRepro: UnpackDebugRepro, diff --git a/src/WinRT.Interop.Generator/References/WellKnownInterfaceIIDs.cs b/src/WinRT.Interop.Generator/References/WellKnownInterfaceIIDs.cs index 4d56b5bdfd..fafda0e0fb 100644 --- a/src/WinRT.Interop.Generator/References/WellKnownInterfaceIIDs.cs +++ b/src/WinRT.Interop.Generator/References/WellKnownInterfaceIIDs.cs @@ -65,7 +65,7 @@ internal static class WellKnownInterfaceIIDs /// Whether to use Windows.UI.Xaml projections. /// The instance to use. /// The for the get_IID_... method for . - /// + /// /// /// The types handled by this method should be kept in sync with /// and diff --git a/src/WinRT.Projection.Generator/Errors/UnhandledProjectionGeneratorException.cs b/src/WinRT.Projection.Generator/Errors/UnhandledProjectionGeneratorException.cs index 75bff109be..da7ea08ff4 100644 --- a/src/WinRT.Projection.Generator/Errors/UnhandledProjectionGeneratorException.cs +++ b/src/WinRT.Projection.Generator/Errors/UnhandledProjectionGeneratorException.cs @@ -17,6 +17,6 @@ internal sealed class UnhandledProjectionGeneratorException(string phase, Except protected override string ErrorPrefix => WellKnownProjectionGeneratorExceptions.ErrorPrefix; /// - protected override string GeneratorDescription => "projection generator"; + protected override string GeneratorName => "projection"; } diff --git a/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.cs b/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.cs index 6c9f164f65..7f871b28d3 100644 --- a/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.cs +++ b/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.cs @@ -22,7 +22,7 @@ internal static partial class ProjectionGenerator /// The token for the operation. public static void Run([Argument] string inputFilePath, CancellationToken token) { - GeneratorPhaseRunner runner = GeneratorHost.CreateRunner( + GeneratorPhaseRunner runner = GeneratorHost.CreateRunner( inputFilePath: inputFilePath, toolName: "cswinrtprojectiongen", unpackDebugRepro: UnpackDebugRepro, diff --git a/src/WinRT.Projection.Generator/Generation/ProjectionGeneratorProcessingState.cs b/src/WinRT.Projection.Generator/Generation/ProjectionGeneratorProcessingState.cs index fd1ede400c..b4c09a58bb 100644 --- a/src/WinRT.Projection.Generator/Generation/ProjectionGeneratorProcessingState.cs +++ b/src/WinRT.Projection.Generator/Generation/ProjectionGeneratorProcessingState.cs @@ -10,7 +10,7 @@ namespace WindowsRuntime.ProjectionGenerator.Generation; /// /// The path to the folder where sources will be generated. /// The reference assembly paths excluding projection assemblies. -/// The options to pass to . +/// The options to pass to . /// Whether any types were found to project. internal sealed class ProjectionGeneratorProcessingState( string sourcesFolder, @@ -29,7 +29,7 @@ internal sealed class ProjectionGeneratorProcessingState( public string[] ReferencesWithoutProjections { get; } = referencesWithoutProjections; /// - /// Gets the options used to invoke . + /// Gets the options used to invoke . /// public ProjectionWriterOptions WriterOptions { get; } = writerOptions; diff --git a/src/WinRT.Projection.Ref.Generator/Errors/UnhandledReferenceProjectionGeneratorException.cs b/src/WinRT.Projection.Ref.Generator/Errors/UnhandledReferenceProjectionGeneratorException.cs index 7c46c60ce8..1bf7f1f132 100644 --- a/src/WinRT.Projection.Ref.Generator/Errors/UnhandledReferenceProjectionGeneratorException.cs +++ b/src/WinRT.Projection.Ref.Generator/Errors/UnhandledReferenceProjectionGeneratorException.cs @@ -17,6 +17,6 @@ internal sealed class UnhandledReferenceProjectionGeneratorException(string phas protected override string ErrorPrefix => WellKnownReferenceProjectionGeneratorExceptions.ErrorPrefix; /// - protected override string GeneratorDescription => "reference projection generator"; + protected override string GeneratorName => "reference projection"; } diff --git a/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGenerator.cs b/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGenerator.cs index ad6d877859..3e7f7026c9 100644 --- a/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGenerator.cs +++ b/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGenerator.cs @@ -29,7 +29,7 @@ internal static partial class ReferenceProjectionGenerator /// The token for the operation. public static void Run([Argument] string inputFilePath, CancellationToken token) { - GeneratorPhaseRunner runner = GeneratorHost.CreateRunner( + GeneratorPhaseRunner runner = GeneratorHost.CreateRunner( inputFilePath: inputFilePath, toolName: "cswinrtprojectionrefgen", unpackDebugRepro: UnpackDebugRepro, diff --git a/src/WinRT.Runtime2/Properties/WindowsRuntimeExceptionExtensions.cs b/src/WinRT.Runtime2/Properties/WindowsRuntimeExceptionExtensions.cs index 06a163f49d..b86a3c1360 100644 --- a/src/WinRT.Runtime2/Properties/WindowsRuntimeExceptionExtensions.cs +++ b/src/WinRT.Runtime2/Properties/WindowsRuntimeExceptionExtensions.cs @@ -1106,7 +1106,7 @@ public static ArgumentException GetInvalidSeekOriginException(string? paramName) extension(UnauthorizedAccessException) { /// - /// Throws an indicating that the internal buffer of a cannot be accessed. + /// Throws an indicating that the internal buffer of a cannot be accessed. /// /// Always thrown. [DoesNotReturn] diff --git a/src/WinRT.WinMD.Generator/Errors/UnhandledWinMDException.cs b/src/WinRT.WinMD.Generator/Errors/UnhandledWinMDException.cs index 7faae43b4c..9002ee8c21 100644 --- a/src/WinRT.WinMD.Generator/Errors/UnhandledWinMDException.cs +++ b/src/WinRT.WinMD.Generator/Errors/UnhandledWinMDException.cs @@ -17,5 +17,5 @@ internal sealed class UnhandledWinMDException(string phase, Exception exception) protected override string ErrorPrefix => WellKnownWinMDExceptions.ErrorPrefix; /// - protected override string GeneratorDescription => "WinMD generator"; + protected override string GeneratorName => "WinMD"; } diff --git a/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.cs b/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.cs index 298a1c683e..188d64c068 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.cs @@ -37,7 +37,7 @@ internal static partial class WinMDGenerator /// The token for the operation. public static void Run([Argument] string inputFilePath, CancellationToken token) { - GeneratorPhaseRunner runner = GeneratorHost.CreateRunner( + GeneratorPhaseRunner runner = GeneratorHost.CreateRunner( inputFilePath: inputFilePath, toolName: "cswinrtwinmdgen", unpackDebugRepro: UnpackDebugRepro, From 55ad790474284dc1808098de7c136d4402374d25 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 9 Jun 2026 14:30:35 -0700 Subject: [PATCH 11/15] Move InternalsVisibleTo declarations from AssemblyInfo.cs to .csproj The 'InternalsVisibleTo' items in 'src/WinRT.Generator.Core/Properties/AssemblyInfo.cs' are emitted by the .NET SDK directly from MSBuild '' items when they're declared in the project file, so there is no need for a hand-written 'AssemblyInfo.cs' to host them. Move the 5 entries (one per consuming generator) into 'WinRT.Generator.Core.csproj' and delete the now-empty 'Properties/AssemblyInfo.cs' (plus its empty parent folder). All 5 generators build with 0 warnings, confirming the SDK-emitted attributes still grant the same internals access. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/WinRT.Generator.Core/Properties/AssemblyInfo.cs | 10 ---------- src/WinRT.Generator.Core/WinRT.Generator.Core.csproj | 8 ++++++++ .../Generation/ReferenceProjectionGenerator.cs | 2 +- 3 files changed, 9 insertions(+), 11 deletions(-) delete mode 100644 src/WinRT.Generator.Core/Properties/AssemblyInfo.cs diff --git a/src/WinRT.Generator.Core/Properties/AssemblyInfo.cs b/src/WinRT.Generator.Core/Properties/AssemblyInfo.cs deleted file mode 100644 index 41f427dd1d..0000000000 --- a/src/WinRT.Generator.Core/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("cswinrtimplgen")] -[assembly: InternalsVisibleTo("cswinrtinteropgen")] -[assembly: InternalsVisibleTo("cswinrtprojectiongen")] -[assembly: InternalsVisibleTo("cswinrtprojectionrefgen")] -[assembly: InternalsVisibleTo("cswinrtwinmdgen")] diff --git a/src/WinRT.Generator.Core/WinRT.Generator.Core.csproj b/src/WinRT.Generator.Core/WinRT.Generator.Core.csproj index e2c8a96a3f..5e0d083a0e 100644 --- a/src/WinRT.Generator.Core/WinRT.Generator.Core.csproj +++ b/src/WinRT.Generator.Core/WinRT.Generator.Core.csproj @@ -33,4 +33,12 @@ + + + + + + + + diff --git a/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGenerator.cs b/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGenerator.cs index 3e7f7026c9..0fa795e2c8 100644 --- a/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGenerator.cs +++ b/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGenerator.cs @@ -39,7 +39,7 @@ public static void Run([Argument] string inputFilePath, CancellationToken token) log: ConsoleApp.Log, token: token); - // Validate the target framework. CsWinRT 3.0 requires .NET 10 or later. + // Validate the target framework. CsWinRT 3.0 requires .NET 10 or later if (!string.IsNullOrEmpty(runner.Args.TargetFramework) && !runner.Args.TargetFramework.StartsWith("net10.0", StringComparison.Ordinal)) { throw WellKnownReferenceProjectionGeneratorExceptions.UnsupportedTargetFramework(runner.Args.TargetFramework); From 125c08f3c212e93583b644c2ca1ef248643075e0 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 9 Jun 2026 14:31:19 -0700 Subject: [PATCH 12/15] Remove redundant global/System qualifiers Simplify references by removing unnecessary qualification prefixes: replace System.StringComparison with StringComparison and global::WindowsRuntime.ProjectionWriter.ProjectionWriter.Run with ProjectionWriter.ProjectionWriter.Run. This is a code cleanup across generator tasks (RunCsWinRTWinMDGenerator.cs, ProjectionGenerator.Generate.cs, ReferenceProjectionGenerator.cs) with no behavior change. --- src/WinRT.Generator.Tasks/RunCsWinRTWinMDGenerator.cs | 2 +- .../Generation/ProjectionGenerator.Generate.cs | 2 +- .../Generation/ReferenceProjectionGenerator.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/WinRT.Generator.Tasks/RunCsWinRTWinMDGenerator.cs b/src/WinRT.Generator.Tasks/RunCsWinRTWinMDGenerator.cs index a5c3aa5ded..64817c827c 100644 --- a/src/WinRT.Generator.Tasks/RunCsWinRTWinMDGenerator.cs +++ b/src/WinRT.Generator.Tasks/RunCsWinRTWinMDGenerator.cs @@ -126,7 +126,7 @@ 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) + if (effectiveArchitecture?.Equals("AnyCPU", StringComparison.OrdinalIgnoreCase) is true) { return Path.Combine(CsWinRTToolsDirectory!, ToolName); } diff --git a/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.Generate.cs b/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.Generate.cs index 3b2e71e2f5..58e6fba9ee 100644 --- a/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.Generate.cs +++ b/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.Generate.cs @@ -67,7 +67,7 @@ private static void GenerateSources(ProjectionGeneratorProcessingState processin // Invoke the projection writer in-process via its public C# API. try { - global::WindowsRuntime.ProjectionWriter.ProjectionWriter.Run(processingState.WriterOptions); + ProjectionWriter.ProjectionWriter.Run(processingState.WriterOptions); } catch (Exception e) when (!e.IsWellKnown) { diff --git a/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGenerator.cs b/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGenerator.cs index 0fa795e2c8..f221165bc7 100644 --- a/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGenerator.cs +++ b/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGenerator.cs @@ -59,7 +59,7 @@ public static void Run([Argument] string inputFilePath, CancellationToken token) { ConsoleApp.Log($"Generating reference projection sources -> {options.OutputFolder}"); - global::WindowsRuntime.ProjectionWriter.ProjectionWriter.Run(options); + ProjectionWriter.ProjectionWriter.Run(options); } catch (Exception e) when (!e.IsWellKnown) { From ef27074ae813a39c03f26a67344aa40c2cd9b432 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 9 Jun 2026 14:33:40 -0700 Subject: [PATCH 13/15] Use primary constructor for GeneratorPhaseRunner Convert GeneratorPhaseRunner to use C# primary-constructor syntax, removing explicit private fields and the manual constructor. Replace uses of _wrapUnhandled and _log with the positional parameters wrapUnhandled and log, and make Args return the positional args field. Add XML param docs for the new parameters. This is a refactor to reduce boilerplate without changing behavior. --- .../GeneratorPhaseRunner.cs | 46 ++++++------------- 1 file changed, 14 insertions(+), 32 deletions(-) diff --git a/src/WinRT.Generator.Core/GeneratorPhaseRunner.cs b/src/WinRT.Generator.Core/GeneratorPhaseRunner.cs index af038e4428..4e9b26594b 100644 --- a/src/WinRT.Generator.Core/GeneratorPhaseRunner.cs +++ b/src/WinRT.Generator.Core/GeneratorPhaseRunner.cs @@ -13,37 +13,19 @@ namespace WindowsRuntime.Generator; /// Unhandled*Exception and optionally logging a progress message before the body runs. /// /// The per-tool args record (must implement ). -internal readonly struct GeneratorPhaseRunner +/// The parsed per-tool args, forwarded to every body. +/// Wraps an unexpected exception into the per-tool Unhandled*Exception with the given phase name. +/// Logs a progress message to the user (typically ConsoleApp.Log from ConsoleAppFramework). +internal readonly struct GeneratorPhaseRunner( + TArgs args, + Func wrapUnhandled, + Action log) where TArgs : IGeneratorArgs { - /// - /// The per-tool wrapUnhandled delegate. - /// - private readonly Func _wrapUnhandled; - - /// - /// The per-tool progress logger. - /// - private readonly Action _log; - - /// - /// Creates a new bound to the given args and per-tool delegates. - /// - /// The parsed per-tool args, forwarded to every body. - /// Wraps an unexpected exception into the per-tool Unhandled*Exception with the given phase name. - /// Logs a progress message to the user (typically ConsoleApp.Log from ConsoleAppFramework). - internal GeneratorPhaseRunner(TArgs args, Func wrapUnhandled, Action log) - { - _wrapUnhandled = wrapUnhandled; - _log = log; - - Args = args; - } - /// /// Gets the parsed per-tool args, forwarded to every body. /// - public TArgs Args { get; } + public TArgs Args => args; /// /// Runs , wrapping any unexpected exception in the per-tool @@ -59,7 +41,7 @@ public void RunPhase(string phaseName, Action body) } catch (Exception e) when (!e.IsWellKnown) { - throw _wrapUnhandled(phaseName, e); + throw wrapUnhandled(phaseName, e); } } @@ -69,13 +51,13 @@ public void RunPhase(string phaseName, string logMessage, Action body) { try { - _log(logMessage); + log(logMessage); body(Args); } catch (Exception e) when (!e.IsWellKnown) { - throw _wrapUnhandled(phaseName, e); + throw wrapUnhandled(phaseName, e); } } @@ -89,7 +71,7 @@ public T RunPhase(string phaseName, Func body) } catch (Exception e) when (!e.IsWellKnown) { - throw _wrapUnhandled(phaseName, e); + throw wrapUnhandled(phaseName, e); } } @@ -100,13 +82,13 @@ public T RunPhase(string phaseName, string logMessage, Func body) { try { - _log(logMessage); + log(logMessage); return body(Args); } catch (Exception e) when (!e.IsWellKnown) { - throw _wrapUnhandled(phaseName, e); + throw wrapUnhandled(phaseName, e); } } } From 98305a1e1a121978dd26cd3b752de90c04c5cba6 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 9 Jun 2026 14:43:16 -0700 Subject: [PATCH 14/15] Auto-call ThrowIfCancellationRequested after each RunPhase body Every per-tool 'Run' method previously placed a manual 'args.Token.ThrowIfCancellationRequested()' between consecutive phases - 8 explicit calls across the 5 generators. Bake that check into 'GeneratorPhaseRunner.RunPhase' (all 4 overloads): after the body completes successfully, the runner calls 'args.Token.ThrowIfCancellationRequested()' before returning. The check sits OUTSIDE the 'try'/'catch' so the resulting 'OperationCanceledException' propagates without going through the per-tool 'wrapUnhandled' delegate, which is already a no-op for it ('OperationCanceledException' is in 'IsWellKnown'). Per-tool 'Run' methods drop the 8 redundant 'runner.Args.Token.ThrowIfCancellationRequested()' calls; the last phase of each generator now does one harmless extra cancellation check before the success log message. Net diff: 6 files, +18 / -18 LOC. All 5 generators build with 0 warnings and pass end-to-end debug-repro validation (ref-gen: 8/8 0 diffs, WinMD: 16-byte MVID-only delta, proj-gen: 327/327 0 diffs, impl-gen: 0 byte diffs, interop-gen: smoke OK). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../GeneratorPhaseRunner.cs | 20 +++++++++++++++++-- .../Generation/ImplGenerator.cs | 6 ------ .../Generation/InteropGenerator.cs | 2 -- .../Generation/ProjectionGenerator.cs | 4 ---- .../ReferenceProjectionGenerator.cs | 2 -- .../Generation/WinMDGenerator.cs | 2 -- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/WinRT.Generator.Core/GeneratorPhaseRunner.cs b/src/WinRT.Generator.Core/GeneratorPhaseRunner.cs index 4e9b26594b..58b3cf1299 100644 --- a/src/WinRT.Generator.Core/GeneratorPhaseRunner.cs +++ b/src/WinRT.Generator.Core/GeneratorPhaseRunner.cs @@ -43,6 +43,8 @@ public void RunPhase(string phaseName, Action body) { throw wrapUnhandled(phaseName, e); } + + args.Token.ThrowIfCancellationRequested(); } /// @@ -59,20 +61,28 @@ public void RunPhase(string phaseName, string logMessage, Action body) { throw wrapUnhandled(phaseName, e); } + + args.Token.ThrowIfCancellationRequested(); } /// /// The value returned by . public T RunPhase(string phaseName, Func body) { + T result; + try { - return body(Args); + result = body(Args); } catch (Exception e) when (!e.IsWellKnown) { throw wrapUnhandled(phaseName, e); } + + args.Token.ThrowIfCancellationRequested(); + + return result; } /// @@ -80,16 +90,22 @@ public T RunPhase(string phaseName, Func body) /// The value returned by . public T RunPhase(string phaseName, string logMessage, Func body) { + T result; + try { log(logMessage); - return body(Args); + result = body(Args); } catch (Exception e) when (!e.IsWellKnown) { throw wrapUnhandled(phaseName, e); } + + args.Token.ThrowIfCancellationRequested(); + + return result; } } diff --git a/src/WinRT.Impl.Generator/Generation/ImplGenerator.cs b/src/WinRT.Impl.Generator/Generation/ImplGenerator.cs index cf6e97c87f..331c19a05d 100644 --- a/src/WinRT.Impl.Generator/Generation/ImplGenerator.cs +++ b/src/WinRT.Impl.Generator/Generation/ImplGenerator.cs @@ -85,15 +85,11 @@ public static void Run([Argument] string inputFilePath, CancellationToken token) phaseName: "loading", body: LoadOutputModule); - runner.Args.Token.ThrowIfCancellationRequested(); - // Define the impl module to emit ModuleDefinition implModule = runner.RunPhase( phaseName: "loading", body: _ => DefineImplModule(runtimeContext, outputModule)); - runner.Args.Token.ThrowIfCancellationRequested(); - // Emit all necessary IL code in the impl module runner.RunPhase(phaseName: "generation", body: _ => { @@ -101,8 +97,6 @@ public static void Run([Argument] string inputFilePath, CancellationToken token) EmitTypeForwards(outputModule, implModule); }); - runner.Args.Token.ThrowIfCancellationRequested(); - // Write the module to disk with all the generated contents runner.RunPhase( phaseName: "emit", diff --git a/src/WinRT.Interop.Generator/Generation/InteropGenerator.cs b/src/WinRT.Interop.Generator/Generation/InteropGenerator.cs index 9b51554ae9..7ec2f9c60c 100644 --- a/src/WinRT.Interop.Generator/Generation/InteropGenerator.cs +++ b/src/WinRT.Interop.Generator/Generation/InteropGenerator.cs @@ -39,8 +39,6 @@ public static void Run([Argument] string inputFilePath, CancellationToken token) logMessage: $"Processing {runner.Args.ReferenceAssemblyPaths.Length + runner.Args.ImplementationAssemblyPaths.Length + 1} module(s)", body: Discover); - runner.Args.Token.ThrowIfCancellationRequested(); - // Emit the resulting interop assembly runner.RunPhase( phaseName: "emit", diff --git a/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.cs b/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.cs index 7f871b28d3..7bb3a9ffb1 100644 --- a/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.cs +++ b/src/WinRT.Projection.Generator/Generation/ProjectionGenerator.cs @@ -45,8 +45,6 @@ public static void Run([Argument] string inputFilePath, CancellationToken token) }, body: ProcessReferences); - runner.Args.Token.ThrowIfCancellationRequested(); - // If no types were found to project (e.g., component mode with no component references), // skip the source generation and emit phases entirely (no .dll will be produced at all). if (!processingState.HasTypesToProject) @@ -60,8 +58,6 @@ public static void Run([Argument] string inputFilePath, CancellationToken token) logMessage: "Generating projection code", body: _ => GenerateSources(processingState)); - runner.Args.Token.ThrowIfCancellationRequested(); - // Invoke Roslyn to compile the generated sources into 'WinRT.Projection.dll' runner.RunPhase( phaseName: "emit", diff --git a/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGenerator.cs b/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGenerator.cs index f221165bc7..a242cbcd8e 100644 --- a/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGenerator.cs +++ b/src/WinRT.Projection.Ref.Generator/Generation/ReferenceProjectionGenerator.cs @@ -50,8 +50,6 @@ public static void Run([Argument] string inputFilePath, CancellationToken token) phaseName: "processing", body: BuildWriterOptions); - runner.Args.Token.ThrowIfCancellationRequested(); - // Invoke the projection writer (in-process) to generate the projection sources. We can't // route this through the shared 'runner.RunPhase' helper because we wrap the exception // into a well-known 'CsWinRTProcessError' rather than the per-tool 'Unhandled' factory. diff --git a/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.cs b/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.cs index 188d64c068..1f71951433 100644 --- a/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.cs +++ b/src/WinRT.WinMD.Generator/Generation/WinMDGenerator.cs @@ -53,8 +53,6 @@ public static void Run([Argument] string inputFilePath, CancellationToken token) logMessage: $"Processing assembly: '{System.IO.Path.GetFileName(runner.Args.InputAssemblyPath)}'", body: Discover); - token.ThrowIfCancellationRequested(); - // Generate and write the .winmd file runner.RunPhase( phaseName: "generation", From 362d5a32572dc883a857f0c6fbafb6262cffb8a1 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 9 Jun 2026 14:44:15 -0700 Subject: [PATCH 15/15] Rename file to GeneratorPhaseRunner{TArgs}.cs Rename src/WinRT.Generator.Core/GeneratorPhaseRunner.cs to src/WinRT.Generator.Core/GeneratorPhaseRunner{TArgs}.cs. No source changes were made; this update clarifies the file name to reflect the generic TArgs type parameter. --- .../{GeneratorPhaseRunner.cs => GeneratorPhaseRunner{TArgs}.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/WinRT.Generator.Core/{GeneratorPhaseRunner.cs => GeneratorPhaseRunner{TArgs}.cs} (100%) diff --git a/src/WinRT.Generator.Core/GeneratorPhaseRunner.cs b/src/WinRT.Generator.Core/GeneratorPhaseRunner{TArgs}.cs similarity index 100% rename from src/WinRT.Generator.Core/GeneratorPhaseRunner.cs rename to src/WinRT.Generator.Core/GeneratorPhaseRunner{TArgs}.cs