Skip to content

Emit debug information for generated forwarder assemblies - #2540

Open
Sergio Pedri (Sergio0694) wants to merge 3 commits into
staging/3.0from
user/sergiopedri/forwarder-debug-symbols
Open

Emit debug information for generated forwarder assemblies#2540
Sergio Pedri (Sergio0694) wants to merge 3 commits into
staging/3.0from
user/sergiopedri/forwarder-debug-symbols

Conversation

@Sergio0694

Copy link
Copy Markdown
Member

Summary

The forwarder assembly that cswinrtimplgen generates for a reference projection shipped with a completely empty PE debug directory. That assembly is what lands in lib/<tfm> of a reference projection NuGet package, so every such package reported as having no symbols at all. cswinrtimplgen now synthesizes the debug information for the forwarder, so it ships with an embedded portable PDB, Source Link, compiler flags, and a Reproducible marker.

Motivation

NuGet Package Explorer (and the other tooling that reads the same data) validates a package by inspecting only the assemblies under lib/ and runtimes/, and reports the worst case across them. For a reference projection package that is exactly one file: the forwarder. Because it carried no debug directory entries at all, the package health report came back as:

Source Link:             Missing Symbols
Deterministic (dll/exe): Non deterministic
Compiler Flags:          Missing

All three rows are red for a single reason: when an assembly yields no symbols, the deterministic and compiler-flag results are force-set to match rather than being evaluated independently. The ref/<tfm> reference assembly and the metadata/*.winmd files are never inspected, so the forwarder is the only actionable file.

The forwarder had no symbols because it is emitted directly as metadata by cswinrtimplgen (via AsmResolver) rather than compiled, and a freshly constructed module is written with an empty debug directory. There is also nothing to carry over from the input assembly: reference projections are compiled with ProduceOnlyReferenceAssembly, and a reference-only compilation emits no debug information whatsoever.

That last point rules out the obvious alternative of relaxing ProduceOnlyReferenceAssembly so the compiler produces a PDB to propagate. Reference projections are only ever meant to be compiled /refonly: their generated constructors implicitly call the WindowsRuntimeObject() constructor, which exists only in the WinRT.Runtime reference assembly and whose body is throw null. Compiling those bodies for real fails to bind against the implementation assembly (over 6000 CS1729 errors when building src/Projections/Windows), and would otherwise emit IL calling a constructor that throws MissingMethodException at runtime. So the debug information has to be synthesized instead.

Changes

  • src/WinRT.Impl.Generator/Writers/PortablePdbWriter.cs: new. Builds a portable PDB for the forwarder using System.Reflection.Metadata, containing one embedded source document, the compilation options, and the compilation metadata references. The PDB id and checksum are both derived from a single content hash, so they stay consistent and the output is a pure function of its input.

  • src/WinRT.Impl.Generator/Writers/TypeForwardsDocumentWriter.cs: new. Renders the document that the PDB embeds: an auto-generated C# file listing every type forward in the assembly as the [assembly: TypeForwardedTo(...)] that would produce it, along with the projection assembly each one resolves to. The forwarder has no source on disk, so this is written to be an accurate, human readable description of the entire contents of the assembly.

  • src/WinRT.Impl.Generator/Writers/DebugDirectoryWriter.cs: new. Emits the debug directory entries, matching what a deterministic build with embedded symbols produces: CodeView, PdbChecksum, Reproducible, and EmbeddedPortablePdb.

  • src/WinRT.Impl.Generator/Generation/ImplGenerator.cs: writes the assembly through ToPEImage() and ManagedPEFileBuilder instead of the ModuleDefinition.Write(path) convenience overload (which is what that overload does anyway), so the debug directory can be populated before the file is written.

  • src/Tests/ImplGeneratorTest/: new test project, following the same shape as WinMDGeneratorTest: it builds cswinrtimplgen and runs it end-to-end as a separate process. Test_DebugDirectory covers the debug directory entries, their mutual consistency, the embedded document and its deterministic name, the compiler flags, byte-for-byte determinism across runs, and that symbols survive strong naming.

  • src/Tests/SmokeTests/run-smoke-tests.ps1: the shared reference projection verification now also asserts the forwarder ships embedded symbols, so this is covered end-to-end against a real built package.

  • build/AzurePipelineTemplates/CsWinRT-Test-Steps.yml: runs the new test project, gated to x64 like the other generator tests.

  • src/cswinrt.slnx, .github/copilot-instructions.md, .github/skills/testing/SKILL.md: register and document the new project and behavior.

Notes

Everything written into the PDB is derived from the forwarder itself, so the result stays deterministic: two runs produce byte-identical output, which is what makes the Reproducible entry honest. The MetadataReferences entries deliberately record a zero timestamp, image size and MVID: those identify the exact image a reference resolved to, and the projection assemblies the forwarder points at do not exist until the consuming application is built, so there is nothing truthful to record.

The size cost is small even in the worst case. For the full Windows SDK projection (5251 type forwards, an 872 KB generated document) the embedded PDB adds roughly 40 KB.

Verified end to end by building src/Projections/Windows and checking the resulting forwarder against the same rules the package health check applies:

DebugDirectory : CodeView, PdbChecksum, Reproducible, EmbeddedPortablePdb
Documents      : 1 (embedded: 1)
CompilerFlags  : 6 (version=2)

Source Link             : Valid
Deterministic (dll/exe) : Valid
Compiler Flags          : Valid

One thing found along the way but deliberately left alone, as it is pre-existing and unrelated: the generator signs the forwarder after writing it, but does not set the StrongNameSigned COR flag. The signature itself is valid (sn -vf passes), and this behavior is identical before and after this change.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

@Sergio0694
Sergio Pedri (Sergio0694) force-pushed the user/sergiopedri/forwarder-debug-symbols branch from 10433fc to ccddf40 Compare August 31, 2026 10:53
The forwarder assembly produced by 'cswinrtimplgen' is emitted directly as
metadata rather than compiled, so it shipped with a completely empty PE debug
directory. Since that assembly is what lands in 'lib/<tfm>' of a reference
projection NuGet package, the whole package reported as having no symbols, and
no Source Link, determinism or compiler flag information.

There is nothing to carry over from the input either: reference projections are
compiled with 'ProduceOnlyReferenceAssembly', and a reference-only compilation
emits no debug information at all.

The generator now synthesizes the debug information itself, emitting the same
debug directory a deterministic build with embedded symbols produces, with an
embedded portable PDB carrying a single generated document describing the type
forwards, plus compilation options and metadata references.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Avoids encoding (and allocating) a constant string on every run.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The smoke tests inspect the built assemblies through
'System.Reflection.PortableExecutable.PEReader', which comes from
'System.Reflection.Metadata'. That assembly does not ship with Windows
PowerShell, so the reference projection smoke tests failed with a TypeNotFound
error when run under it.

Run them on PowerShell 7 instead, which has it in the box. 'build.cmd' now also
checks for 'pwsh' up front, so a machine without it gets an actionable message
rather than a failure from inside the tests.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@Sergio0694
Sergio Pedri (Sergio0694) force-pushed the user/sergiopedri/forwarder-debug-symbols branch from ccddf40 to f0e32c4 Compare August 31, 2026 13:45
@Sergio0694
Sergio Pedri (Sergio0694) marked this pull request as ready for review August 31, 2026 13:45
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CsWinRT 3.0 debugging Improvements to debugging experience tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant