Skip to content

Port WindowsRuntime.Internal.idl to C# in new WinRT.Internal project#2429

Open
Sergio0694 wants to merge 11 commits into
staging/CodeWritersfrom
user/sergiopedri/csharp-internal
Open

Port WindowsRuntime.Internal.idl to C# in new WinRT.Internal project#2429
Sergio0694 wants to merge 11 commits into
staging/CodeWritersfrom
user/sergiopedri/csharp-internal

Conversation

@Sergio0694

Copy link
Copy Markdown
Member

Summary

Port the contents of src/cswinrt/WindowsRuntime.Internal.idl (the
hand-authored Windows Runtime metadata that the legacy C++ cswinrt
tool used to compile into WindowsRuntime.Internal.winmd) to a new
C# WinRT.Internal project, and wire the existing C# WinMD generator
in as the producer of the .winmd. The new WinRT.Internal project
now owns this artifact end-to-end so the CsWinRT toolchain no longer
needs the legacy C++ cswinrt project to be built at all.

Motivation

The legacy C++ cswinrt project does two things: it produces
cswinrt.exe (the old projection compiler) and it compiles
WindowsRuntime.Internal.idl into WindowsRuntime.Internal.winmd.
The first responsibility has already been ported to C# (the
WinRT.Projection.Writer library + cswinrtprojectiongen.exe); the
second was still tying the rest of the build to the C++ project.

Moving the .idl to a C# source-of-truth + the C# WinMD generator
removes that dependency. A follow-up PR can now delete src/cswinrt
entirely.

Changes

New WinRT.Internal C# project

  • src/WinRT.Internal/WinRT.Internal.csproj: targets
    net10.0-windows10.0.26100.1 (TFM revision .1 selects the
    cswinrt3 Windows SDK projection reference assemblies which carry
    [WindowsRuntimeMetadata] attributes the WinMD generator reads).
    Pins WindowsSdkPackageVersion so the SDK adds the matching
    framework reference. Disables all CsWinRT MSBuild processing
    (CsWinRTEnabled / CsWinRTGenerateProjection /
    CsWinRTGenerateInteropAssembly[2]) since this project itself
    feeds back into the CsWinRT toolchain. References the locally built
    WinRT.Runtime.dll directly via a ProjectReference Private="false".
  • src/WinRT.Internal/HWND.cs: struct counterpart of the IDL
    HWND (custom-mapped to nint by the projection writer).
  • src/WinRT.Internal/ProjectionInternalAttribute.cs: forward
    declaration of the [ProjectionInternal] marker the projection
    writer reads to emit internal projections.
  • src/WinRT.Internal/I*Interop.cs: one file per interop
    interface, all 14 of the IDL ones
    (IAccountsSettingsPaneInterop, IDragDropManagerInterop,
    IInputPaneInterop, IPlayToManagerInterop,
    IPrintManagerInterop, IRadialControllerInterop,
    IRadialControllerConfigurationInterop,
    IRadialControllerIndependentInputSourceInterop,
    ISpatialInteractionManagerInterop,
    ISystemMediaTransportControlsInterop,
    IUIViewSettingsInterop, IUserConsentVerifierInterop,
    IWebAuthenticationCoreManagerInterop,
    IDisplayInformationStaticsInterop) with their original IIDs,
    parameter ordering, and reference links to the corresponding
    Win32 *interop.idl documentation pages.

WinMD generator improvements

Two small fixes to WinRT.WinMD.Generator were needed for the output
to be functionally equivalent to the C++ cswinrt-generated .winmd:

  • src/WinRT.WinMD.Generator/Writers/WinMDWriter.Members.cs:
    GetWinRTParameterAttributes now preserves the input parameter's
    [In] / [Out] flags rather than unconditionally emitting [Out]
    for any by-reference parameter. When no flag is set on the input,
    byref params default to [In] (matching MIDL's convention for
    ref const T), keeping the projection writer's
    ParameterCategoryResolver classifying ref Guid riid as
    ParameterCategory.Ref rather than Out.
  • src/WinRT.WinMD.Generator/Writers/WinMDWriter.cs +
    src/WinRT.WinMD.Generator/References/WinMDValues.cs:
    The authored .winmd assembly identity is now always stamped
    255.255.255.255 (the WinRT "unbound" convention used by every
    Windows Runtime metadata assembly). The --assembly-version
    argument continues to drive the type-level
    [Windows.Foundation.Metadata.Version] attribute. Verified against
    the existing TestComponent / TestComponentCSharp .winmd
    outputs, both of which carry v255.255.255.255 regardless of the
    project's <AssemblyVersion>.

MSBuild integration

  • src/WinRT.Internal/WinRT.Internal.csproj (target
    GenerateWindowsRuntimeInternalWinMD): writes a response file to
    $(IntermediateOutputPath) and invokes cswinrtwinmdgen.exe
    directly via <Exec> (instead of a UsingTask). This avoids the
    MSB3027 file-lock contention that happens in Visual Studio when
    the persistent MSBuild build server keeps a custom-task assembly
    loaded across incremental builds — MSBuild never needs to load
    WinRT.Generator.Tasks.dll for this project at all.

Build infrastructure rewiring

  • src/Directory.Build.props: $(CsWinRTInteropMetadata) now
    resolves to
    src/WinRT.Internal/bin/$(Configuration)/net10.0-windows10.0.26100.1/WindowsRuntime.Internal.winmd.
    $(AssemblyVersionNumber) defaults to 3.0.0.0 so dotnet build
    works locally without explicit /p:AssemblyVersionNumber=....
    Legacy $(CsWinRTPath) / $(CsWinRTExe) are kept defined as
    path strings (the .exe no longer needs to exist; they will be
    removed entirely once src/cswinrt is dropped).
  • src/build.cmd: interop_winmd now points at the WinRT.Internal
    output instead of _build/.../cswinrt/bin/.
  • build/AzurePipelineTemplates/CsWinRT-Build-Steps.yml: the
    "Stage WindowsRuntime.Internal.winmd" task copies from
    src/WinRT.Internal/bin/.../net10.0-windows10.0.26100.1/ rather
    than the cswinrt bin folder; downstream staging into
    $(StagingFolder)/native is unchanged so the nuget pack step
    still finds it at the same place.
  • src/cswinrt.slnx: WinRT.Internal is included in the
    default solution-level build.

Verification

  • The new WindowsRuntime.Internal.winmd contains exactly the same
    set of public types (HWND + 14 interop interfaces +
    ProjectionInternalAttribute) with identical IIDs and method
    counts as the legacy C++-generated WindowsRuntime.Internal.winmd.
  • dotnet build src/WinRT.Sdk.Projection/... successfully consumes
    the new .winmd and produces a WinRT.Sdk.Projection.dll
    containing the expected
    ABI.WindowsRuntime.Internal.IAccountsSettingsPaneInteropMethods
    internal projection alongside the regular
    Windows.UI.ApplicationSettings.AccountsSettingsPane runtime
    class, confirming the full interop pipeline still works.
  • Four back-to-back incremental builds (including ones that force
    rebuilds of WinRT.Generator.Tasks.dll and cswinrtwinmdgen.exe
    themselves while the build session is alive) all complete cleanly
    with 0 errors — no MSB3027 file-lock issues.

Follow-up

The legacy src/cswinrt directory is still on disk for now (no
consumer depends on it being built). It will be deleted entirely in
a follow-up PR.

Sergio0694 and others added 10 commits June 7, 2026 17:23
Add a new SDK project src/WinRT.Internal/WinRT.Internal.csproj targeting net10.0 with implicit usings and nullable enabled. Also update cswinrt.slnx to include the new project (solution entry: WinRT.Internal/Windows.Internal.csproj).
Port the contents of src/cswinrt/WindowsRuntime.Internal.idl to C#
source files in the new src/WinRT.Internal project, in preparation for
removing the legacy C++ cswinrt project. The C# definitions cover:

- HWND struct (custom-mapped to System.IntPtr by the projection writer).
- [ProjectionInternal] custom attribute (consumed by CsWinRT to emit
  internal projections for the marked interfaces).
- All 14 interop interfaces (IAccountsSettingsPaneInterop,
  IDisplayInformationStaticsInterop, IDragDropManagerInterop,
  IInputPaneInterop, IPlayToManagerInterop, IPrintManagerInterop,
  IRadialControllerConfigurationInterop,
  IRadialControllerIndependentInputSourceInterop,
  IRadialControllerInterop, ISpatialInteractionManagerInterop,
  ISystemMediaTransportControlsInterop, IUIViewSettingsInterop,
  IUserConsentVerifierInterop, IWebAuthenticationCoreManagerInterop)
  with their original IIDs, parameter lists, and reference links
  to the corresponding Win32 *interop.idl documentation pages.

Each interface carries [Guid(...)] (from System.Runtime.InteropServices)
and [ProjectionInternal], and exposes the same methods as the IDL with
the same parameter order. The "riid" out-parameter selector uses
"ref Guid riid" rather than "in" / "[In] ref" so that the C# compiler
emits a plain "Guid&" byref without "modreq(InAttribute)" /
"modopt(IsReadOnlyAttribute)" decorations.

The csproj targets "net10.0-windows10.0.26100.1" (revision .1 selects
the cswinrt3 Windows SDK projection reference assemblies, which carry
[WindowsRuntimeMetadata] attributes the WinMD generator reads) and
pins WindowsSdkPackageVersion to the matching SDK NET Ref package so
the implicit FrameworkReference resolves. All CsWinRT MSBuild
processing (CsWinRTEnabled / CsWinRTGenerateProjection /
CsWinRTGenerateInteropAssembly[2]) is disabled because this project
itself feeds back into the CsWinRT build pipeline. It references the
locally built WinRT.Runtime.dll directly via a HintPath so it does
not require the Microsoft.Windows.CsWinRT NuGet package.

The output .dll is only an intermediate artifact; the .winmd that the
CsWinRT toolchain consumes will be wired up in a follow-up commit.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…sion

Two small fixes to the WinMD generator needed for the C# port of
WindowsRuntime.Internal.idl to produce metadata compatible with both
the existing toolchain and the original C++-generated .winmd:

1. Preserve [In] / [Out] parameter direction flags from input
   parameters. Previously, GetWinRTParameterAttributes unconditionally
   returned ParameterAttributes.Out for any by-reference parameter,
   which is wrong for "ref Guid riid"-style COM interop signatures
   that MIDL emits as [in] ref. The new logic honors any explicit
   In/Out flag already set by the C# compiler on the input parameter,
   and only falls back to a type-based heuristic when no flag is set
   (Span<T> -> [out], everything else including byref -> [in]).
   This matches MIDL's convention for "ref const T" parameters and
   keeps the projection writer's ParameterCategoryResolver classifying
   them as Ref rather than Out.

2. Always stamp the .winmd assembly identity with version
   255.255.255.255 (the WinRT "unbound" convention used by every
   Windows Runtime metadata assembly). The "--assembly-version"
   argument is still used as-is for the type-level
   "[Windows.Foundation.Metadata.Version]" attribute applied to each
   authored type. Verified against the existing TestComponent and
   TestComponentCSharp .winmd outputs, both of which carry
   v255.255.255.255 as their assembly identity regardless of the
   project's <AssemblyVersion>.

Also bumps the WinRT.Internal csproj to use AssemblyName
"WindowsRuntime.Internal" so the produced .dll (and its .winmd) match
the identity that the rest of the CsWinRT toolchain expects.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds an MSBuild target to WinRT.Internal.csproj that invokes
'cswinrtwinmdgen.exe' (built from this solution) after CoreCompile and
emits 'WindowsRuntime.Internal.winmd' next to the compiled
'WindowsRuntime.Internal.dll' in '$(TargetDir)'.

Adds ProjectReference items to WinRT.WinMD.Generator (the tool
'cswinrtwinmdgen.exe') and WinRT.Generator.Tasks (the
RunCsWinRTWinMDGenerator MSBuild task) with
ReferenceOutputAssembly=false so they participate in the build graph
without becoming runtime references. The 'UsingTask' resolves the task
assembly out of the locally built WinRT.Generator.Tasks output, and
the tool path is computed from the locally built WinRT.WinMD.Generator
output. 'CsWinRTToolsArchitecture=AnyCPU' is passed so the tool task
uses the top-level 'cswinrtwinmdgen.exe' apphost wrapper (no per-arch
publish folder).

Verified: the produced 'WindowsRuntime.Internal.winmd' contains
exactly the same set of public types as the legacy C++-generated
'WindowsRuntime.Internal.winmd' (HWND struct, all 14 interop
interfaces with their original IIDs and method counts, and the
ProjectionInternalAttribute class). The only extra entry is a
synthesized 'IProjectionInternalAttributeClass' interface that the
C# WinMD generator emits for attribute classes; it is non-public and
is filtered out by the projection writer's
'AssemblyAnalyzer.DiscoverPublicTypes' step.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Now that WinRT.Internal produces WindowsRuntime.Internal.winmd via the
C# WinMD generator, point all build infrastructure at the new file
instead of the legacy cswinrt (C++) bin directory. The output cswinrt
project still exists on disk for now but no longer needs to be built
for any consumer of WindowsRuntime.Internal.winmd to function; that
project will be deleted in a follow-up commit.

Specifically:

- src/Directory.Build.props:
  - 'CsWinRTInteropMetadata' now resolves to
    'src/WinRT.Internal/bin/$(Configuration)/$(CsWinRTInternalTargetFramework)/WindowsRuntime.Internal.winmd'
    (with 'CsWinRTInternalTargetFramework' defaulting to
    'net10.0-windows10.0.26100.1', matching the WinRT.Internal csproj
    TFM that selects the cswinrt3 SDK projection reference assemblies).
  - 'AssemblyVersionNumber' now defaults to '3.0.0.0' (the WinRT
    runtime baseline for CsWinRT 3.0). Without this, dev builds of
    WinRT.Runtime would stamp v1.0.0.0 by default, and the cswinrt3
    SDK projection would fail to resolve against the local runtime.
  - 'CsWinRTPath' / 'CsWinRTExe' are kept defined (still pointing at
    the legacy cswinrt bin folder), but only as path strings: the C#
    'cswinrtprojectiongen.exe' tool no longer reads or executes the
    .exe path, so the file no longer needs to exist. They will be
    removed entirely once 'src/cswinrt' itself is dropped.

- src/build.cmd: 'interop_winmd' now points at the WinRT.Internal
  output instead of '_build/{platform}/{config}/cswinrt/bin/'.

- build/AzurePipelineTemplates/CsWinRT-Build-Steps.yml: the "Stage
  WindowsRuntime.Internal.winmd" task copies from
  'src/WinRT.Internal/bin/$(BuildConfiguration)/net10.0-windows10.0.26100.1/'
  rather than the cswinrt bin folder; staging into
  '$(StagingFolder)/native' is preserved so the downstream nuget pack
  step (CsWinRT-PublishToNuGet-Steps.yml) still finds the file at the
  same place.

- src/cswinrt.slnx: WinRT.Internal no longer carries '<Build
  Project="false" />' so it participates in default solution-level
  builds.

- src/WinRT.Internal/WinRT.Internal.csproj: switches from
  '<Reference HintPath="..." />' to '<ProjectReference Private="false"
  />' for WinRT.Runtime so that 'dotnet build' on this project alone
  implicitly builds WinRT.Runtime2 first (no copy of WinRT.Runtime.dll
  into bin).

Verified end-to-end: 'dotnet build src/WinRT.Sdk.Projection/...' now
consumes the new WindowsRuntime.Internal.winmd and produces
WinRT.Sdk.Projection.dll containing the expected
'ABI.WindowsRuntime.Internal.IAccountsSettingsPaneInteropMethods'
internal projection (alongside the regular projected
'Windows.UI.ApplicationSettings.AccountsSettingsPane' runtime class),
confirming the full interop pipeline still works.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Refactor several WinRT.Internal sources and adjust project settings:

