From bf1c80be732a0be21313628bbe889952f6ca8fe3 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 6 Jul 2026 15:02:48 -0700 Subject: [PATCH 01/14] Support enum-typed arguments in the response file parser Add case-insensitive, name-based enum parsing to ResponseFileParser and matching name-based serialization to ResponseFileBuilder, so generator args can expose enum-typed options that round-trip through response files (and debug repros) deterministically. Numeric values are rejected to avoid silently accepting out-of-range enum inputs. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Parsing/ResponseFileBuilder.cs | 7 +++++++ .../Parsing/ResponseFileParser.cs | 20 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/src/WinRT.Generator.Core/Parsing/ResponseFileBuilder.cs b/src/WinRT.Generator.Core/Parsing/ResponseFileBuilder.cs index fa90f6971..a7324201e 100644 --- a/src/WinRT.Generator.Core/Parsing/ResponseFileBuilder.cs +++ b/src/WinRT.Generator.Core/Parsing/ResponseFileBuilder.cs @@ -89,6 +89,13 @@ private static string FormatValue(object value) return string.Join(',', array); } + // Enum values are serialized by their member name (e.g. 'All'), matching the parser's + // case-insensitive, name-based enum handling so the round-trip stays deterministic. + if (value is Enum enumValue) + { + return enumValue.ToString(); + } + // 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 25a2bc7a6..39dbacd8b 100644 --- a/src/WinRT.Generator.Core/Parsing/ResponseFileParser.cs +++ b/src/WinRT.Generator.Core/Parsing/ResponseFileParser.cs @@ -261,6 +261,26 @@ private static bool TryConvert(string rawValue, Type targetType, out object? con return true; } + // Enum-typed arguments are parsed by name (case-insensitively), so that MSBuild values such + // as 'all' round-trip to the matching enum member (e.g. 'All'). Numeric values are rejected, + // as they carry no meaning in a response file and would silently accept out-of-range inputs. + if (targetType.IsEnum) + { + if (rawValue.Length > 0 && + !char.IsAsciiDigit(rawValue[0]) && + rawValue[0] != '-' && + Enum.TryParse(targetType, rawValue, ignoreCase: true, out object? parsedEnum)) + { + converted = parsedEnum; + + return true; + } + + converted = null; + + return false; + } + // For primitives ('bool', 'int', etc.) we rely on the standard 'Convert.ChangeType', which // is AOT-safe for the built-in primitives and uses invariant culture for deterministic parsing. try From b674184350fdf7620d5af923b2e38ae194d95c30 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 6 Jul 2026 15:04:07 -0700 Subject: [PATCH 02/14] Add base class library assembly detection to the generator core Add BaseClassLibraryIdentity, which identifies .NET base class library (BCL) and shared-framework assemblies by their well-known Microsoft public key tokens. This is used to let the interop generator skip framework assemblies when discovering user-defined/CCW/generic types. The token set was validated against the Microsoft.NETCore.App, Microsoft.WindowsDesktop.App, and Microsoft.AspNetCore.App shared frameworks. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../References/BaseClassLibraryIdentity.cs | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 src/WinRT.Generator.Core/References/BaseClassLibraryIdentity.cs diff --git a/src/WinRT.Generator.Core/References/BaseClassLibraryIdentity.cs b/src/WinRT.Generator.Core/References/BaseClassLibraryIdentity.cs new file mode 100644 index 000000000..cc4c07192 --- /dev/null +++ b/src/WinRT.Generator.Core/References/BaseClassLibraryIdentity.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace WindowsRuntime.Generator.References; + +/// +/// Helpers to identify assemblies that are part of the .NET base class library (BCL), i.e. the default set +/// of libraries shipped by the .NET SDK (the runtime shared frameworks and their reference assemblies). +/// +/// +/// +/// Detection is based on the assembly's public key token. The .NET runtime and framework assemblies are all +/// strong-name signed with a small, fixed set of Microsoft-owned keys. User code cannot be signed with these +/// keys, so a match reliably identifies a framework assembly (and, conversely, user or third-party assemblies +/// never match). +/// +/// +/// The set below was validated against the assemblies shipped in the Microsoft.NETCore.App, +/// Microsoft.WindowsDesktop.App, and Microsoft.AspNetCore.App shared frameworks. +/// +/// +internal static class BaseClassLibraryIdentity +{ + /// The ECMA public key token (b77a5c561934e089): mscorlib, System, System.Core, System.Xml, etc. + private static ReadOnlySpan EcmaPublicKeyToken => [0xB7, 0x7A, 0x5C, 0x56, 0x19, 0x34, 0xE0, 0x89]; + + /// The Microsoft public key token (b03f5f7f11d50a3a): most System.* assemblies, Microsoft.CSharp, Microsoft.VisualBasic, Microsoft.Win32.*, etc. + private static ReadOnlySpan MicrosoftPublicKeyToken => [0xB0, 0x3F, 0x5F, 0x7F, 0x11, 0xD5, 0x0A, 0x3A]; + + /// The .NET public key token (cc7b13ffcd2ddd51): netstandard, System.Buffers, System.Memory, System.Numerics.Vectors, etc. + private static ReadOnlySpan DotNetPublicKeyToken => [0xCC, 0x7B, 0x13, 0xFF, 0xCD, 0x2D, 0xDD, 0x51]; + + /// The runtime public key token (7cec85d7bea7798e): System.Private.CoreLib. + private static ReadOnlySpan CoreLibPublicKeyToken => [0x7C, 0xEC, 0x85, 0xD7, 0xBE, 0xA7, 0x79, 0x8E]; + + /// The Windows public key token (31bf3856ad364e35): WindowsBase, WCF/WPF assemblies, System.ServiceModel.Web, etc. + private static ReadOnlySpan WindowsPublicKeyToken => [0x31, 0xBF, 0x38, 0x56, 0xAD, 0x36, 0x4E, 0x35]; + + /// The ASP.NET Core public key token (adb9793829ddae60): the Microsoft.AspNetCore.* shared framework assemblies. + private static ReadOnlySpan AspNetCorePublicKeyToken => [0xAD, 0xB9, 0x79, 0x38, 0x29, 0xDD, 0xAE, 0x60]; + + /// + /// Checks whether a given public key token belongs to a .NET base class library / framework assembly. + /// + /// The 8-byte public key token to check. + /// Whether identifies a .NET base class library / framework assembly. + public static bool IsBaseClassLibraryPublicKeyToken(ReadOnlySpan publicKeyToken) + { + // A public key token is always exactly 8 bytes. Anything else (including an empty span for + // unsigned assemblies) is definitely not a framework assembly. The 'SequenceEqual' checks + // below would already return 'false' in that case, but the explicit length check documents + // the invariant and short-circuits the comparisons for non-framework assemblies. + return + publicKeyToken.Length == 8 && + (publicKeyToken.SequenceEqual(EcmaPublicKeyToken) || + publicKeyToken.SequenceEqual(MicrosoftPublicKeyToken) || + publicKeyToken.SequenceEqual(DotNetPublicKeyToken) || + publicKeyToken.SequenceEqual(CoreLibPublicKeyToken) || + publicKeyToken.SequenceEqual(WindowsPublicKeyToken) || + publicKeyToken.SequenceEqual(AspNetCorePublicKeyToken)); + } +} From 643e912f2bbee04a6515b4c7b3c000d4ee4c2af5 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 6 Jul 2026 15:07:25 -0700 Subject: [PATCH 03/14] Add CsWinRTMarshallingMode to the interop generator Introduce the CsWinRTMarshallingMode option ('all', 'minimal', 'strict'), controlling which assemblies are analyzed when discovering user-defined/CCW types and generic instantiations: - 'all' (default): analyze every assembly, including those that don't reference any CsWinRT assembly. This lets projects that don't target a Windows TFM (e.g. a class library with just MVVM viewmodels) contribute the marshalling code their types need. - 'minimal': analyze every assembly except those from the BCL, to reduce binary size. - 'strict': only analyze assemblies referencing the Windows Runtime assembly (the historical behavior). Assemblies that reference the Windows Runtime assembly are always analyzed, regardless of the mode. Adds the CsWinRTMarshallingMode enum, the --marshalling-mode argument (defaulting to 'all'), an IsBaseClassLibraryModule module helper, and factors the discovery filter into ShouldProcessModule. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Extensions/WindowsRuntimeExtensions.cs | 9 ++++ .../Generation/CsWinRTMarshallingMode.cs | 32 +++++++++++++ .../Generation/InteropGenerator.Discover.cs | 45 +++++++++++++++++-- .../Generation/InteropGeneratorArgs.cs | 7 +++ 4 files changed, 89 insertions(+), 4 deletions(-) create mode 100644 src/WinRT.Interop.Generator/Generation/CsWinRTMarshallingMode.cs diff --git a/src/WinRT.Interop.Generator/Extensions/WindowsRuntimeExtensions.cs b/src/WinRT.Interop.Generator/Extensions/WindowsRuntimeExtensions.cs index ce70b8a1a..83ecd96bd 100644 --- a/src/WinRT.Interop.Generator/Extensions/WindowsRuntimeExtensions.cs +++ b/src/WinRT.Interop.Generator/Extensions/WindowsRuntimeExtensions.cs @@ -8,6 +8,7 @@ using AsmResolver.DotNet; using AsmResolver.DotNet.Signatures; using AsmResolver.PE.DotNet.Metadata.Tables; +using WindowsRuntime.Generator.References; using WindowsRuntime.InteropGenerator.References; #pragma warning disable IDE0046 @@ -1116,6 +1117,14 @@ arrayType.BaseType is not SzArrayTypeSignature && /// Whether the module is the Windows Runtime assembly. public bool IsWindowsRuntimeModule => module.Name == WellKnownMetadataNames.WinRTRuntimeModuleName; + /// + /// Checks whether a belongs to the .NET base class library (BCL), i.e. the + /// default set of libraries shipped by the .NET SDK (the runtime shared frameworks). Detection is based on + /// the assembly's public key token (see ). + /// + /// Whether the module is a .NET base class library / framework assembly. + public bool IsBaseClassLibraryModule => BaseClassLibraryIdentity.IsBaseClassLibraryPublicKeyToken(module.Assembly?.GetPublicKeyToken()); + /// /// Checks whether a references the Windows Runtime assembly. /// diff --git a/src/WinRT.Interop.Generator/Generation/CsWinRTMarshallingMode.cs b/src/WinRT.Interop.Generator/Generation/CsWinRTMarshallingMode.cs new file mode 100644 index 000000000..e7e779f34 --- /dev/null +++ b/src/WinRT.Interop.Generator/Generation/CsWinRTMarshallingMode.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace WindowsRuntime.InteropGenerator.Generation; + +/// +/// Controls which assemblies the interop generator analyzes when discovering user-defined (CCW) types and +/// generic instantiations that require marshalling code. +/// +/// +/// Assemblies that reference the Windows Runtime assembly (i.e. those targeting a Windows TFM) are always +/// analyzed, regardless of the selected mode. The mode only affects assemblies that do not reference any +/// CsWinRT assembly, which lets projects that don't target a Windows TFM (e.g. a class library with just +/// MVVM viewmodels) still contribute the marshalling code their types need. +/// +internal enum CsWinRTMarshallingMode +{ + /// + /// Analyzes all assemblies, including those that don't reference any CsWinRT assembly. This is the default. + /// + All, + + /// + /// Analyzes all assemblies except those from the .NET base class library (BCL), to reduce binary size. + /// + Minimal, + + /// + /// Only analyzes assemblies that reference the Windows Runtime assembly (i.e. those targeting a Windows TFM). + /// + Strict +} diff --git a/src/WinRT.Interop.Generator/Generation/InteropGenerator.Discover.cs b/src/WinRT.Interop.Generator/Generation/InteropGenerator.Discover.cs index 42760ba3c..51555ddf9 100644 --- a/src/WinRT.Interop.Generator/Generation/InteropGenerator.Discover.cs +++ b/src/WinRT.Interop.Generator/Generation/InteropGenerator.Discover.cs @@ -241,10 +241,10 @@ private static void LoadAndProcessModule( args.Token.ThrowIfCancellationRequested(); - // We're only interested in harvesting .dll-s which reference the Windows SDK projections. - // This is true for all .dll-s that were built targeting 'netX.0-windows10.0.XXXX.0'. - // So this check effectively lets us filter all .dll-s that were in projects with this TFM. - if (!module.ReferencesWindowsRuntimeAssembly && !module.IsWindowsRuntimeModule) + // Determine whether this module should be analyzed, based on the configured marshalling mode. + // Modules that reference the Windows Runtime assembly (i.e. those targeting a Windows TFM) are + // always analyzed; the mode only affects modules that don't reference any CsWinRT assembly. + if (!ShouldProcessModule(args, module)) { return; } @@ -279,6 +279,43 @@ private static void LoadAndProcessModule( DiscoverSzArrayTypes(args, discoveryState, module); } + /// + /// Determines whether a given module should be analyzed for discovery, based on the marshalling mode. + /// + /// The arguments for this invocation. + /// The module to check. + /// Whether should be analyzed for discovery. + /// + /// Modules that reference the Windows Runtime assembly were built targeting a Windows TFM (i.e. + /// netX.0-windows10.0.XXXX.0), and the Windows Runtime assembly itself, are always analyzed, + /// regardless of the marshalling mode. Only modules that don't reference any CsWinRT assembly are + /// subject to the mode-specific filtering. + /// + private static bool ShouldProcessModule(InteropGeneratorArgs args, ModuleDefinition module) + { + return args.MarshallingMode switch + { + // 'All' analyzes every module, even those not referencing any CsWinRT assembly. + CsWinRTMarshallingMode.All => true, + + // 'Minimal' analyzes every module except those from the BCL. Non-BCL modules are checked + // first, as that is the common case for user code and avoids the more expensive Windows + // Runtime reference lookup (a BCL module can only match via the checks that follow if a + // future framework assembly were to reference the Windows Runtime assembly). + CsWinRTMarshallingMode.Minimal => + !module.IsBaseClassLibraryModule || + module.IsWindowsRuntimeModule || + module.ReferencesWindowsRuntimeAssembly, + + // 'Strict' only analyzes modules referencing the Windows Runtime assembly. This matches the + // historical behavior, effectively filtering to '-windows' TFM projects. + CsWinRTMarshallingMode.Strict => module.IsWindowsRuntimeModule || module.ReferencesWindowsRuntimeAssembly, + + // The marshalling mode is always one of the values above (validated during argument parsing). + _ => throw new ArgumentOutOfRangeException(nameof(args), args.MarshallingMode, "Unexpected marshalling mode.") + }; + } + /// /// Discovers all type hierarchy types in a given assembly. /// diff --git a/src/WinRT.Interop.Generator/Generation/InteropGeneratorArgs.cs b/src/WinRT.Interop.Generator/Generation/InteropGeneratorArgs.cs index baf3434fe..3ab3ed687 100644 --- a/src/WinRT.Interop.Generator/Generation/InteropGeneratorArgs.cs +++ b/src/WinRT.Interop.Generator/Generation/InteropGeneratorArgs.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.ComponentModel; using System.Threading; using WindowsRuntime.Generator; using WindowsRuntime.Generator.Attributes; @@ -48,6 +49,12 @@ internal sealed class InteropGeneratorArgs : IGeneratorArgs [CommandLineArgumentName("--use-windows-ui-xaml-projections")] public required bool UseWindowsUIXamlProjections { get; init; } + /// Gets the marshalling mode, controlling which assemblies are analyzed to discover user-defined/CCW/generic types. + /// Defaults to when not specified in the response file. + [CommandLineArgumentName("--marshalling-mode")] + [DefaultValue(CsWinRTMarshallingMode.All)] + public CsWinRTMarshallingMode MarshallingMode { get; init; } + /// Gets whether to validate the assembly version of WinRT.Runtime.dll, to ensure it matches the generator. [CommandLineArgumentName("--validate-winrt-runtime-assembly-version")] public required bool ValidateWinRTRuntimeAssemblyVersion { get; init; } From cb3807c02e3c80663d64c3acd5b940f53a4cf3fe Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 6 Jul 2026 15:10:08 -0700 Subject: [PATCH 04/14] Wire CsWinRTMarshallingMode through the MSBuild task and targets Add a MarshallingMode parameter to the RunCsWinRTInteropGenerator task (with validation against the well-known values) that emits '--marshalling-mode' to the response file. Add the user-facing CsWinRTMarshallingMode MSBuild property (defaulting to 'all'), pass it to the task, and include it in the generator's property-inputs cache hash so changing the mode correctly re-runs generation. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- ...crosoft.Windows.CsWinRT.CsWinRTGen.targets | 15 +++++++ .../RunCsWinRTInteropGenerator.cs | 44 +++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/nuget/Microsoft.Windows.CsWinRT.CsWinRTGen.targets b/nuget/Microsoft.Windows.CsWinRT.CsWinRTGen.targets index bf683b9df..1911a60a7 100644 --- a/nuget/Microsoft.Windows.CsWinRT.CsWinRTGen.targets +++ b/nuget/Microsoft.Windows.CsWinRT.CsWinRTGen.targets @@ -45,6 +45,19 @@ Copyright (C) Microsoft Corporation. All rights reserved. High true + + all + @@ -133,6 +146,7 @@ Copyright (C) Microsoft Corporation. All rights reserved. <_RunCsWinRTGeneratorInputsCacheToHash Include="$(CsWinRTGeneratorInteropAssemblyDirectory)" /> <_RunCsWinRTGeneratorInputsCacheToHash Include="$(CsWinRTGeneratorDebugReproDirectory)" /> <_RunCsWinRTGeneratorInputsCacheToHash Include="$(CsWinRTUseWindowsUIXamlProjections)" /> + <_RunCsWinRTGeneratorInputsCacheToHash Include="$(CsWinRTMarshallingMode)" /> <_RunCsWinRTGeneratorInputsCacheToHash Include="$(CsWinRTGeneratorValidateWinRTRuntimeAssemblyVersion)" /> <_RunCsWinRTGeneratorInputsCacheToHash Include="$(CsWinRTGeneratorValidateWinRTRuntimeDllVersion2References)" /> <_RunCsWinRTGeneratorInputsCacheToHash Include="$(CsWinRTGeneratorEnableIncrementalGeneration)" /> @@ -185,6 +199,7 @@ Copyright (C) Microsoft Corporation. All rights reserved. CsWinRTToolsDirectory="$(CsWinRTInteropGenEffectiveToolsDirectory)" CsWinRTToolsArchitecture="$(CsWinRTToolsArchitecture)" UseWindowsUIXamlProjections="$(CsWinRTUseWindowsUIXamlProjections)" + MarshallingMode="$(CsWinRTMarshallingMode)" ValidateWinRTRuntimeAssemblyVersion="$(CsWinRTGeneratorValidateWinRTRuntimeAssemblyVersion)" ValidateWinRTRuntimeDllVersion2References="$(CsWinRTGeneratorValidateWinRTRuntimeDllVersion2References)" EnableIncrementalGeneration="$(CsWinRTGeneratorEnableIncrementalGeneration)" diff --git a/src/WinRT.Generator.Tasks/RunCsWinRTInteropGenerator.cs b/src/WinRT.Generator.Tasks/RunCsWinRTInteropGenerator.cs index df3cda15c..71ebc4e2f 100644 --- a/src/WinRT.Generator.Tasks/RunCsWinRTInteropGenerator.cs +++ b/src/WinRT.Generator.Tasks/RunCsWinRTInteropGenerator.cs @@ -96,6 +96,13 @@ public sealed class RunCsWinRTInteropGenerator : ToolTask /// If not set, it will default to (i.e. using Microsoft.UI.Xaml projections). public bool UseWindowsUIXamlProjections { get; set; } = false; + /// + /// Gets or sets the marshalling mode, controlling which assemblies are analyzed to discover + /// user-defined/CCW/generic types (one of all, minimal, or strict). + /// + /// If not set, it will default to all (i.e. analyzing every assembly). + public string MarshallingMode { get; set; } = DefaultMarshallingMode; + /// /// Gets whether to validate the assembly version of WinRT.Runtime.dll, to ensure it matches the generator. /// @@ -127,6 +134,16 @@ public sealed class RunCsWinRTInteropGenerator : ToolTask /// public ITaskItem[]? AdditionalArguments { get; set; } + /// + /// The default marshalling mode, used when none is specified. + /// + private const string DefaultMarshallingMode = "all"; + + /// + /// The set of valid marshalling modes (compared case-insensitively). + /// + private static readonly string[] ValidMarshallingModes = ["all", "minimal", "strict"]; + /// protected override string ToolName => "cswinrtinteropgen.exe"; @@ -219,9 +236,35 @@ protected override bool ValidateParameters() return false; } + // The marshalling mode must be one of the well-known values (compared case-insensitively). + if (!IsValidMarshallingMode(MarshallingMode)) + { + Log.LogWarning("Invalid 'MarshallingMode' value '{0}'. It must be one of 'all', 'minimal', or 'strict'.", MarshallingMode); + + return false; + } + return true; } + /// + /// Checks whether a given marshalling mode value is valid (i.e. one of the well-known values). + /// + /// The marshalling mode value to check. + /// Whether is a valid marshalling mode. + private static bool IsValidMarshallingMode(string marshallingMode) + { + foreach (string validMode in ValidMarshallingModes) + { + if (validMode.Equals(marshallingMode, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } + /// [SuppressMessage("Style", "IDE0072", Justification = "We always use 'x86' as a fallback for all other CPU architectures.")] protected override string GenerateFullPathToTool() @@ -271,6 +314,7 @@ protected override string GenerateResponseFileCommands() AppendResponseFileCommand(args, "--generated-assembly-directory", InteropAssemblyDirectory!); AppendResponseFileOptionalCommand(args, "--debug-repro-directory", DebugReproDirectory); AppendResponseFileCommand(args, "--use-windows-ui-xaml-projections", UseWindowsUIXamlProjections.ToString()); + AppendResponseFileCommand(args, "--marshalling-mode", MarshallingMode); AppendResponseFileCommand(args, "--validate-winrt-runtime-assembly-version", ValidateWinRTRuntimeAssemblyVersion.ToString()); AppendResponseFileCommand(args, "--validate-winrt-runtime-dll-version-2-references", ValidateWinRTRuntimeDllVersion2References.ToString()); AppendResponseFileCommand(args, "--enable-incremental-generation", EnableIncrementalGeneration.ToString()); From e751239bfb103ed429bdee586596e299e2a79ef2 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 6 Jul 2026 15:11:16 -0700 Subject: [PATCH 05/14] Document CsWinRTMarshallingMode Document the new CsWinRTMarshallingMode option in docs/usage.md and nuget/readme.md, and update the interop generator skill with the new --marshalling-mode argument and the mode-based input filtering behavior. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/skills/interop-generator/SKILL.md | 6 ++++++ docs/usage.md | 19 +++++++++++++++++++ nuget/readme.md | 1 + 3 files changed, 26 insertions(+) diff --git a/.github/skills/interop-generator/SKILL.md b/.github/skills/interop-generator/SKILL.md index 96e5d1611..3261e3151 100644 --- a/.github/skills/interop-generator/SKILL.md +++ b/.github/skills/interop-generator/SKILL.md @@ -230,6 +230,7 @@ The generator is invoked via a response file: `cswinrtinteropgen.exe @path/to/re | `--winrt-component-assembly-path` | `string?` | `WinRT.Component.dll` path (for authored components) | | `--generated-assembly-directory` | `string` | Output folder for `WinRT.Interop.dll` | | `--use-windows-ui-xaml-projections` | `bool` | Use UWP XAML (`Windows.UI.Xaml`) instead of WinUI | +| `--marshalling-mode` | `CsWinRTMarshallingMode` | Which assemblies to analyze for discovery: `All` (default), `Minimal` (skip BCL), or `Strict` (only WinRT-referencing) | | `--validate-winrt-runtime-assembly-version` | `bool` | Check version compatibility | | `--validate-winrt-runtime-dll-version-2-references` | `bool` | Reject CsWinRT 2.x references | | `--enable-incremental-generation` | `bool` | Enable incremental generation (caching) | @@ -282,6 +283,11 @@ The generator processes two categories of assemblies: - `WinRT.Projection.dll` — 3rd-party component projections (optional) - `WinRT.Component.dll` — Authored component projections (optional) +**Marshalling mode** (`--marshalling-mode`, `ShouldProcessModule` in `InteropGenerator.Discover.cs`): controls which modules are analyzed for discovery. Modules referencing the Windows Runtime assembly (i.e. those built targeting a Windows TFM) are **always** analyzed; the mode only affects modules that don't reference any CsWinRT assembly: +- `All` (default) — analyze every module, even plain `.NET` assemblies. This lets projects that don't target a Windows TFM (e.g. a class library with just MVVM viewmodels) contribute the marshalling code their types need. +- `Minimal` — analyze every module except those from the BCL (detected via well-known .NET/Microsoft public key tokens, see `BaseClassLibraryIdentity` in `WinRT.Generator.Core` and the `ModuleDefinition.IsBaseClassLibraryModule` extension), to reduce binary size. +- `Strict` — only analyze modules referencing the Windows Runtime assembly (the historical behavior). Corresponds to `!ReferencesWindowsRuntimeAssembly && !IsWindowsRuntimeModule → skip`. + **Type exclusions** (`Helpers/TypeExclusions.cs`): - `System.Threading.Tasks.Task` — Cannot be marshalled across Windows Runtime boundary - `System.Collections.Concurrent.ConditionalWeakTable<,>` — Memory semantics conflict diff --git a/docs/usage.md b/docs/usage.md index 12dd217e9..d91583344 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -136,10 +136,29 @@ Below are the most commonly used MSBuild properties. For a full list, refer to t | `CsWinRTGenerateProjection` | `true` | Generate C# projection sources from `.winmd` metadata. | | `CsWinRTGenerateReferenceProjection` | `false` | Generate reference-only projections (for NuGet distribution). | | `CsWinRTComponent` | `false` | Enable Windows Runtime component authoring mode. | +| `CsWinRTMarshallingMode` | `all` | Controls which assemblies the interop generator analyzes for marshalling code (`all`, `minimal`, or `strict`). See below. | | `CsWinRTIncludes` | *(empty)* | Semicolon-separated namespaces to include in the projection. | | `CsWinRTExcludes` | `Windows;Microsoft` | Semicolon-separated namespaces to exclude from the projection. | | `CsWinRTMessageImportance` | `normal` | Build message verbosity (`normal` or `high`). | +### Marshalling mode + +By default (`all`), the interop generator analyzes **every** assembly in the app to discover user-defined +(CCW) types and generic instantiations that need marshalling code — even assemblies that don't target a +Windows TFM and don't reference any CsWinRT assembly. This means a plain `.NET` class library (e.g. one +holding MVVM viewmodels) no longer needs to target a `-windows` TFM just so its types can be marshalled to +native code. + +`CsWinRTMarshallingMode` controls this behavior: + +| Value | Behavior | +|-------|----------| +| `all` (default) | Analyze all assemblies, including those that don't reference any CsWinRT assembly. | +| `minimal` | Same as `all`, but skip assemblies from the .NET base class library (BCL) to reduce binary size. | +| `strict` | Only analyze assemblies referencing the Windows Runtime assembly (i.e. those targeting a Windows TFM). | + +Assemblies that reference the Windows Runtime assembly are always analyzed, regardless of the mode. + ## Author and consume a C#/WinRT component This is coming soon for CsWinRT 3.0. diff --git a/nuget/readme.md b/nuget/readme.md index e74e375d6..80bb0a497 100644 --- a/nuget/readme.md +++ b/nuget/readme.md @@ -33,6 +33,7 @@ C#/WinRT behavior can be customized with these project properties: | CsWinRTGenerateReferenceProjection | true \| *false | Generate reference-only projections (for NuGet distribution) | | CsWinRTGenerateInteropAssembly | auto | Generate interop assemblies at build time (defaults to `true` for Exe/WinExe, or Library with `PublishAot=true`) | | CsWinRTComponent | true \| *false | Enable Windows Runtime component authoring mode | +| CsWinRTMarshallingMode | *all \| minimal \| strict | Controls which assemblies the interop generator analyzes for marshalling code. `all` analyzes every assembly (even those not referencing CsWinRT), `minimal` skips the .NET base class library (BCL), and `strict` only analyzes assemblies referencing the Windows Runtime assembly | | CsWinRTUseWindowsUIXamlProjections | true \| *false | Use UWP XAML (`Windows.UI.Xaml`) instead of WinUI (`Microsoft.UI.Xaml`) | | CsWinRTMergeReferencedActivationFactories | true \| *false | Makes `DllGetActivationFactory` forward activation calls to all referenced WinRT components, allowing them to be merged into a single executable | | CsWinRTIncludes | "" | Semicolon-separated namespaces to include in projection output | From 77e582321fbbce0b7a786e25077e21960bd90d5c Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 6 Jul 2026 15:28:49 -0700 Subject: [PATCH 06/14] Default the marshalling mode to 'minimal' Change the default CsWinRTMarshallingMode from 'all' to 'minimal', so the interop generator analyzes every assembly (even those not targeting a Windows TFM) except the .NET base class library (BCL) by default. This keeps the usability win for plain .NET projects while avoiding the binary-size and analysis cost of crawling the BCL. The enum members are kept ordered from least to most restrictive (All, Minimal, Strict); the default is expressed via [DefaultValue] and the MSBuild/task defaults. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/skills/interop-generator/SKILL.md | 6 +++--- docs/usage.md | 15 ++++++++------- .../Microsoft.Windows.CsWinRT.CsWinRTGen.targets | 12 ++++++------ nuget/readme.md | 2 +- .../RunCsWinRTInteropGenerator.cs | 4 ++-- .../Generation/CsWinRTMarshallingMode.cs | 4 ++-- .../Generation/InteropGeneratorArgs.cs | 4 ++-- 7 files changed, 24 insertions(+), 23 deletions(-) diff --git a/.github/skills/interop-generator/SKILL.md b/.github/skills/interop-generator/SKILL.md index 3261e3151..d4aaab897 100644 --- a/.github/skills/interop-generator/SKILL.md +++ b/.github/skills/interop-generator/SKILL.md @@ -230,7 +230,7 @@ The generator is invoked via a response file: `cswinrtinteropgen.exe @path/to/re | `--winrt-component-assembly-path` | `string?` | `WinRT.Component.dll` path (for authored components) | | `--generated-assembly-directory` | `string` | Output folder for `WinRT.Interop.dll` | | `--use-windows-ui-xaml-projections` | `bool` | Use UWP XAML (`Windows.UI.Xaml`) instead of WinUI | -| `--marshalling-mode` | `CsWinRTMarshallingMode` | Which assemblies to analyze for discovery: `All` (default), `Minimal` (skip BCL), or `Strict` (only WinRT-referencing) | +| `--marshalling-mode` | `CsWinRTMarshallingMode` | Which assemblies to analyze for discovery: `All`, `Minimal` (default; skip BCL), or `Strict` (only WinRT-referencing) | | `--validate-winrt-runtime-assembly-version` | `bool` | Check version compatibility | | `--validate-winrt-runtime-dll-version-2-references` | `bool` | Reject CsWinRT 2.x references | | `--enable-incremental-generation` | `bool` | Enable incremental generation (caching) | @@ -284,8 +284,8 @@ The generator processes two categories of assemblies: - `WinRT.Component.dll` — Authored component projections (optional) **Marshalling mode** (`--marshalling-mode`, `ShouldProcessModule` in `InteropGenerator.Discover.cs`): controls which modules are analyzed for discovery. Modules referencing the Windows Runtime assembly (i.e. those built targeting a Windows TFM) are **always** analyzed; the mode only affects modules that don't reference any CsWinRT assembly: -- `All` (default) — analyze every module, even plain `.NET` assemblies. This lets projects that don't target a Windows TFM (e.g. a class library with just MVVM viewmodels) contribute the marshalling code their types need. -- `Minimal` — analyze every module except those from the BCL (detected via well-known .NET/Microsoft public key tokens, see `BaseClassLibraryIdentity` in `WinRT.Generator.Core` and the `ModuleDefinition.IsBaseClassLibraryModule` extension), to reduce binary size. +- `All` — analyze every module, even plain `.NET` assemblies (including the BCL). +- `Minimal` (default) — analyze every module except those from the BCL (detected via well-known .NET/Microsoft public key tokens, see `BaseClassLibraryIdentity` in `WinRT.Generator.Core` and the `ModuleDefinition.IsBaseClassLibraryModule` extension), to reduce binary size. This still lets projects that don't target a Windows TFM (e.g. a class library with just MVVM viewmodels) contribute the marshalling code their types need. - `Strict` — only analyze modules referencing the Windows Runtime assembly (the historical behavior). Corresponds to `!ReferencesWindowsRuntimeAssembly && !IsWindowsRuntimeModule → skip`. **Type exclusions** (`Helpers/TypeExclusions.cs`): diff --git a/docs/usage.md b/docs/usage.md index d91583344..2873e07e5 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -136,25 +136,26 @@ Below are the most commonly used MSBuild properties. For a full list, refer to t | `CsWinRTGenerateProjection` | `true` | Generate C# projection sources from `.winmd` metadata. | | `CsWinRTGenerateReferenceProjection` | `false` | Generate reference-only projections (for NuGet distribution). | | `CsWinRTComponent` | `false` | Enable Windows Runtime component authoring mode. | -| `CsWinRTMarshallingMode` | `all` | Controls which assemblies the interop generator analyzes for marshalling code (`all`, `minimal`, or `strict`). See below. | +| `CsWinRTMarshallingMode` | `minimal` | Controls which assemblies the interop generator analyzes for marshalling code (`all`, `minimal`, or `strict`). See below. | | `CsWinRTIncludes` | *(empty)* | Semicolon-separated namespaces to include in the projection. | | `CsWinRTExcludes` | `Windows;Microsoft` | Semicolon-separated namespaces to exclude from the projection. | | `CsWinRTMessageImportance` | `normal` | Build message verbosity (`normal` or `high`). | ### Marshalling mode -By default (`all`), the interop generator analyzes **every** assembly in the app to discover user-defined +By default (`minimal`), the interop generator analyzes **every** assembly in the app to discover user-defined (CCW) types and generic instantiations that need marshalling code — even assemblies that don't target a -Windows TFM and don't reference any CsWinRT assembly. This means a plain `.NET` class library (e.g. one -holding MVVM viewmodels) no longer needs to target a `-windows` TFM just so its types can be marshalled to -native code. +Windows TFM and don't reference any CsWinRT assembly — with the exception of the .NET base class library +(BCL). This means a plain `.NET` class library (e.g. one holding MVVM viewmodels) no longer needs to target +a `-windows` TFM just so its types can be marshalled to native code, while the BCL is skipped to keep the +generated interop assembly small. `CsWinRTMarshallingMode` controls this behavior: | Value | Behavior | |-------|----------| -| `all` (default) | Analyze all assemblies, including those that don't reference any CsWinRT assembly. | -| `minimal` | Same as `all`, but skip assemblies from the .NET base class library (BCL) to reduce binary size. | +| `all` | Analyze all assemblies, including those from the .NET base class library (BCL). | +| `minimal` (default) | Same as `all`, but skip assemblies from the .NET base class library (BCL) to reduce binary size. | | `strict` | Only analyze assemblies referencing the Windows Runtime assembly (i.e. those targeting a Windows TFM). | Assemblies that reference the Windows Runtime assembly are always analyzed, regardless of the mode. diff --git a/nuget/Microsoft.Windows.CsWinRT.CsWinRTGen.targets b/nuget/Microsoft.Windows.CsWinRT.CsWinRTGen.targets index 1911a60a7..6ec225a49 100644 --- a/nuget/Microsoft.Windows.CsWinRT.CsWinRTGen.targets +++ b/nuget/Microsoft.Windows.CsWinRT.CsWinRTGen.targets @@ -48,15 +48,15 @@ Copyright (C) Microsoft Corporation. All rights reserved. - all + minimal