Skip to content

Add CsWinRTMarshallingMode option for non-Windows TFM marshalling#2482

Open
Sergio0694 wants to merge 14 commits into
staging/3.0from
user/sergiopedri/cswinrt-marshalling-mode
Open

Add CsWinRTMarshallingMode option for non-Windows TFM marshalling#2482
Sergio0694 wants to merge 14 commits into
staging/3.0from
user/sergiopedri/cswinrt-marshalling-mode

Conversation

@Sergio0694

@Sergio0694 Sergio0694 commented Jul 6, 2026

Copy link
Copy Markdown
Member

Summary

Add a new CsWinRTMarshallingMode MSBuild option (default minimal) that lets the interop generator (cswinrtinteropgen) analyze assemblies that don't target a -windows TFM (i.e. don't reference any CsWinRT assembly) when discovering user-defined/CCW types and generic instantiations. Also add a CsWinRTMarshallingEnabledAssembly item for opting in specific assemblies regardless of the mode.

Motivation

Today, the interop generator only harvests marshalling info from .dlls that reference the Windows Runtime assembly (i.e. those built targeting a -windows TFM). This forces developers to put any type that needs to be marshalled to native (e.g. MVVM viewmodels implementing WinRT interfaces) into a -windows project, even when that project conceptually shouldn't target Windows at all and doesn't use any Windows API. Ideally, a project with just viewmodels should be able to target plain .NET.

This change removes that restriction: by default, the generator now analyzes all (non-BCL) assemblies for user-defined/CCW/generic types, regardless of their TFM, so marshalling those types just works without forcing a -windows TFM.

Behavior and modes

CsWinRTMarshallingMode supports three modes:

  • 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). This is the previous behavior.

Assemblies that reference the Windows Runtime assembly are always analyzed, regardless of the mode. Assemblies targeting a legacy/portable runtime (.NET Standard or .NET Framework) are never analyzed (see below). BCL/framework assemblies are identified by their well-known Microsoft public key tokens.

Opting in specific assemblies

The CsWinRTMarshallingEnabledAssembly item forces the generator to analyze specific assemblies regardless of the mode. This enables fine-tuning binary size — e.g. keeping strict mode while opting in a few assemblies known to be needed, without switching to minimal (which increases binary size more):

<PropertyGroup>
  <CsWinRTMarshallingMode>strict</CsWinRTMarshallingMode>
</PropertyGroup>
<ItemGroup>
  <CsWinRTMarshallingEnabledAssembly Include="MyApp.ViewModels" />
</ItemGroup>

Three diagnostics validate the opt-in entries:

  • CSWINRTINTEROPGEN0098 (warning): an entry doesn't match any referenced assembly.
  • CSWINRTINTEROPGEN0099 (message): an entry already targets Windows, so it's redundant.
  • CSWINRTINTEROPGEN0100 (message): the mode is all, so all entries are redundant.

Skipping legacy-runtime assemblies (fixes a regression from the new default)

Defaulting to minimal means the generator now analyzes third-party libraries that target .NET Standard / .NET Framework (e.g. Roslyn and BenchmarkDotNet, referenced by SourceGenerator2Test and Benchmarks). The entire interop generator infrastructure identifies well-known types — including custom-mapped types such as IEnumerable<T> — by comparing against type references scoped to the modern .NET corlib (e.g. System.Runtime), which the emit phase also uses. Legacy-runtime assemblies declare those same types against a different corlib (netstandard/mscorlib), which AsmResolver's SignatureComparer treats as a distinct scope — so the generator can't match them, and generating marshalling code for them failed with CSWINRTINTEROPGEN0055/0020. Such assemblies could in principle still use custom-mapped types that need marshalling, but for simplicity they're now skipped during discovery via a new ModuleDefinition.TargetsLegacyRuntime check.

Changes

  • src/WinRT.Generator.Core/: add case-insensitive, name-based parsing/serialization of enum-typed response file arguments; BaseClassLibraryIdentity (identify BCL assemblies by public key token); and the shared WellKnownGeneratorMessage type (informational build messages, logged through the ConsoleAppFramework logger).
  • src/WinRT.Interop.Generator/: add the CsWinRTMarshallingMode enum + --marshalling-mode argument (default Minimal), the --marshalling-enabled-assembly-names opt-in argument and its diagnostics, the IsBaseClassLibraryModule / TargetsLegacyRuntime module helpers, and the ShouldProcessModule / ValidateMarshallingEnabledAssemblies discovery logic.
  • src/WinRT.Generator.Tasks/RunCsWinRTInteropGenerator.cs: add the MarshallingMode and MarshallingEnabledAssemblies task parameters, with validation and response-file emission.
  • nuget/Microsoft.Windows.CsWinRT.CsWinRTGen.targets: add the CsWinRTMarshallingMode property (default minimal) and the CsWinRTMarshallingEnabledAssembly item, plumbed to the task and the generator's property-inputs cache hash.
  • docs/usage.md, nuget/readme.md, .github/skills/interop-generator/SKILL.md, .github/copilot-instructions.md: document the new options, diagnostics, and behavior.

Testing

All affected tooling projects build clean in Release (warnings-as-errors, 0 warnings). The full argument pipeline (item → task → response file → parser → discovery) was validated end-to-end against the built cswinrtinteropgen. The corlib-scope root cause and the TargetsLegacyRuntime fix were validated against real net10.0 (System.Runtime) and .NET Standard 2.0 (netstandard) assemblies.

Sergio0694 and others added 5 commits July 6, 2026 15:02
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>
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>
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>
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>
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>
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>
@Sergio0694 Sergio0694 force-pushed the user/sergiopedri/cswinrt-marshalling-mode branch from 4fe50b6 to 77e5823 Compare July 6, 2026 23:07
Sergio0694 and others added 8 commits July 6, 2026 16:20
Move the 'DefaultMarshallingMode' constant and 'ValidMarshallingModes'
static field to the top of the class (constants and static fields before
instance members), and quote the mode string literals in the XML docs
(e.g. <c>"all"</c>) to make clear they are string values.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
With the marshalling mode defaulting to 'minimal', the interop generator now
analyzes assemblies that don't reference the Windows Runtime, including
third-party libraries that target .NET Standard or .NET Framework (e.g. Roslyn
and BenchmarkDotNet, referenced by the SourceGenerator2Test and Benchmarks
projects). Such assemblies are scoped to a legacy corlib ('netstandard' or
'mscorlib'), while the emit phase builds its references from the application's
modern .NET runtime corlib ('System.Runtime'). Because AsmResolver's
'SignatureComparer' treats these corlib scopes as distinct, generic collection
instantiations from those assemblies (e.g. 'IEnumerator<char>',
'IEnumerator<object>') failed the emit-time well-known interface IID lookup,
producing CSWINRTINTEROPGEN0055/0020 errors.

Legacy/portable-runtime assemblies cannot reference the Windows Runtime 3.0
projections in the first place, so they never contain marshalling-relevant
types. Skip them during discovery via the new 'ModuleDefinition.TargetsLegacyRuntime'
check. This restores the pre-marshalling-mode behavior for such assemblies
(previously only Windows-Runtime-referencing assemblies, which always target a
modern .NET runtime, were ever analyzed).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Add the 'CsWinRTMarshallingEnabledAssembly' MSBuild item, which lets users
force the interop generator to analyze specific assemblies for discovery,
regardless of the marshalling mode. This enables fine-tuning for size: for
example, using the 'strict' mode while opting in a few specific assemblies
known to be needed, without switching to 'minimal' (which increases binary
size more).

The item flows through the RunCsWinRTInteropGenerator task as the
'--marshalling-enabled-assembly-names' argument, and is matched (by assembly
name, ignoring the '.dll' extension and any directory) in 'ShouldProcessModule'
so opted-in assemblies are always analyzed.

Add three diagnostics for the opt-in entries:
  - CSWINRTINTEROPGEN0098 (warning): an entry doesn't match any referenced
    assembly (likely a typo or stale reference).
  - CSWINRTINTEROPGEN0099 (message): an entry already targets Windows and is
    therefore always analyzed, so it's redundant and can be removed.
  - CSWINRTINTEROPGEN0100 (message): the marshalling mode is 'all' (which
    already analyzes everything), so all entries are redundant.

Add a WellKnownInteropMessage type (surfaced by MSBuild as a build message
rather than a warning) to support the two informational diagnostics.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Document the new CsWinRTMarshallingEnabledAssembly opt-in item in docs/usage.md
and nuget/readme.md, and update the interop generator skill with the new
--marshalling-enabled-assembly-names argument, the three opt-in diagnostics
(CSWINRTINTEROPGEN0098-0100), and the legacy-runtime skip behavior. Also bump
the interop generator error ID range in the copilot instructions.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Move the inline 'MarshallingEnabledAssemblies' comma-joining logic in
RunCsWinRTInteropGenerator into a new AppendResponseFileOptionalCommand overload
(taking an ITaskItem[]), keeping GenerateResponseFileCommands easy to read and
consistent with the other AppendResponseFile* helpers.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Move WellKnownInteropMessage from the interop generator to the shared
WinRT.Generator.Core project (renamed WellKnownGeneratorMessage) so all
generators can use it. Its Log method now writes through a provided logger
delegate (ConsoleApp.Log from ConsoleAppFramework) instead of calling
Console.WriteLine directly, so any future change to that logger is reflected
here too.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The previous rationale for skipping legacy/portable-runtime assemblies was
inaccurate: such assemblies can still use custom-mapped types (e.g.
'IEnumerable<T>') that would need marshalling. The actual reason the skip is
relevant is that the entire interop generator infrastructure identifies
well-known types by comparing against type references scoped to the modern .NET
corlib (e.g. 'System.Runtime'), which the emit phase also uses. Legacy-runtime
assemblies declare those types against a different corlib ('netstandard' or
'mscorlib'), which AsmResolver's SignatureComparer treats as a distinct scope,
so the generator can't match them. Update the TargetsLegacyRuntime doc comment,
the discovery comment, and the usage/skill docs accordingly.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The 'TargetsLegacyRuntime' check is part of the 'should this module be analyzed'
decision, so move it (and its explanatory comment) from LoadAndProcessModule
into ShouldProcessModule, keeping LoadAndProcessModule easier to read. The check
runs first (before the opt-in check), preserving the existing behavior that
legacy-runtime assemblies are skipped even when explicitly opted in.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant