CsWin32 follow-up: CLR metadata + TypeLib interop migration#13853
Conversation
Continues the CsWin32 struct-based COM migration from dotnet#13705 and dotnet#13746, this time covering the CLR metadata APIs (IMetaDataDispenser / IMetaDataImport2 / IMetaDataAssemblyImport) consumed by AssemblyInformation and ManifestUtil.MetadataReader, plus the ICreateTypeLib path used by RegisterAssembly. New files - src/Tasks/AssemblyDependency/Metadata/ - Metadata.cs folder header / namespace docs - IMetaDataDispenser.cs OpenScope with typed CorOpenFlags - IMetaDataImport2.cs GetCustomAttributeByName + GetPEKind with canonical inherited 3-72 slot map - IMetaDataAssemblyImport.cs GetAssemblyProps / GetAssemblyRefProps / GetFileProps / EnumAssemblyRefs / EnumFiles / GetAssemblyFromScope / CloseEnum - ASSEMBLYMETADATA.cs blittable struct from cor.h - OSINFO.cs blittable struct from cor.h - CorOpenFlags.cs typed [Flags] enum - CorAssemblyFlags.cs typed [Flags] enum - CorTokenType.cs 26 mdt* table-type tags - Tokens.cs MdToken / MdAssembly / MdAssemblyRef / MdFile readonly structs with validating constructors - src/Tasks/TypeLib/ICreateTypeLib.cs struct-based wrapper for RegisterAssembly - src/Tasks.UnitTests/MetadataReader_Tests.cs 12 tests / 24 cross-TFM (PEReader on net10, COM on net472), verifies identity parity for Accessibility.dll Modifications - AssemblyInformation.cs uses MdAssembly / BufferScope<char> / typed CorAssemblyFlags; OpenScope asks for IMetaDataImport2 directly (skips one QI); AllocAsmMeta / FreeAsmMeta eliminated - MetadataReader.cs net472 path migrated from [ComImport] to struct-based COM via AgileComPointer; failure to OpenScope returns null (per-file contract) - RegisterAssembly.cs uses the new ICreateTypeLib struct - Microsoft.Build.Tasks.csproj wildcard <Compile Include> for the Fusion / Metadata / TypeLib folders (FeatureWindowsInterop-gated) - NativeMethods.cs legacy [ComImport] declarations removed; only oleaut32 typelib P/Invokes remain; System.Runtime.Versioning using gated on FEATURE_WINDOWSINTEROP for source-build Skills - .github/skills/cswin32-com/SKILL.md adds 'Strongly-typed handle / token wrappers' with CLR validation table; pair-pointer to cswin32-interop; error- handling-parity guidance; IComIID polyfill heading rename - .github/skills/cswin32-interop/SKILL.md reorganized: Rules / Blittable signatures / Infrastructure / BufferScope<T> / Gotchas; source-build verification note Verified - Normal build: 0 warnings, 0 errors (net472 + net10.0) - Source build: 0 warnings, 0 errors (DotNetBuildSourceOnly=true DotNetBuild=true) - Tests: 24/24 pass on both TFMs
🔍 Skill Validator Results
Summary
Full validator output```text Found 2 skill(s) [cswin32-interop] 📊 cswin32-interop: 3,402 BPE tokens [chars/4: 3,195] (standard ~), 14 sections, 3 code blocks [cswin32-interop] ⚠ Skill is 3,402 BPE tokens (chars/4 estimate: 3,195) — approaching "comprehensive" range where gains diminish. [cswin32-com] 📊 cswin32-com: 4,200 BPE tokens [chars/4: 4,075] (standard ~), 13 sections, 6 code blocks [cswin32-com] ⚠ Skill is 4,200 BPE tokens (chars/4 estimate: 4,075) — approaching "comprehensive" range where gains diminish. ✅ All checks passed (2 skill(s)) ``` |
There was a problem hiding this comment.
Pull request overview
Migrates CLR metadata (IMetaData*) and typelib authoring (ICreateTypeLib) interop from [ComImport]/RCW-based calls to CsWin32-style struct-based COM, improving blittability and aligning Tasks/ManifestUtil with the newer ComScope<T> / AgileComPointer<T> infrastructure.
Changes:
- Adds new struct-based COM definitions for CLR metadata APIs and strongly-typed metadata token wrappers (
MdAssembly,MdAssemblyRef, etc.). - Updates
AssemblyInformationandManifestUtil.MetadataReaderto useCoCreateInstance,ComScope<T>, andAgileComPointer<T>instead of[ComImport]interfaces and manual marshalling. - Migrates
RegisterAssembly’sICreateTypeLib.SaveAllChangesusage to a struct-based COM call and removes legacy declarations fromNativeMethods.cs; adds unit tests for MetadataReader parity.
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/Tasks/TypeLib/ICreateTypeLib.cs | New struct-based COM wrapper for ICreateTypeLib (typelib persistence). |
| src/Tasks/RegisterAssembly.cs | Uses QI + struct-based ICreateTypeLib to call SaveAllChanges(). |
| src/Tasks/NativeMethods.cs | Removes legacy [ComImport] declarations for metadata + typelib interop. |
| src/Tasks/Microsoft.Build.Tasks.csproj | Switches to wildcard Compile includes for Fusion/Metadata/TypeLib interop folders under FeatureWindowsInterop. |
| src/Tasks/ManifestUtil/MetadataReader.cs | Replaces net472 COM RCW usage with CoCreateInstance + ComScope<T> + AgileComPointer<T>. |
| src/Tasks/AssemblyDependency/Metadata/Tokens.cs | Adds strongly-typed CLR metadata token wrappers and conversions. |
| src/Tasks/AssemblyDependency/Metadata/OSINFO.cs | Adds blittable OSINFO struct for CLR metadata APIs. |
| src/Tasks/AssemblyDependency/Metadata/Metadata.cs | Adds CorMetadata helper (CLSID constants) for activation. |
| src/Tasks/AssemblyDependency/Metadata/IMetaDataImport2.cs | Adds struct-based COM for IMetaDataImport2 with vtable slot mapping. |
| src/Tasks/AssemblyDependency/Metadata/IMetaDataDispenser.cs | Adds struct-based COM for IMetaDataDispenser::OpenScope. |
| src/Tasks/AssemblyDependency/Metadata/IMetaDataAssemblyImport.cs | Adds struct-based COM for IMetaDataAssemblyImport methods used by RAR/ManifestUtil. |
| src/Tasks/AssemblyDependency/Metadata/CorTokenType.cs | Adds typed CorTokenType table-tag enum for token decoding/validation. |
| src/Tasks/AssemblyDependency/Metadata/CorOpenFlags.cs | Adds typed CorOpenFlags for OpenScope options. |
| src/Tasks/AssemblyDependency/Metadata/CorAssemblyFlags.cs | Adds typed CorAssemblyFlags for assembly/ref flags. |
| src/Tasks/AssemblyDependency/Metadata/ASSEMBLYMETADATA.cs | Adds blittable ASSEMBLYMETADATA struct and allocation contract docs. |
| src/Tasks/AssemblyDependency/AssemblyInformation.cs | Uses new metadata COM structs + AgileComPointer<T>; removes AllocAsmMeta/FreeAsmMeta. |
| src/Tasks.UnitTests/MetadataReader_Tests.cs | New parity/regression tests for MetadataReader across TFMs. |
| .github/skills/cswin32-interop/SKILL.md | Documentation updates for interop rules and parity guidance. |
| .github/skills/cswin32-com/SKILL.md | Documentation updates for COM patterns, token wrappers, and error-handling parity. |
There was a problem hiding this comment.
Review Summary: COM Interop Migration to CsWin32 Struct-Based Patterns
This is a well-executed migration from legacy [ComImport] / Marshal.GetComInterfaceForObject() patterns to CsWin32 struct-based COM vtable patterns. The change is mechanically sound and follows the same patterns established in the WMI and Fusion interop code in this repo.
✅ Verified Correct
- Vtable indices: All slot numbers verified against the removed
[ComImport]declarations (IMetaDataAssemblyImportslots 3-15,IMetaDataImport2slots 60/70,ICreateTypeLibslot 12). - ComScope / AgileComPointer lifecycle: The
takeOwnership: falsesemantics are correctly used — GIT AddRefs, ComScope'susingreleases the transient ref, leaving GIT as the sole owner. - DisposeManagedResources vs DisposeUnmanagedResources: Correct switch since
AgileComPointer<T>is a managed class with its own finalizer. - Memory safety in RegisterAssembly: The bridge between RCW (
ITypeLib) and struct-based COM (ICreateTypeLib) is properly structured with nested try/finally. - HRESULT checking: Every COM call checks the return via
.ThrowOnFailure()— an improvement over the old pattern where some calls silently swallowed errors. - Test coverage: The new
MetadataReader_Tests.csprovides good coverage including threading, idempotent disposal, negative paths, and cross-assembly validation. - Token types: The strongly-typed
MdAssembly/MdAssemblyRef/MdFilewrappers with documented bypass semantics for native out-params are well-designed.
⚠️ One Performance Suggestion (Non-Blocking)
GetStringCustomAttribute acquires its own ComScope<IMetaDataImport2> via a GIT round-trip on every call, even when the caller (GetAssemblyMetadata) already holds one. This introduces ~4 unnecessary GIT lookups per GetAssemblyMetadata() invocation. The old code passed IMetaDataImport2 as a parameter. Consider accepting a pointer parameter in a follow-up to eliminate this overhead for the RAR hot path.
Assessment
No blocking issues. The vtable layouts, memory management, disposal patterns, and behavioral contracts are all correct. The one actionable suggestion (GIT round-trip reduction) is a minor performance optimization, not a correctness concern. This PR is ready to merge from a technical review perspective.
Note
🔒 Integrity filter blocked 1 item
The following item were blocked because they don't meet the GitHub integrity level.
- #13853
pull_request_read: has lower integrity than agent requires. The agent cannot read data with integrity below "approved".
To allow these resources, lower min-integrity in your GitHub frontmatter:
tools:
github:
min-integrity: approved # merged | approved | unapproved | noneGenerated by Expert Code Review (on open) for issue #13853 · ● 14M
HasAssemblyAttribute (MetadataReader.cs) - Initialize valuePtr/valueLen and treat hr != S_OK as 'not present'. The old [ComImport] signature gave us out params that the marshaller zeroed on failure; with raw pointers an error HRESULT that does not write pcbData could leave valueLen indeterminate. Now S_FALSE and error HRESULTs both deterministically return false. - Drop the CA1806 suppression / now-unused System.Diagnostics.CodeAnalysis using since the HRESULT is checked. GetStringCustomAttribute (AssemblyInformation.cs) - Accept a borrowed IMetaDataImport2* parameter instead of opening a new ComScope (which round-trips through the GIT) per call. GetAssemblyMetadata reuses its existing import2 scope across 4-5 attribute lookups; GetFrameworkName opens one scope of its own. Both addressed Copilot reviewer / expert-reviewer feedback on PR dotnet#13853.
ViktorHofer
left a comment
There was a problem hiding this comment.
@JeremyKuhne this looks good. I reviewed the changes manually and all looks good. I only had one observation on the testing front. AssemblyInformation.cs would benefit of some additional testing. Here's me asking LLM to find what to actually test:
● Yes — I’d add one or two targeted AssemblyInformation tests, mainly because the PR’s new tests cover ManifestUtil.MetadataReader, but the largest risky migration is actually AssemblyInformation.cs.
Best additions:
1. net472-only real GetAssemblyMetadata() parity test
- Construct new AssemblyInformation(typeof(SomeTestType).Assembly.Location).
- Call internal GetAssemblyMetadata().
- Verify key fields against AssemblyName.GetAssemblyName(path) / assembly attributes:
AssemblyName, version parts, DefaultAlias, PublicHexKey, TargetFrameworkMoniker, PeKind != 0.
- This directly exercises CoCreateInstance, OpenScope, QueryInterface, GetAssemblyProps, ASSEMBLYMETADATA, GetCustomAttributeByName, and GetPEKind.
2. net472-only GetTargetFrameworkAttribute() real-file test
- Compare AssemblyInformation.GetTargetFrameworkAttribute(path) with the assembly’s actual TargetFrameworkAttribute.FrameworkName.
- This covers the borrowed IMetaDataImport2* path outside GetAssemblyMetadata().
Existing coverage already has RealGetAssemblyNameIncludesCulture() for Dependencies, so I wouldn’t add much there unless you want extra confidence around EnumAssemblyRefs/locale handling. I also wouldn’t bother testing Files unless the repo already has an easy multi-module assembly fixture; manufacturing one is probably not worth it for this PR
Commit 5682672 (dotnet#13853) migrated AssemblyInformation / MetadataReader from RCW-based COM to struct-based COM through CsWin32 and AgileComPointer. The new code activates CLSID_CorMetaDataDispenser via raw CoCreateInstance. That loads mscoree.dll (the .NET Framework shim), which in turn calls LoadLibraryShim to bind a runtime before delegating to MetaDataDllGetClassObject. In hosts that did not enter the CLR via mscoree (notably the VC cppxplatdev test harness, which embeds MSBuild in-process via BuildManager) the shim's bound-runtime state is uninitialized and LoadLibraryShim returns CLR_E_SHIM_RUNTIMELOAD (0x80131700) "Failed to load the runtime". RAR catches that as a DependencyResolutionException and silently drops every transitive dependency for the failing reference, manifesting as VC's P2PReferences.08 regression (Referenced_C_IJW.dll missing from Referencing_A_IJW\Debug). The fix activates the dispenser by calling clr.dll's exported DllGetClassObjectInternal directly via a new ComClassFactory.TryCreateFromModule overload, bypassing the shim entirely. This is what the CLR's own managed-COM activation does internally for CLSIDs in IsClrHostedLegacyComObject. The approach is also AOT-friendly (no Type.GetTypeFromCLSID, no Activator, no RCW), so .NET 10+ source-generated COM can use it unchanged. * ComClassFactory: add two public TryCreateFromModule overloads (standard DllGetClassObject and caller-specified export name) plus ClrDllGetClassObjectInternalExportName constant. Single Stdcall function- pointer path serves both TFMs. * AssemblyInformation, MetadataReader: switch dispenser activation to TryCreateFromModule("clr.dll", ..., ClrDllGetClassObjectInternalExportName). Tolerate GetAssemblyFromScope / GetPEKind failures (preserve [PreserveSig] semantics from the previous RCW path). * NativeMethods.txt: add GetModuleHandle (kept for completeness; LoadLibrary, FreeLibrary, GetProcAddress were already present). * AssemblyInformation_Tests: new net472-only tests covering both file-mapping lifetime (delete/overwrite after Dispose) and dispenser activation.
…3899) ## Context Fixes a regression in RAR introduced by #13853 (CsWin32 CLR metadata + TypeLib interop migration), reported by the VC team as `VC.Tests.MsBuild.VC.MsBuild.P2PReferences.08` (CLR+CLR+CLR P2P chain): `Referenced_C_IJW.dll` is missing from `Referencing_A_IJW\Debug\` after build. ## Root cause The migration switched `AssemblyInformation` / `MetadataReader` from RCW-based COM to struct-based COM through CsWin32 and `AgileComPointer`, and along the way activates `CLSID_CorMetaDataDispenser` via raw `CoCreateInstance`. `CLSID_CorMetaDataDispenser` is registered with `mscoree.dll` (the .NET Framework shim) as its `InprocServer32`. `CoCreateInstance` therefore loads the shim, which then has to call `LoadLibraryShim` to bind a runtime before it can delegate to `MetaDataDllGetClassObject`. In hosts that did not enter the CLR via mscoree's startup path — notably the VC `cppxplatdev` test harness, which embeds MSBuild in-process via `BuildManager` from a non-mscoree-rooted process — the shim's bound-runtime state is uninitialized and `LoadLibraryShim` fails with `CLR_E_SHIM_RUNTIMELOAD` (`0x80131700`, "Failed to load the runtime"). RAR catches that as a `DependencyResolutionException` and silently drops every transitive dependency for the failing reference. End-to-end symptom is the missing `Referenced_C_IJW.dll` copy. Standalone `msbuild.exe` cannot reproduce because the msbuild.exe process enters the CLR via the standard mscoree startup path, so the shim is bound. ## Fix Activate the dispenser by calling `clr.dll`'s exported `DllGetClassObjectInternal` directly via a new `ComClassFactory.TryCreateFromModule` overload, bypassing the shim entirely. This is exactly what the CLR's own managed-COM activation does internally for CLSIDs on its `IsClrHostedLegacyComObject` list. The approach is AOT-friendly (no `Type.GetTypeFromCLSID`, no `Activator`, no RCW), so .NET 10+ source-generated COM can use it unchanged. ### `ComClassFactory` - Two public `TryCreateFromModule` overloads (standard `DllGetClassObject` and caller-specified export name). - Public `ClrDllGetClassObjectInternalExportName` constant with documentation of the shim mechanism. - Single `delegate* unmanaged[Stdcall]<...>` function-pointer path serves both net472 and .NET Core. ### Callers - `AssemblyInformation` and `MetadataReader` now use `TryCreateFromModule("clr.dll", CLSID, ClrDllGetClassObjectInternalExportName, ...)`. - Both retain the previous `[PreserveSig]` tolerance for `GetAssemblyFromScope` and `GetPEKind` failures (the legacy RCW code silently ignored those HRs). - Pointer release moved to `DisposeUnmanagedResources` so the finalizer path still revokes GIT cookies if the user forgets to dispose. ### Other - `NativeMethods.txt`: add `GetModuleHandle` (kept for completeness — `LoadLibrary`, `FreeLibrary`, `GetProcAddress` were already present). - New net472-only `AssemblyInformation_Tests` covering both file-mapping lifetime (delete/overwrite after Dispose, including a repeated open-close stress loop) and dispenser activation. ## Validation - Local unit tests: 8/8 new + 12/12 existing `MetadataReader_Tests` pass on net472 x86. - End-to-end: built `Microsoft.Build.Framework.dll` + `Microsoft.Build.Tasks.Core.dll` deployed into a local VS18 install, ran the failing `Project.vcxproj` directly with `msbuild.exe` against the actual VC P2PReferences asset — both `Referenced_B_IJW.dll` and `Referenced_C_IJW.dll` now land in `Referencing_A_IJW\Debug\`. - Build clean, 0 warnings/0 errors.
… hand-rolled interop Continues the CsWin32 struct-based interop migration from dotnet#13746 and dotnet#13853. Removes the legacy Microsoft.VisualStudio.Setup.Configuration.Interop RCW package outright and migrates the directory-enumeration / FileTracker / VS locator / test-fixture call sites to PInvoke + ComScope<T>. New manual COM struct definitions src/Framework/Shared/VisualStudio/ SetupConfiguration.cs CLSID + GetSetupConfiguration fallback DllImport ISetupConfiguration.cs v1 (IUnknown surface only, for QI) ISetupConfiguration2.cs EnumAllInstances (vtable slot 6) IEnumSetupInstances.cs Next (slot 3) ISetupInstance.cs GetInstallationPath/Version/DisplayName (slots 6/7/8) ISetupInstance2.cs GetState (slot 11) InstanceState.cs [Flags] enum InstanceState : uint, Complete = MAXUINT Rewrites src/Framework/VisualStudioLocationHelper.cs Replaces the RCW-based GetInstances() with PInvoke.CoCreateInstance + REGDB_E_CLASSNOTREG fallback to GetSetupConfiguration P/Invoke + QI. Pointer lifetimes managed entirely via ComScope<T>; BSTR out-params via `using BSTR x = default;`. Helper returns `default` ComScope on COM failure rather than throwing a COMException the caller would discard. src/Framework/FileSystem/WindowsNative.cs src/Framework/FileSystem/SafeFileHandle.cs FindFirstFileW / FindNextFileW / FindClose / PathMatchSpecExW migrated to PInvoke.FindFirstFile / FindNextFile / FindClose / PathMatchSpecEx (added to NativeMethods.txt). The legacy hand-shaped Win32FindData (with [MarshalAs(UnmanagedType.ByValTStr)] string fields) is preserved as a caller-facing adapter built from the blittable WIN32_FIND_DATAW. SafeFindFileHandle now wraps a manually-constructed HANDLE and its ReleaseHandle calls PInvoke.FindClose. src/Shared/InprocTrackingNativeMethods.cs FileTracker.dll loader rewritten to use PInvoke.LoadLibrary + PInvoke.GetProcAddress with typed delegate* unmanaged[Stdcall]<...> function pointers matching the actual export signatures from FileTracker.h / FileTracker.def (StartTrackingContext @2 ... SetThreadCount @10). The SafeLibraryHandle subclass and all [UnmanagedFunctionPointer] managed delegate types are deleted; the public API surface gets full XML doc comments. The HMODULE is intentionally never freed (FileTracker detours ExitProcess; unloading mid-shutdown would corrupt the CLR). ANSI export-name encoding uses Encoding.Default + a pooled byte[] (correct for DBCS code pages). Test fixtures migrated to CsWin32 src/UnitTests.Shared/DriveMapping.cs DefineDosDevice / QueryDosDevice -> PInvoke.* with DEFINE_DOS_DEVICE_FLAGS. src/Tasks.UnitTests/AddToWin32Manifest_Tests.cs LoadLibrary / FindResource / LoadResource / LockResource / SizeofResource -> PInvoke.LoadLibraryEx / FindResource / LoadResource / LockResource / SizeofResource consuming HMODULE / HRSRC / HGLOBAL. src/Build.UnitTests/BackEnd/TargetUpToDateChecker_Tests.cs Hand-rolled CreateSymbolicLink / CreateFileForSymlink / SetFileTime replaced with wrappers over PInvoke.* (CreateSymbolicLink already in NativeMethods.txt; SetFileTime added). Deletion src/Build/BackEnd/Node/NativeMethods.cs (~230 lines) The CreateProcess + STARTUP_INFO + PROCESS_INFORMATION + SECURITY_ATTRIBUTES wrapper was consumed by exactly one place -- FileTrackerTests' [Fact(Skip=...)] helper methods. That helper now defines a small private BackEndNativeMethods static class inline, forwarding to PInvoke.CreateProcess and bridging the test-facing struct shapes to Win32 STARTUPINFOW / PROCESS_INFORMATION / SECURITY_ATTRIBUTES at the call. Package reference removed (now unused everywhere) - src/Framework/Microsoft.Build.Framework.csproj - src/Build/Microsoft.Build.csproj - src/Utilities/Microsoft.Build.Utilities.csproj - src/Tasks/Microsoft.Build.Tasks.csproj - src/Framework.UnitTests/Microsoft.Build.Framework.UnitTests.csproj PR dotnet#13853 review feedback (carried in) src/Tasks/ManifestUtil/MetadataReader.cs src/Tasks/ManifestUtil/ApplicationManifest.cs Adds MetadataReader.HasAssemblyAttributes(string[], bool[]) batch overload on both .NETCore and net472 paths; the net472 path acquires IMetaDataAssemblyImport / IMetaDataImport2 from the GIT once and reuses the same GetAssemblyFromScope across all probes. AssemblyAttributeFlags in ApplicationManifest.cs now does one GIT round-trip for all four attribute checks instead of eight (Jan's review comment). src/Tasks.UnitTests/AssemblyInformation_Tests.cs (new) Two real-file tests exercising the full CLR-metadata COM call chain end-to-end on net472 (CoCreateInstance -> OpenScope -> QI -> GetAssemblyProps -> GetCustomAttributeByName x4 -> GetPEKind), with parity assertions against AssemblyName.GetAssemblyName / TargetFrameworkAttribute (Viktor's review comment). Other new tests src/Tasks.UnitTests/MetadataReader_Tests.cs HasAssemblyAttributes_Batch_AgreesWithSingular: cross-validates the new batch overload against the singular overload for both known-true and known-false attributes. src/Framework.UnitTests/FileSystem/WindowsNative_Tests.cs (new) Six tests directly covering the FindFirstFile / FindNextFile / PathMatchSpecEx migration + the Win32FindData adapter + SafeFindFileHandle idempotent-dispose, with WindowsOnlyFact gating. src/Framework.UnitTests/VisualStudioLocationHelper_Tests.cs (new) Six baseline tests asserting the contract that GetInstances() never throws, never returns null, that each returned VisualStudioInstance has valid Name/Path/Version (Major >= 15) shape, paths are unique, and existing paths are absolute directories. Establishes the regression net before the RCW -> struct-based COM migration. NativeMethods.txt additions DefineDosDevice, QueryDosDevice, FindClose, FindFirstFile, FindNextFile, FindResource, LoadResource, LockResource, SizeofResource, LoadLibraryEx, LOAD_LIBRARY_FLAGS, PathMatchSpecEx, SetFileTime, PCSTR. Skill updates .github/skills/cswin32-com/SKILL.md Compressed from ~5,400 -> ~3,520 estimated tokens (-35%). Adds: - "Lifetime: using ComScope<T>" promoted to workflow step 2; raw try/finally Release pattern explicitly banned. - Helper "ownership-transfer" pattern (return ComScope<T> from a helper) with the AcquireSetupConfiguration2 worked example. - BSTR scoping bullet -- `using BSTR x = default;` replaces manual try/finally + SysFreeString. - "Don't throw when the caller swallows it" subsection under Error-Handling Parity, citing AcquireSetupConfiguration2. Verified - Full build: dotnet build MSBuild.Dev.slnf -> 0 warnings, 0 errors. - Tests: Framework.UnitTests 750/750 net472, 721/721 net10.0 Tasks.UnitTests new MetadataReader/AssemblyInformation + AddToWin32Manifest tests pass on both TFMs. - Source build (DotNetBuildSourceOnly=true) unchanged: every new interop file is gated #if FEATURE_WINDOWSINTEROP (and && FEATURE_VISUALSTUDIOSETUP for the VS Setup interfaces).
… hand-rolled interop Continues the CsWin32 struct-based interop migration from dotnet#13746 and dotnet#13853. Removes the legacy Microsoft.VisualStudio.Setup.Configuration.Interop RCW package outright and migrates the directory-enumeration / FileTracker / VS locator / test-fixture call sites to PInvoke + ComScope<T>. New manual COM struct definitions src/Framework/Shared/VisualStudio/ SetupConfiguration.cs CLSID + GetSetupConfiguration fallback DllImport ISetupConfiguration.cs v1 (IUnknown surface only, for QI) ISetupConfiguration2.cs EnumAllInstances (vtable slot 6) IEnumSetupInstances.cs Next (slot 3) ISetupInstance.cs GetInstallationPath/Version/DisplayName (slots 6/7/8) ISetupInstance2.cs GetState (slot 11) InstanceState.cs [Flags] enum InstanceState : uint, Complete = MAXUINT Rewrites src/Framework/VisualStudioLocationHelper.cs Replaces the RCW-based GetInstances() with PInvoke.CoCreateInstance + REGDB_E_CLASSNOTREG fallback to GetSetupConfiguration P/Invoke + QI. Pointer lifetimes managed entirely via ComScope<T>; BSTR out-params via `using BSTR x = default;`. Helper returns `default` ComScope on COM failure rather than throwing a COMException the caller would discard. src/Framework/FileSystem/WindowsNative.cs src/Framework/FileSystem/SafeFileHandle.cs FindFirstFileW / FindNextFileW / FindClose / PathMatchSpecExW migrated to PInvoke.FindFirstFile / FindNextFile / FindClose / PathMatchSpecEx (added to NativeMethods.txt). The legacy hand-shaped Win32FindData (with [MarshalAs(UnmanagedType.ByValTStr)] string fields) is preserved as a caller-facing adapter built from the blittable WIN32_FIND_DATAW. SafeFindFileHandle now wraps a manually-constructed HANDLE and its ReleaseHandle calls PInvoke.FindClose. src/Shared/InprocTrackingNativeMethods.cs FileTracker.dll loader rewritten to use PInvoke.LoadLibrary + PInvoke.GetProcAddress with typed delegate* unmanaged[Stdcall]<...> function pointers matching the actual export signatures from FileTracker.h / FileTracker.def (StartTrackingContext @2 ... SetThreadCount @10). The SafeLibraryHandle subclass and all [UnmanagedFunctionPointer] managed delegate types are deleted; the public API surface gets full XML doc comments. The HMODULE is intentionally never freed (FileTracker detours ExitProcess; unloading mid-shutdown would corrupt the CLR). ANSI export-name encoding uses Encoding.Default + a pooled byte[] (correct for DBCS code pages). Test fixtures migrated to CsWin32 src/UnitTests.Shared/DriveMapping.cs DefineDosDevice / QueryDosDevice -> PInvoke.* with DEFINE_DOS_DEVICE_FLAGS. src/Tasks.UnitTests/AddToWin32Manifest_Tests.cs LoadLibrary / FindResource / LoadResource / LockResource / SizeofResource -> PInvoke.LoadLibraryEx / FindResource / LoadResource / LockResource / SizeofResource consuming HMODULE / HRSRC / HGLOBAL. src/Build.UnitTests/BackEnd/TargetUpToDateChecker_Tests.cs Hand-rolled CreateSymbolicLink / CreateFileForSymlink / SetFileTime replaced with wrappers over PInvoke.* (CreateSymbolicLink already in NativeMethods.txt; SetFileTime added). Deletion src/Build/BackEnd/Node/NativeMethods.cs (~230 lines) The CreateProcess + STARTUP_INFO + PROCESS_INFORMATION + SECURITY_ATTRIBUTES wrapper was consumed by exactly one place -- FileTrackerTests' [Fact(Skip=...)] helper methods. That helper now defines a small private BackEndNativeMethods static class inline, forwarding to PInvoke.CreateProcess and bridging the test-facing struct shapes to Win32 STARTUPINFOW / PROCESS_INFORMATION / SECURITY_ATTRIBUTES at the call. Package reference removed (now unused everywhere) - src/Framework/Microsoft.Build.Framework.csproj - src/Build/Microsoft.Build.csproj - src/Utilities/Microsoft.Build.Utilities.csproj - src/Tasks/Microsoft.Build.Tasks.csproj - src/Framework.UnitTests/Microsoft.Build.Framework.UnitTests.csproj PR dotnet#13853 review feedback (carried in) src/Tasks/ManifestUtil/MetadataReader.cs src/Tasks/ManifestUtil/ApplicationManifest.cs Adds MetadataReader.HasAssemblyAttributes(string[], bool[]) batch overload on both .NETCore and net472 paths; the net472 path acquires IMetaDataAssemblyImport / IMetaDataImport2 from the GIT once and reuses the same GetAssemblyFromScope across all probes. AssemblyAttributeFlags in ApplicationManifest.cs now does one GIT round-trip for all four attribute checks instead of eight (Jan's review comment). src/Tasks.UnitTests/AssemblyInformation_Tests.cs (new) Two real-file tests exercising the full CLR-metadata COM call chain end-to-end on net472 (CoCreateInstance -> OpenScope -> QI -> GetAssemblyProps -> GetCustomAttributeByName x4 -> GetPEKind), with parity assertions against AssemblyName.GetAssemblyName / TargetFrameworkAttribute (Viktor's review comment). Other new tests src/Tasks.UnitTests/MetadataReader_Tests.cs HasAssemblyAttributes_Batch_AgreesWithSingular: cross-validates the new batch overload against the singular overload for both known-true and known-false attributes. src/Framework.UnitTests/FileSystem/WindowsNative_Tests.cs (new) Six tests directly covering the FindFirstFile / FindNextFile / PathMatchSpecEx migration + the Win32FindData adapter + SafeFindFileHandle idempotent-dispose, with WindowsOnlyFact gating. src/Framework.UnitTests/VisualStudioLocationHelper_Tests.cs (new) Six baseline tests asserting the contract that GetInstances() never throws, never returns null, that each returned VisualStudioInstance has valid Name/Path/Version (Major >= 15) shape, paths are unique, and existing paths are absolute directories. Establishes the regression net before the RCW -> struct-based COM migration. NativeMethods.txt additions DefineDosDevice, QueryDosDevice, FindClose, FindFirstFile, FindNextFile, FindResource, LoadResource, LockResource, SizeofResource, LoadLibraryEx, LOAD_LIBRARY_FLAGS, PathMatchSpecEx, SetFileTime, PCSTR. Skill updates .github/skills/cswin32-com/SKILL.md Compressed from ~5,400 -> ~3,520 estimated tokens (-35%). Adds: - "Lifetime: using ComScope<T>" promoted to workflow step 2; raw try/finally Release pattern explicitly banned. - Helper "ownership-transfer" pattern (return ComScope<T> from a helper) with the AcquireSetupConfiguration2 worked example. - BSTR scoping bullet -- `using BSTR x = default;` replaces manual try/finally + SysFreeString. - "Don't throw when the caller swallows it" subsection under Error-Handling Parity, citing AcquireSetupConfiguration2. Verified - Full build: dotnet build MSBuild.Dev.slnf -> 0 warnings, 0 errors. - Tests: Framework.UnitTests 750/750 net472, 721/721 net10.0 Tasks.UnitTests new MetadataReader/AssemblyInformation + AddToWin32Manifest tests pass on both TFMs. - Source build (DotNetBuildSourceOnly=true) unchanged: every new interop file is gated #if FEATURE_WINDOWSINTEROP (and && FEATURE_VISUALSTUDIOSETUP for the VS Setup interfaces).
… hand-rolled interop (#13872) Continues the CsWin32 struct-based interop migration from #13746 and #13853. Removes the legacy `Microsoft.VisualStudio.Setup.Configuration.Interop` RCW package outright and migrates the directory-enumeration / FileTracker / VS locator / test-fixture call sites to `PInvoke` + `ComScope<T>`. ## New manual COM struct definitions Under `src/Framework/Shared/VisualStudio/`: | File | Contents | | --- | --- | | `SetupConfiguration.cs` | `CLSID_SetupConfiguration` + `GetSetupConfiguration` fallback `[DllImport]` | | `ISetupConfiguration.cs` | v1 — `IUnknown` surface only, for QI | | `ISetupConfiguration2.cs` | `EnumAllInstances` (vtable slot 6) | | `IEnumSetupInstances.cs` | `Next` (slot 3) | | `ISetupInstance.cs` | `GetInstallationPath` / `GetInstallationVersion` / `GetDisplayName` (slots 6/7/8) | | `ISetupInstance2.cs` | `GetState` (slot 11) | | `InstanceState.cs` | `[Flags] enum InstanceState : uint`, `Complete = MAXUINT` | ## Rewrites ### `src/Framework/VisualStudioLocationHelper.cs` Replaces the RCW-based `GetInstances()` with `PInvoke.CoCreateInstance` + `REGDB_E_CLASSNOTREG` fallback to the `GetSetupConfiguration` P/Invoke + QI. Pointer lifetimes managed entirely via `ComScope<T>`; `BSTR` out-params via `using BSTR x = default;`. The helper returns a `default` `ComScope` on COM failure rather than throwing a `COMException` the caller would immediately discard. ### `src/Framework/FileSystem/WindowsNative.cs`, `src/Framework/FileSystem/SafeFileHandle.cs` `FindFirstFileW` / `FindNextFileW` / `FindClose` / `PathMatchSpecExW` migrated to `PInvoke.FindFirstFile` / `FindNextFile` / `FindClose` / `PathMatchSpecEx` (added to `NativeMethods.txt`). The legacy hand-shaped `Win32FindData` (with `[MarshalAs(UnmanagedType.ByValTStr)]` string fields) is preserved as a caller-facing adapter built from the blittable `WIN32_FIND_DATAW`. `SafeFindFileHandle` now wraps a manually-constructed `HANDLE` and its `ReleaseHandle` calls `PInvoke.FindClose`. ### `src/Shared/InprocTrackingNativeMethods.cs` FileTracker.dll loader rewritten to use `PInvoke.LoadLibrary` + `PInvoke.GetProcAddress` with typed `delegate* unmanaged[Stdcall]<…>` function pointers matching the actual export signatures from `FileTracker.h` / `FileTracker.def` (`StartTrackingContext` `@2` through `SetThreadCount` `@10`). The `SafeLibraryHandle` subclass and all `[UnmanagedFunctionPointer]` managed delegate types are deleted; the public API surface gets full XML doc comments. The `HMODULE` is intentionally never freed — FileTracker detours `ExitProcess`, so unloading mid-shutdown would corrupt the CLR. ANSI export-name encoding uses `Encoding.Default` + a pooled `byte[]` (correct for DBCS code pages). ## Test fixtures migrated to CsWin32 - `src/UnitTests.Shared/DriveMapping.cs` — `DefineDosDevice` / `QueryDosDevice` → `PInvoke.*` with `DEFINE_DOS_DEVICE_FLAGS`. - `src/Tasks.UnitTests/AddToWin32Manifest_Tests.cs` — `LoadLibrary` / `FindResource` / `LoadResource` / `LockResource` / `SizeofResource` → `PInvoke.LoadLibraryEx` / `FindResource` / `LoadResource` / `LockResource` / `SizeofResource` consuming `HMODULE` / `HRSRC` / `HGLOBAL`. - `src/Build.UnitTests/BackEnd/TargetUpToDateChecker_Tests.cs` — hand-rolled `CreateSymbolicLink` / `CreateFileForSymlink` / `SetFileTime` replaced with wrappers over `PInvoke.*` (`CreateSymbolicLink` already in `NativeMethods.txt`; `SetFileTime` added). ## Deletion - `src/Build/BackEnd/Node/NativeMethods.cs` (~230 lines) — the `CreateProcess` + `STARTUP_INFO` + `PROCESS_INFORMATION` + `SECURITY_ATTRIBUTES` wrapper was consumed by exactly one place: `FileTrackerTests`' `[Fact(Skip=...)]` helper methods. That helper now defines a small private `BackEndNativeMethods` static class inline, forwarding to `PInvoke.CreateProcess` and bridging the test-facing struct shapes to Win32 `STARTUPINFOW` / `PROCESS_INFORMATION` / `SECURITY_ATTRIBUTES` at the call. ## Package reference removed (now unused everywhere) - `src/Framework/Microsoft.Build.Framework.csproj` - `src/Build/Microsoft.Build.csproj` - `src/Utilities/Microsoft.Build.Utilities.csproj` - `src/Tasks/Microsoft.Build.Tasks.csproj` - `src/Framework.UnitTests/Microsoft.Build.Framework.UnitTests.csproj` ## PR #13853 review feedback (carried in) ### `src/Tasks/ManifestUtil/MetadataReader.cs`, `src/Tasks/ManifestUtil/ApplicationManifest.cs` Adds `MetadataReader.HasAssemblyAttributes(string[], bool[])` batch overload on both .NETCore and net472 paths; the net472 path acquires `IMetaDataAssemblyImport` / `IMetaDataImport2` from the GIT once and reuses the same `GetAssemblyFromScope` across all probes. `AssemblyAttributeFlags` in `ApplicationManifest.cs` now does one GIT round-trip for all four attribute checks instead of eight (@JanProvaznik's review comment). ### `src/Tasks.UnitTests/AssemblyInformation_Tests.cs` (new) Two real-file tests exercising the full CLR-metadata COM call chain end-to-end on net472 (`CoCreateInstance` → `OpenScope` → QI → `GetAssemblyProps` → `GetCustomAttributeByName` ×4 → `GetPEKind`), with parity assertions against `AssemblyName.GetAssemblyName` / `TargetFrameworkAttribute` (@ViktorHofer's review comment). ## Other new tests - `src/Tasks.UnitTests/MetadataReader_Tests.cs` — `HasAssemblyAttributes_Batch_AgreesWithSingular`: cross-validates the new batch overload against the singular overload for both known-true and known-false attributes. - `src/Framework.UnitTests/FileSystem/WindowsNative_Tests.cs` (new) — six tests directly covering the `FindFirstFile` / `FindNextFile` / `PathMatchSpecEx` migration + the `Win32FindData` adapter + `SafeFindFileHandle` idempotent-dispose, with `WindowsOnlyFact` gating. - `src/Framework.UnitTests/VisualStudioLocationHelper_Tests.cs` (new) — six baseline tests asserting the contract that `GetInstances()` never throws, never returns null, that each returned `VisualStudioInstance` has valid `Name`/`Path`/`Version` (Major ≥ 15) shape, paths are unique, and existing paths are absolute directories. Establishes the regression net before the RCW → struct-based COM migration. ## `NativeMethods.txt` additions `DefineDosDevice`, `QueryDosDevice`, `FindClose`, `FindFirstFile`, `FindNextFile`, `FindResource`, `LoadResource`, `LockResource`, `SizeofResource`, `LoadLibraryEx`, `LOAD_LIBRARY_FLAGS`, `PathMatchSpecEx`, `SetFileTime`, `PCSTR`. ## Skill updates `.github/skills/cswin32-com/SKILL.md` compressed from ~5,400 → ~3,520 estimated tokens (−35%). Adds: - "Lifetime: `using ComScope<T>`" promoted to workflow step 2; raw `try`/`finally` `Release` pattern explicitly banned. - Helper "ownership-transfer" pattern (return `ComScope<T>` from a helper) with the `AcquireSetupConfiguration2` worked example. - `BSTR` scoping bullet — `using BSTR x = default;` replaces manual `try`/`finally` + `SysFreeString`. - "Don't throw when the caller swallows it" subsection under Error-Handling Parity, citing `AcquireSetupConfiguration2`. ## Verified - **Full build**: `dotnet build MSBuild.Dev.slnf` → 0 warnings, 0 errors. - **Tests**: - `Framework.UnitTests` — 750/750 net472, 721/721 net10.0. - `Tasks.UnitTests` — new `MetadataReader` / `AssemblyInformation` + `AddToWin32Manifest` tests pass on both TFMs. - **Source build** (`DotNetBuildSourceOnly=true`) unchanged: every new interop file is gated `#if FEATURE_WINDOWSINTEROP` (and `&& FEATURE_VISUALSTUDIOSETUP` for the VS Setup interfaces).
CsWin32 follow-up: CLR metadata + TypeLib interop migration
Continues the CsWin32 struct-based COM migration from #13705 and #13746,
this time covering the CLR metadata APIs (IMetaDataDispenser /
IMetaDataImport2 / IMetaDataAssemblyImport) consumed by AssemblyInformation
and ManifestUtil.MetadataReader, plus the ICreateTypeLib path used by
RegisterAssembly.
New files
with canonical inherited 3-72 slot map
GetFileProps / EnumAssemblyRefs / EnumFiles /
GetAssemblyFromScope / CloseEnum
readonly structs with validating constructors
RegisterAssembly
(PEReader on net10, COM on
net472), verifies identity
parity for Accessibility.dll
Modifications
CorAssemblyFlags; OpenScope asks for
IMetaDataImport2 directly (skips one QI);
AllocAsmMeta / FreeAsmMeta eliminated
struct-based COM via AgileComPointer; failure
to OpenScope returns null (per-file contract)
Fusion / Metadata / TypeLib folders
(FeatureWindowsInterop-gated)
only oleaut32 typelib P/Invokes remain;
System.Runtime.Versioning using gated on
FEATURE_WINDOWSINTEROP for source-build
Skills
token wrappers' with CLR
validation table; pair-pointer
to cswin32-interop; error-
handling-parity guidance;
IComIID polyfill heading rename
signatures / Infrastructure /
BufferScope / Gotchas;
source-build verification note
Verified
(DotNetBuildSourceOnly=true DotNetBuild=true)