为什么这个互操作会使.net运行时崩溃?

本文关键字:net 运行时 崩溃 互操作 为什么 | 更新日期: 2023-09-27 18:06:40

我正在尝试迭代一些文件并获取它们的shell图标;为了实现这一点,我使用DirectoryInfo.EnumerateFileSystemInfos和一些P/Invoke来调用Win32 SHGetFileInfo函数。但是两者的结合似乎会在内部某个地方破坏内存,导致难看的崩溃。

我把我的代码归结为两个类似的测试用例,这两个用例似乎都无缘无故地崩溃了。如果我不调用DirectoryInfo.EnumerateFileSystemInfos,不会出现崩溃;如果我不调用SHGetFileInfo,不会出现崩溃。请注意,我已经在代码中删除了FileSystemInfo对象的实际use,因为我可以通过迭代它们并反复请求文本文件图标来复制它。但是为什么呢?

这里是我的完整的,最小的测试用例。在VS调试器下运行它们以确保没有启用优化:

using System;
using System.Linq;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Media.Imaging;
namespace IconCrashRepro
{
    // Compile for .NET 4 (I'm using 4.5.1).
    // Also seems to fail in 3.5 with GetFileSystemInfos() instead of EnumerateFileSystemInfos()
    public class Program
    {
        // Compile for .NET 4 (I'm using 4.5.1)
        public static void Main()
        {
            // Keep a list of the objects we generate so
            // that they're not garbage collected right away
            var sources = new List<BitmapSource>();
            // Any directory seems to do the trick, so long
            // as it's not empty. Within VS, '.' should be
            // the Debug folder
            var dir = new DirectoryInfo(@".");
            // Track the number of iterations, just to see
            ulong iteration = 0;
            while (true)
            {
                // This is where things get interesting -- without the EnumerateFileSystemInfos,
                // the bug does not appear. Without the call to SHGetFileInfo, the bug also
                // does not appear. It seems to be the combination that causes problems.
                var infos = dir.EnumerateFileSystemInfos().ToList();
                Debug.Assert(infos.Count > 0);
                foreach (var info in infos)
                {
                    var shFileInfo = new SHFILEINFO();
                    var result = SHGetFileInfo(".txt", (uint)FileAttributes.Normal, ref shFileInfo, (uint)Marshal.SizeOf(shFileInfo), SHGFI_USEFILEATTRIBUTES | SHGFI_ICON | SHGFI_SMALLICON);
                    //var result = SHGetFileInfo(info.FullName, (uint)info.Attributes, ref shFileInfo, (uint)Marshal.SizeOf(shFileInfo), SHGFI_USEFILEATTRIBUTES | SHGFI_ICON | SHGFI_SMALLICON);
                    if (result != IntPtr.Zero && shFileInfo.hIcon != IntPtr.Zero)
                    {
                        var bmpSource = Imaging.CreateBitmapSourceFromHIcon(
                            shFileInfo.hIcon,
                            Int32Rect.Empty,
                            BitmapSizeOptions.FromEmptyOptions());
                        sources.Add(bmpSource);
                        // Originally I was releasing the handle, but even if
                        // I don't the bug occurs!
                        //DestroyIcon(shFileInfo.hIcon);
                    }
                    // Execution fails during Collect; if I remove the
                    // call to Collect, execution fails later during
                    // CreateBitmapSourceFromHIcon (it calls
                    // AddMemoryPressure internally which I suspect
                    // results in a collect at that point).
                    GC.Collect();
                    ++iteration;
                }
            }
        }

        public static void OtherBugRepro()
        {
            // Rename this to Main() to run.
            // Removing any single line from this method
            // will stop it from crashing -- including the
            // empty if and the Debug.Assert!
            var sources = new List<BitmapSource>();
            var dir = new DirectoryInfo(@".");
            var infos = dir.EnumerateFileSystemInfos().ToList();
            Debug.Assert(infos.Count > 0);
            // Crashes on the second iteration -- says that
            // `infos` has been modified during loop execution!!
            foreach (var info in infos)
            {
                var shFileInfo = new SHFILEINFO();
                var result = SHGetFileInfo(".txt", (uint)FileAttributes.Normal, ref shFileInfo, (uint)Marshal.SizeOf(shFileInfo), SHGFI_USEFILEATTRIBUTES | SHGFI_ICON | SHGFI_SMALLICON);
                if (result != IntPtr.Zero && shFileInfo.hIcon != IntPtr.Zero)
                {
                    if (sources.Count == 1000) { }
                }
            }
        }

        [StructLayout(LayoutKind.Sequential)]
        private struct SHFILEINFO
        {
            public IntPtr hIcon;
            public int iIcon;
            public uint dwAttributes;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
            public string szDisplayName;
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 80)]
            public string szTypeName;
        }
        private const uint SHGFI_ICON = 0x100;
        private const uint SHGFI_LARGEICON = 0x0;
        private const uint SHGFI_SMALLICON = 0x1;
        private const uint SHGFI_USEFILEATTRIBUTES = 0x10;
        [DllImport("shell32.dll", CharSet = CharSet.Unicode)]
        private static extern IntPtr SHGetFileInfo([MarshalAs(UnmanagedType.LPWStr)] string pszPath, uint dwFileAttributes, ref SHFILEINFO psfi, uint cbSizeFileInfo, uint uFlags);
        [DllImport("user32.dll", SetLastError = true)]
        private static extern bool DestroyIcon(IntPtr hIcon);
    }
}

有人能发现这个bug吗?任何帮助都是感激的!

为什么这个互操作会使.net运行时崩溃?

调用的是Unicode版本的函数,但传递的是ANSI版本的结构体。您需要在SHFILEINFO结构体声明中指定CharSet

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]