From 7806cd7c4ff08e294e7705ee91dcd8320bd168c8 Mon Sep 17 00:00:00 2001 From: Tomas Matousek Date: Fri, 11 Sep 2026 11:26:42 -0700 Subject: [PATCH 1/6] Load Microsoft.DiaSymReader.Native from the runtime The .NET runtime now ships Microsoft.DiaSymReader.Native.*.dll in the shared framework, so the Microsoft.DiaSymReader.Native package reference is no longer needed. Load the native binaries from the runtime instead. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 2 +- eng/DiaSymReaderNative.targets | 27 +++++++++++++++++++ src/Directory.Packages.props | 1 - ....DiaSymReader.PortablePdb.UnitTests.csproj | 19 ++----------- 4 files changed, 30 insertions(+), 19 deletions(-) create mode 100644 eng/DiaSymReaderNative.targets diff --git a/README.md b/README.md index eac0365f8e..0b9689faad 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Reader of [Portable PDBs](https://github.com/dotnet/core/blob/main/Documentation/diagnostics/portable_pdb.md) format that implements DiaSymReader interfaces ([ISymUnmanagedReader](https://msdn.microsoft.com/en-us/library/ms232131.aspx), [ISymUnmanagedBinder](https://msdn.microsoft.com/en-us/library/ms232451.aspx), etc.). The definitions of the DiaSymReader COM interfaces are provided by [Microsoft.DiaSymReader](https://www.nuget.org/packages/Microsoft.DiaSymReader) package. -The implementation of these interfaces for Windows PDBs is provided by [Microsoft.DiaSymReader.Native](https://www.nuget.org/packages/Microsoft.DiaSymReader.Native) package. +The implementation of these interfaces for Windows PDBs is provided by `Microsoft.DiaSymReader.Native.{platform}.dll`, which ships in the .NET runtime. It is recommended that new applications and libraries read Portable PDBs directly using APIs provided by [System.Reflection.Metadata](https://www.nuget.org/packages/System.Reflection.Metadata) package. These APIs are much more efficient than DiaSymReader APIs. Microsoft.DiaSymReader.PortablePdb bridge is recommended for existings apps that already use DiaSymReader APIs and need to be able to read Portable PDBs without significant changes to their source. diff --git a/eng/DiaSymReaderNative.targets b/eng/DiaSymReaderNative.targets new file mode 100644 index 0000000000..db8b1df304 --- /dev/null +++ b/eng/DiaSymReaderNative.targets @@ -0,0 +1,27 @@ + + + + + + <_DotNetRoot Condition="'$(_DotNetRoot)' == ''">$(DOTNET_ROOT) + <_DotNetRoot Condition="'$(_DotNetRoot)' == ''">$(DOTNET_INSTALL_DIR) + <_DotNetRoot Condition="'$(_DotNetRoot)' == ''">$(NetCoreRoot) + <_DiaSymReaderNativeDir Condition="'$(_DotNetRoot)' != '' and '$(BundledNETCoreAppPackageVersion)' != ''">$([MSBuild]::NormalizeDirectory('$(_DotNetRoot)', 'shared', 'Microsoft.NETCore.App', '$(BundledNETCoreAppPackageVersion)')) + <_DiaSymReaderNativeDir Condition="'$(_DiaSymReaderNativeDir)' == ''">$([System.Runtime.InteropServices.RuntimeEnvironment]::GetRuntimeDirectory()) + + + + + PreserveNewest + false + false + $(DiaSymReaderNativeLinkDir)\%(Filename)%(Extension) + %(Filename)%(Extension) + + + diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 09568fd521..d5a27a6f62 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -10,7 +10,6 @@ - \ No newline at end of file diff --git a/src/Microsoft.DiaSymReader.PortablePdb.Tests/Microsoft.DiaSymReader.PortablePdb.UnitTests.csproj b/src/Microsoft.DiaSymReader.PortablePdb.Tests/Microsoft.DiaSymReader.PortablePdb.UnitTests.csproj index efd5a69247..e48e57618e 100644 --- a/src/Microsoft.DiaSymReader.PortablePdb.Tests/Microsoft.DiaSymReader.PortablePdb.UnitTests.csproj +++ b/src/Microsoft.DiaSymReader.PortablePdb.Tests/Microsoft.DiaSymReader.PortablePdb.UnitTests.csproj @@ -7,17 +7,13 @@ We would need to download 32bit dotnet cli, which would add extra time to PR runs Testing 64bit only on Desktop suffixiently covers our interop code paths. --> - x64;x86 + x64 - - - - @@ -175,17 +171,6 @@ - - - PreserveNewest - false - Microsoft.DiaSymReader.Native.x86.dll - - - PreserveNewest - false - Microsoft.DiaSymReader.Native.amd64.dll - - + From 76233bbd0ad9debe704a9379ef05430264f05050 Mon Sep 17 00:00:00 2001 From: Tomas Matousek Date: Fri, 11 Sep 2026 11:59:29 -0700 Subject: [PATCH 2/6] Stop copying Microsoft.DiaSymReader.Native DLLs Load the native binaries from the .NET runtime instead of copying them into test output. .NET tests rely on the runtime load path. .NET Framework tests call AddDllDirectory with the installed .NET shared-framework directory. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/DiaSymReaderNative.targets | 27 ---- ....DiaSymReader.PortablePdb.UnitTests.csproj | 2 - .../TestHelpers/DiaSymReaderNativeRuntime.cs | 142 ++++++++++++++++++ 3 files changed, 142 insertions(+), 29 deletions(-) delete mode 100644 eng/DiaSymReaderNative.targets create mode 100644 src/Microsoft.DiaSymReader.PortablePdb.Tests/TestHelpers/DiaSymReaderNativeRuntime.cs diff --git a/eng/DiaSymReaderNative.targets b/eng/DiaSymReaderNative.targets deleted file mode 100644 index db8b1df304..0000000000 --- a/eng/DiaSymReaderNative.targets +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - <_DotNetRoot Condition="'$(_DotNetRoot)' == ''">$(DOTNET_ROOT) - <_DotNetRoot Condition="'$(_DotNetRoot)' == ''">$(DOTNET_INSTALL_DIR) - <_DotNetRoot Condition="'$(_DotNetRoot)' == ''">$(NetCoreRoot) - <_DiaSymReaderNativeDir Condition="'$(_DotNetRoot)' != '' and '$(BundledNETCoreAppPackageVersion)' != ''">$([MSBuild]::NormalizeDirectory('$(_DotNetRoot)', 'shared', 'Microsoft.NETCore.App', '$(BundledNETCoreAppPackageVersion)')) - <_DiaSymReaderNativeDir Condition="'$(_DiaSymReaderNativeDir)' == ''">$([System.Runtime.InteropServices.RuntimeEnvironment]::GetRuntimeDirectory()) - - - - - PreserveNewest - false - false - $(DiaSymReaderNativeLinkDir)\%(Filename)%(Extension) - %(Filename)%(Extension) - - - diff --git a/src/Microsoft.DiaSymReader.PortablePdb.Tests/Microsoft.DiaSymReader.PortablePdb.UnitTests.csproj b/src/Microsoft.DiaSymReader.PortablePdb.Tests/Microsoft.DiaSymReader.PortablePdb.UnitTests.csproj index e48e57618e..649c9c8573 100644 --- a/src/Microsoft.DiaSymReader.PortablePdb.Tests/Microsoft.DiaSymReader.PortablePdb.UnitTests.csproj +++ b/src/Microsoft.DiaSymReader.PortablePdb.Tests/Microsoft.DiaSymReader.PortablePdb.UnitTests.csproj @@ -171,6 +171,4 @@ - - diff --git a/src/Microsoft.DiaSymReader.PortablePdb.Tests/TestHelpers/DiaSymReaderNativeRuntime.cs b/src/Microsoft.DiaSymReader.PortablePdb.Tests/TestHelpers/DiaSymReaderNativeRuntime.cs new file mode 100644 index 0000000000..2666a48643 --- /dev/null +++ b/src/Microsoft.DiaSymReader.PortablePdb.Tests/TestHelpers/DiaSymReaderNativeRuntime.cs @@ -0,0 +1,142 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the License.txt file in the project root for more information. + +using System; +using System.IO; +using System.Runtime.InteropServices; +#if NETFRAMEWORK +using System.Linq; +using System.Runtime.CompilerServices; +#endif + +namespace Roslyn.Test.Utilities +{ + public static class DiaSymReaderNativeRuntime + { +#if NETFRAMEWORK + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern IntPtr AddDllDirectory(string newDirectory); + + [ModuleInitializer] + internal static void AddInstalledDotNetRuntimeDirectory() + { + if (Path.DirectorySeparatorChar != '\\') + { + return; + } + + string directory = GetNativeLibraryDirectory(); + if (string.IsNullOrEmpty(directory)) + { + throw new InvalidOperationException("Could not locate an installed .NET runtime containing Microsoft.DiaSymReader.Native."); + } + + if (AddDllDirectory(directory) == IntPtr.Zero) + { + throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error()); + } + } +#endif + + public static string GetNativeLibraryDirectory() + { +#if NETFRAMEWORK + return GetInstalledDotNetRuntimeDirectory(); +#else + return RuntimeEnvironment.GetRuntimeDirectory(); +#endif + } + +#if NETFRAMEWORK + private static string GetInstalledDotNetRuntimeDirectory() + { + string root = Environment.GetEnvironmentVariable("DOTNET_ROOT") + ?? Environment.GetEnvironmentVariable("DOTNET_INSTALL_DIR"); + + if (string.IsNullOrEmpty(root)) + { + string hostPath = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH"); + if (!string.IsNullOrEmpty(hostPath)) + { + root = Path.GetDirectoryName(hostPath); + } + } + + if (string.IsNullOrEmpty(root)) + { + string dotnetHost = FindDotNetHost(); + if (dotnetHost != null) + { + root = Path.GetDirectoryName(dotnetHost); + } + } + + if (string.IsNullOrEmpty(root)) + { + root = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "dotnet"); + } + + string shared = Path.Combine(root, "shared", "Microsoft.NETCore.App"); + if (!Directory.Exists(shared)) + { + return null; + } + + string dllName = IntPtr.Size == 4 + ? "Microsoft.DiaSymReader.Native.x86.dll" + : RuntimeInformation.ProcessArchitecture == Architecture.Arm64 + ? "Microsoft.DiaSymReader.Native.arm64.dll" + : "Microsoft.DiaSymReader.Native.amd64.dll"; + + return Directory.GetDirectories(shared) + .Select(dir => (dir, version: TryParseVersion(Path.GetFileName(dir)))) + .Where(item => item.version != null && File.Exists(Path.Combine(item.dir, dllName))) + .OrderByDescending(item => item.version) + .Select(item => item.dir) + .FirstOrDefault(); + } + + private static Version TryParseVersion(string name) + { + string core = name.Split('-')[0]; + return Version.TryParse(core, out var version) ? version : null; + } + + private static string FindDotNetHost() + { + string pathEnv = Environment.GetEnvironmentVariable("PATH"); + if (pathEnv == null) + { + return null; + } + + foreach (string dir in pathEnv.Split(Path.PathSeparator)) + { + if (string.IsNullOrWhiteSpace(dir)) + { + continue; + } + + string candidate = Path.Combine(dir.Trim(), "dotnet.exe"); + if (File.Exists(candidate)) + { + return candidate; + } + } + + return null; + } +#endif + } +} + +#if NETFRAMEWORK +namespace System.Runtime.CompilerServices +{ + [AttributeUsage(AttributeTargets.Method, Inherited = false)] + internal sealed class ModuleInitializerAttribute : Attribute + { + } +} +#endif From ebb928efcc263e2791568a6862b4d25e6eef5ccb Mon Sep 17 00:00:00 2001 From: Tomas Matousek Date: Fri, 11 Sep 2026 13:46:56 -0700 Subject: [PATCH 3/6] Simplify Framework-only DiaSymReaderNativeRuntime helper Wrap the test helper in a single NETFRAMEWORK block instead of scattering ifdefs through APIs that only run on .NET Framework. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../TestHelpers/DiaSymReaderNativeRuntime.cs | 30 ++++--------------- 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/src/Microsoft.DiaSymReader.PortablePdb.Tests/TestHelpers/DiaSymReaderNativeRuntime.cs b/src/Microsoft.DiaSymReader.PortablePdb.Tests/TestHelpers/DiaSymReaderNativeRuntime.cs index 2666a48643..0f219e036b 100644 --- a/src/Microsoft.DiaSymReader.PortablePdb.Tests/TestHelpers/DiaSymReaderNativeRuntime.cs +++ b/src/Microsoft.DiaSymReader.PortablePdb.Tests/TestHelpers/DiaSymReaderNativeRuntime.cs @@ -2,31 +2,25 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the License.txt file in the project root for more information. +#if NETFRAMEWORK + using System; using System.IO; -using System.Runtime.InteropServices; -#if NETFRAMEWORK using System.Linq; using System.Runtime.CompilerServices; -#endif +using System.Runtime.InteropServices; namespace Roslyn.Test.Utilities { - public static class DiaSymReaderNativeRuntime + internal static class DiaSymReaderNativeRuntime { -#if NETFRAMEWORK [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] private static extern IntPtr AddDllDirectory(string newDirectory); [ModuleInitializer] internal static void AddInstalledDotNetRuntimeDirectory() { - if (Path.DirectorySeparatorChar != '\\') - { - return; - } - - string directory = GetNativeLibraryDirectory(); + string directory = GetInstalledDotNetRuntimeDirectory(); if (string.IsNullOrEmpty(directory)) { throw new InvalidOperationException("Could not locate an installed .NET runtime containing Microsoft.DiaSymReader.Native."); @@ -37,18 +31,7 @@ internal static void AddInstalledDotNetRuntimeDirectory() throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error()); } } -#endif - - public static string GetNativeLibraryDirectory() - { -#if NETFRAMEWORK - return GetInstalledDotNetRuntimeDirectory(); -#else - return RuntimeEnvironment.GetRuntimeDirectory(); -#endif - } -#if NETFRAMEWORK private static string GetInstalledDotNetRuntimeDirectory() { string root = Environment.GetEnvironmentVariable("DOTNET_ROOT") @@ -127,11 +110,9 @@ private static string FindDotNetHost() return null; } -#endif } } -#if NETFRAMEWORK namespace System.Runtime.CompilerServices { [AttributeUsage(AttributeTargets.Method, Inherited = false)] @@ -139,4 +120,5 @@ internal sealed class ModuleInitializerAttribute : Attribute { } } + #endif From 21804b540efe8fc4e027bc913274335425c0fd00 Mon Sep 17 00:00:00 2001 From: Tomas Matousek Date: Fri, 11 Sep 2026 13:51:56 -0700 Subject: [PATCH 4/6] Use TargetFrameworkIdentifier for Framework test architecture Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.DiaSymReader.PortablePdb.UnitTests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.DiaSymReader.PortablePdb.Tests/Microsoft.DiaSymReader.PortablePdb.UnitTests.csproj b/src/Microsoft.DiaSymReader.PortablePdb.Tests/Microsoft.DiaSymReader.PortablePdb.UnitTests.csproj index 649c9c8573..e8315564fc 100644 --- a/src/Microsoft.DiaSymReader.PortablePdb.Tests/Microsoft.DiaSymReader.PortablePdb.UnitTests.csproj +++ b/src/Microsoft.DiaSymReader.PortablePdb.Tests/Microsoft.DiaSymReader.PortablePdb.UnitTests.csproj @@ -7,7 +7,7 @@ We would need to download 32bit dotnet cli, which would add extra time to PR runs Testing 64bit only on Desktop suffixiently covers our interop code paths. --> - x64 + x64 From e264e9ad2555c3c06bf4453af091fecd7c814ca1 Mon Sep 17 00:00:00 2001 From: Tomas Matousek Date: Fri, 11 Sep 2026 14:03:26 -0700 Subject: [PATCH 5/6] Remove TestArchitectures from portable tests 64-bit is the default; no need to set the architecture explicitly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.DiaSymReader.PortablePdb.UnitTests.csproj | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Microsoft.DiaSymReader.PortablePdb.Tests/Microsoft.DiaSymReader.PortablePdb.UnitTests.csproj b/src/Microsoft.DiaSymReader.PortablePdb.Tests/Microsoft.DiaSymReader.PortablePdb.UnitTests.csproj index e8315564fc..9bfb889de3 100644 --- a/src/Microsoft.DiaSymReader.PortablePdb.Tests/Microsoft.DiaSymReader.PortablePdb.UnitTests.csproj +++ b/src/Microsoft.DiaSymReader.PortablePdb.Tests/Microsoft.DiaSymReader.PortablePdb.UnitTests.csproj @@ -3,11 +3,6 @@ $(NetCurrent);$(NetFrameworkCurrent) true - - x64 From 68baaba7d5440a6b64c37b1f43704f2b40581641 Mon Sep 17 00:00:00 2001 From: Tomas Matousek Date: Fri, 11 Sep 2026 14:40:14 -0700 Subject: [PATCH 6/6] Enable x86 .NET Framework test runs on Windows. Install the x86 shared framework from global.json tools.runtimes and run Framework tests on both x64 and x86. Core x86 is omitted until Microsoft.DiaSymReader passes Guid by pointer. Framework tests locate Native.x86.dll under .dotnet/x86. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- global.json | 7 ++- src/Directory.Build.targets | 6 +++ .../TestHelpers/DiaSymReaderNativeRuntime.cs | 50 ++++++++++++++++--- 3 files changed, 56 insertions(+), 7 deletions(-) diff --git a/global.json b/global.json index 68b114e5c9..0b39638943 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,11 @@ { "tools": { - "dotnet": "11.0.100-preview.5.26227.104" + "dotnet": "11.0.100-preview.5.26227.104", + "runtimes": { + "dotnet/x86": [ + "11.0.0-preview.5.26227.104" + ] + } }, "msbuild-sdks": { "Microsoft.DotNet.Arcade.Sdk": "11.0.0-beta.26310.1" diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index 8888098f03..fffa6e5430 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -2,4 +2,10 @@ + + + + x64;x86 + diff --git a/src/Microsoft.DiaSymReader.PortablePdb.Tests/TestHelpers/DiaSymReaderNativeRuntime.cs b/src/Microsoft.DiaSymReader.PortablePdb.Tests/TestHelpers/DiaSymReaderNativeRuntime.cs index 0f219e036b..1016ceeec0 100644 --- a/src/Microsoft.DiaSymReader.PortablePdb.Tests/TestHelpers/DiaSymReaderNativeRuntime.cs +++ b/src/Microsoft.DiaSymReader.PortablePdb.Tests/TestHelpers/DiaSymReaderNativeRuntime.cs @@ -5,6 +5,7 @@ #if NETFRAMEWORK using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.CompilerServices; @@ -60,18 +61,55 @@ private static string GetInstalledDotNetRuntimeDirectory() root = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "dotnet"); } - string shared = Path.Combine(root, "shared", "Microsoft.NETCore.App"); - if (!Directory.Exists(shared)) - { - return null; - } - string dllName = IntPtr.Size == 4 ? "Microsoft.DiaSymReader.Native.x86.dll" : RuntimeInformation.ProcessArchitecture == Architecture.Arm64 ? "Microsoft.DiaSymReader.Native.arm64.dll" : "Microsoft.DiaSymReader.Native.amd64.dll"; + foreach (string candidate in GetRuntimeRootCandidates(root)) + { + string directory = FindRuntimeDirectory(candidate, dllName); + if (directory != null) + { + return directory; + } + } + + return null; + } + + private static IEnumerable GetRuntimeRootCandidates(string root) + { + if (IntPtr.Size == 4) + { + string rootX86 = Environment.GetEnvironmentVariable("DOTNET_ROOT(x86)") + ?? Environment.GetEnvironmentVariable("DOTNET_ROOT_X86"); + if (!string.IsNullOrEmpty(rootX86)) + { + yield return rootX86; + } + + if (!string.IsNullOrEmpty(root)) + { + yield return Path.Combine(root, "x86"); + } + } + + if (!string.IsNullOrEmpty(root)) + { + yield return root; + } + } + + private static string FindRuntimeDirectory(string root, string dllName) + { + string shared = Path.Combine(root, "shared", "Microsoft.NETCore.App"); + if (!Directory.Exists(shared)) + { + return null; + } + return Directory.GetDirectories(shared) .Select(dir => (dir, version: TryParseVersion(Path.GetFileName(dir)))) .Where(item => item.version != null && File.Exists(Path.Combine(item.dir, dllName)))