- HWND.cs: add pragma to suppress naming warning, rename reserved field to __Reserved, document projection to nint and add reference link.
- Interop interfaces: normalize GUID literals to uppercase for IPrintManagerInterop, IRadialControllerConfigurationInterop, ISystemMediaTransportControlsInterop, and IUIViewSettingsInterop.
- ProjectionInternalAttribute.cs: add C# language tag to the code sample and convert the attribute type to a forward declaration (public sealed class ProjectionInternalAttribute : Attribute;).
- WinRT.Internal.csproj: clean up XML/comments, include BOM, disable nullable (Windows Runtime types don't support nullability annotations), adjust NoWarn entries, inline ProjectReference entries, and reformat UsingTask/Target elements.

These changes improve consistency, clarify projection mappings, and update build settings to match the Windows Runtime metadata requirements.
When Visual Studio (or the persistent 'dotnet build-server') loads
'WinRT.Generator.Tasks.dll' to run the 'RunCsWinRTWinMDGenerator'
custom MSBuild task, the main MSBuild process keeps the assembly
loaded for the lifetime of the build session. The 'ProjectReference'
from WinRT.Internal to WinRT.Generator.Tasks then triggers
WinRT.Generator.Tasks to rebuild on the next incremental build (or
the next 'right-click -> Build' in VS), which fails with:

  warning MSB3026: Could not copy ... WinRT.Generator.Tasks.dll ...
  The file is locked by: "MSBuild.exe (...) , MSBuild.exe (...), ..."
  error MSB3027: ... Exceeded retry count of 10.

Adding 'TaskFactory="TaskHostFactory"' to the 'UsingTask' loads the
task in a separate 'MSBuild.TaskHost' child process, which exits
after the build, so the main MSBuild process never keeps a handle
to 'WinRT.Generator.Tasks.dll'. Subsequent rebuilds can then
overwrite it without contention.

Verified with three back-to-back 'dotnet build' invocations that
each trigger 'CoreCompile' on WinRT.Internal (and, in the third
case, a rebuild of WinRT.Generator.Tasks itself): all complete
cleanly with 0 warnings / 0 errors and 'WindowsRuntime.Internal.winmd'
gets regenerated.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add explicit properties and invoke the WinMD generator task to produce this project's .winmd and ensure incremental builds recognize the generated file. Introduces _CsWinRTWinMDGenToolsDirectory and _CsWinRTInternalWinMDOutputPath, calls RunCsWinRTWinMDGenerator with those paths, and adds a FileWrites entry for the generated .winmd. Also reformats the UsingTask tag and adds inline comments for clarity.
The previous 'TaskFactory="TaskHostFactory"' attempt did not fully
resolve the file-lock issue in Visual Studio: VS keeps multiple
'MSBuild.exe' build-server processes alive across IntelliSense and
build sessions, and at least one of them ends up holding a handle to
'WinRT.Generator.Tasks.dll' even with the task host opt-in. The result
in VS is the same 'MSB3027' (file locked by multiple 'MSBuild.exe'
processes) error on every other build of WinRT.Internal.

The root issue is that this project depends on its own custom MSBuild
task ('RunCsWinRTWinMDGenerator' from WinRT.Generator.Tasks) and is
also part of the build graph that produces that task assembly via a
'ProjectReference'. Any time MSBuild loads the task DLL during a build
session and VS later tries to rebuild it, the copy fails.

Rather than chase that contention, this commit removes the custom-task
dependency for this project entirely:

- Drops the 'ProjectReference' to WinRT.Generator.Tasks (no longer
  needed).
- Drops the 'UsingTask' / 'RunCsWinRTWinMDGenerator' call.
- Invokes 'cswinrtwinmdgen.exe' directly via a stock '<Exec>' task
  with a generated '.rsp' response file passed as '@<file>' (the same
  format 'ConsoleAppFramework' / 'WinMDGeneratorArgs.ParseFromResponseFile'
  already understands).

The response file is written under '$(IntermediateOutputPath)' via
'<WriteLinesToFile WriteOnlyWhenDifferent="true">' and registered as
a 'FileWrites' item so it's tracked by the incremental-build cache.
The end-to-end behavior is identical to before — same arguments,
same generator, same '.winmd' output — but the build never loads
'WinRT.Generator.Tasks.dll' into MSBuild's process space, so it can
freely be rebuilt at any time.

Verified with four back-to-back 'dotnet build' invocations on the
same build server, each triggering a different point in the
dependency chain (HWND.cs source change -> rebuild of
'WinRT.Generator.Tasks.dll' itself -> rebuild of
'cswinrtwinmdgen.exe' itself), all completing with 0 errors and the
'.winmd' regenerated correctly each time.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Reformat and clarify WinRT.Internal.csproj: normalize header, collapse XML comment into a block, wrap ProjectReference attributes across lines for readability, tweak comment wording, add comments around preparing/writing the generator '.rsp' file, and mark the response and output files in FileWrites to help incremental builds. Also small doc fix in WinMDWriter: fully qualify ReadOnlySpan/Span as System.ReadOnlySpan/System.Span in XML docs to avoid ambiguity.
@Sergio0694 Sergio0694 added code cleanup Code cleanup and refactoring tooling CsWinRT 3.0 labels Jun 8, 2026
@Sergio0694 Sergio0694 requested a review from manodasanW June 8, 2026 02:01
Clarify and normalize in-code comments and formatting across the repo and remove a stale comment in the build script. Changes touch Directory.Build.props, WinMD.WinMD.Generator (WinMDValues.cs, WinMDWriter.Members.cs, WinMDWriter.cs) and build.cmd. All edits are comment/whitespace-level (consistent quoting, naming like 'Windows Runtime'/'WinRT.Runtime', and punctuation) and do not change runtime behavior.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

code cleanup Code cleanup and refactoring CsWinRT 3.0 tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant