none
64BitアプリでWinAPIを呼び出す(SHGetPathFromIDList、SHGetPathFromIDListW) RRS feed

  • 質問

  • USBカードリーダーの抜き差しを検知するために SHChangeNotifyRegister を使用してさらにドライブレター取得のために SHGetPathFromIDListW を使用しています。
    プログラムが32bitで動いているときは問題ないのですが、64bitにするとエラーになります。
    SHGetPathFromIDListWが8バイトのIntPtrを処理できていないようです。
    32bit、64bitともに(AnyCPUで)動かしたいのですが、良い方法はないでしょうか?

    開発環境: VS2017, C#, WPF

    public partial class MainWindow : Window {
        public MainWindow() {
            InitializeComponent();
        }
    
        protected override void OnContentRendered( EventArgs e ) {
            base.OnContentRendered( e );
            RegiterNotify(this);
        }
    
        private static void RegiterNotify( Window window ) {
            var source = HwndSource.FromHwnd( new WindowInteropHelper( window ).Handle );
            source.AddHook( WndProc );
    
            var notifyEntry = new SHChangeNotifyEntry() { pIdl = IntPtr.Zero, Recursively = true };
            var SHChangeNotifyRegisterHndle = SHChangeNotifyRegister( source.Handle,
                                                  SHCNF.SHCNF_TYPE | SHCNF.SHCNF_IDLIST,
                                                  SHCNE.SHCNE_MEDIAINSERTED | SHCNE.SHCNE_MEDIAREMOVED,
                                                  (uint)WM_SHNOTIFY,
                                                  1,
                                                  ref notifyEntry );
        }
    
        private static IntPtr WndProc( IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled ) {
            var hwndSource = HwndSource.FromHwnd( hwnd );
            var target = hwndSource.RootVisual;
            if( target != null ) {
                if( msg == WM_SHNOTIFY ) {
                    var driveRootPathBuffer = new StringBuilder( "A:\\" );
                    switch( (SHCNE)lParam ) {
                        case SHCNE.SHCNE_MEDIAINSERTED:
                        case SHCNE.SHCNE_MEDIAREMOVED:
                            var item1 = Marshal.ReadInt32( wParam );
                            // ドライブレター取得
                            SHGetPathFromIDListW( (IntPtr)item1, driveRootPathBuffer ); // ★ System.AccessViolationException
    
                            var driveString = driveRootPathBuffer.ToString().Substring( 0, 1 );
                            System.Diagnostics.Debug.WriteLine( driveString );
                            break;
    
                    }
                }
            }
    
            return IntPtr.Zero;
        }
    
        [DllImport( "shell32.dll", SetLastError = true, EntryPoint = "#2", CharSet = CharSet.Auto )]
        public static extern IntPtr SHChangeNotifyRegister( IntPtr hWnd, SHCNF fSources, SHCNE fEvents, uint wMsg, int cEntries, ref SHChangeNotifyEntry pFsne );
    
        [StructLayout( LayoutKind.Sequential, CharSet = CharSet.Auto )]
        public struct SHChangeNotifyEntry {
            public IntPtr pIdl;
            [MarshalAs( UnmanagedType.Bool )]
            public Boolean Recursively;
        }
    
        [DllImport( "shell32.dll" )]
        [return: MarshalAs( UnmanagedType.Bool )]
        public static extern bool SHGetPathFromIDListW( IntPtr pidl, [MarshalAs( UnmanagedType.LPTStr )] StringBuilder pszPath );
    
        public const int WM_SHNOTIFY = 0x00000401;
    
        public enum SHCNF {
            SHCNF_IDLIST = 0x0000,
            SHCNF_TYPE = 0x00FF,
        }

        [Flags]
        public enum SHCNE : uint {
            SHCNE_MEDIAINSERTED = 0x00000020,
            SHCNE_MEDIAREMOVED = 0x00000040,
        }



    2018年3月20日 9:10

回答

  • すみません。検証等は行っていないのですが、wParam (64 ビット) を Marshal.ReadInt32 で 32 ビットに切り詰めているところが気になりました。

    Marshal.ReadInt32 を Marshal.ReadInt64 へ変更したり、wParam をそのまま SHGetPathFromIDListW 関数の引数に渡すように修正したりすることで現象は変わりますでしょうか?

    参考サイト : https://msdn.microsoft.com/ja-jp/library/aa384242(v=vs.85).aspx
    2018年3月20日 9:23
  • すでに kenjinote さんが指摘されていますが、一応。

    -----------------------------------------------------
    SHGetPathFromIDList function
    https://msdn.microsoft.com/ja-jp/library/windows/desktop/bb762194(v=vs.85).aspx

    pidl [in]
    Type: PCIDLIST_ABSOLUTE
    The address of an item identifier list that specifies a file
    or directory location relative to the root of the namespace (the desktop).
    -----------------------------------------------------
    つまり SHGetPathFromIDList AIP の 1st パラメータはポインタ。
    • 回答としてマーク kitunechan 2018年3月22日 3:38
    2018年3月20日 9:51
  • Marshal.ReadInt32 を Marshal.ReadInt64 へ変更したり、wParam をそのまま SHGetPathFromIDListW 関数の引数に渡すように修正したりすることで現象は変わりますでしょうか?

    参考サイト : https://msdn.microsoft.com/ja-jp/library/aa384242(v=vs.85).aspx

    x86/x64 問わずに動かせるコードにしたいということですし、現状の等価コードで考えると、Marshal.ReadIntPtr でしょうね。
    また、このページの Remarks によると、"wParam is a pointer to two PIDLIST_ABSOLUTE pointers" なので、wParam 自身を SHGetPathFromIDList に渡すと問題がありそうです。

    別件ですが、SHGetPathFromIDList の第 2 引数は MAX_PATH 文字数以上のバッファが必要という仕様ですので、StringBuilder を "A:\\" みたいにせず、260 と渡してバッファ確保しておいた方が良いと思います。

    2018年3月20日 13:52
    モデレータ
  • 既に指摘があるようにMarshal.ReadIntPtrを使う必要があります。それ以外にもこのコードにはいくつも問題があります。現在は動作しているかもしれませんが、今後動かなくなる可能性があるため、わかる範囲で指摘しておきます。

    • SHChangeNotifyRegisterの定義がいろいろと間違っています。LastErrorが設定されるとは書かれていないため「SetLastError = true」に意味はありません。EntryPointはWindows 2000以降はデフォルトで動作するため「EntryPoint = "#2"」はむしろ動作しなくなる危険性をはらんでいます。文字列は扱っていないため「CharSet = CharSet.Auto」に意味はありません。戻り値はULONGであり、C#であればintにすべきです。第2引数がSHCNFではなくSHCNRFで、指定すべき値も全く間違っています。確認したところ「SHCNRF_ShellLevel = 0x0002」で動作しました。最終引数はrefではなく配列です。
    • SHChangeNotifyEntryは文字列を扱っていないため「CharSet = CharSet.Auto」に意味はありません。IntPtr.Zeroを与えていますが正しくありません。ドライブの変更を検出するのであればコンピューターフォルダーを監視対象と指定すべきです。(とは言えやっぱりIntPtr.Zero / falseでも動作しますね。うーん)
    • SHGetPathFromIDListWは明示的にUnicodeエントリーポイントを指定していますがCharSetが指定されていないため危険です。
    • WndProcはメッセージを処理したのであればhandledを指定すべきです。
    • WM_SHNOTIFYはWM_USERではなくWM_APPを使う方が安全です。

    以上を踏まえ、Visual Studio 2017を使用されているとのことですのでC# 7.2で記述したコードは次のようになります。

    public partial class MainWindow : Window {
        public MainWindow() {
            InitializeComponent();
            ContentRendered += delegate { RegiterNotify(this); };
        }
    
        [StructLayout(LayoutKind.Sequential)]
        struct SHChangeNotifyEntry {
            public IntPtr pidl;
            public bool fRecursive;
        }
    
        [DllImport("shell32.dll", CharSet = CharSet.Unicode)]
        static extern bool SHGetPathFromIDList(IntPtr pidl, StringBuilder pszPath);
        [DllImport("shell32.dll", PreserveSig = false)]
        static extern void SHGetKnownFolderIDList(in Guid rfid, int dwFlags, IntPtr hToken, out IntPtr ppidl);
        [DllImport("shell32.dll")]
        static extern int SHChangeNotifyRegister(IntPtr hwnd, int fSources, int fEvents, int wMsg, int cEntries, SHChangeNotifyEntry[] pshcne);
    
        const int WM_APP = 0x8000;
        const int WM_SHNOTIFY = WM_APP + 1;
        const int SHCNRF_ShellLevel = 0x0002;
        const int SHCNE_MEDIAINSERTED = 0x00000020;
        const int SHCNE_MEDIAREMOVED = 0x00000040;
        static Guid FOLDERID_ComputerFolder = new Guid("{0AC0837C-BBF8-452A-850D-79D08E667CA7}");
    
        static IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) {
            if (msg == WM_SHNOTIFY)
                switch ((int)lParam) {
                    case SHCNE_MEDIAINSERTED:
                    case SHCNE_MEDIAREMOVED:
                        var path = new StringBuilder(260);
                        var result = SHGetPathFromIDList(Marshal.ReadIntPtr(wParam), path);
                        Debug.Assert(result);
                        var driveName = path.ToString(0, 1);
                        Debug.WriteLine($"Drive: {driveName}");
                        handled = true;
                        return IntPtr.Zero;
                }
            return IntPtr.Zero;
        }
    
        static void RegiterNotify(Window window) {
            var source = HwndSource.FromHwnd(new WindowInteropHelper(window).Handle);
            source.AddHook(WndProc);
            SHGetKnownFolderIDList(FOLDERID_ComputerFolder, 0, IntPtr.Zero, out var computer);
            var entries = new[] { new SHChangeNotifyEntry { pidl = computer, fRecursive = false } };
            var SHChangeNotifyRegisterHndle = SHChangeNotifyRegister(source.Handle, SHCNRF_ShellLevel, SHCNE_MEDIAINSERTED | SHCNE_MEDIAREMOVED, WM_SHNOTIFY, entries.Length, entries);
            Marshal.FreeCoTaskMem(computer);
        }
    }

    あとはSHChangeNotifyDeregisterしないとですね。

    • 回答としてマーク kitunechan 2018年3月22日 3:38
    2018年3月21日 1:35

すべての返信

  • すみません。検証等は行っていないのですが、wParam (64 ビット) を Marshal.ReadInt32 で 32 ビットに切り詰めているところが気になりました。

    Marshal.ReadInt32 を Marshal.ReadInt64 へ変更したり、wParam をそのまま SHGetPathFromIDListW 関数の引数に渡すように修正したりすることで現象は変わりますでしょうか?

    参考サイト : https://msdn.microsoft.com/ja-jp/library/aa384242(v=vs.85).aspx
    2018年3月20日 9:23
  • すでに kenjinote さんが指摘されていますが、一応。

    -----------------------------------------------------
    SHGetPathFromIDList function
    https://msdn.microsoft.com/ja-jp/library/windows/desktop/bb762194(v=vs.85).aspx

    pidl [in]
    Type: PCIDLIST_ABSOLUTE
    The address of an item identifier list that specifies a file
    or directory location relative to the root of the namespace (the desktop).
    -----------------------------------------------------
    つまり SHGetPathFromIDList AIP の 1st パラメータはポインタ。
    • 回答としてマーク kitunechan 2018年3月22日 3:38
    2018年3月20日 9:51
  • Marshal.ReadInt32 を Marshal.ReadInt64 へ変更したり、wParam をそのまま SHGetPathFromIDListW 関数の引数に渡すように修正したりすることで現象は変わりますでしょうか?

    参考サイト : https://msdn.microsoft.com/ja-jp/library/aa384242(v=vs.85).aspx

    x86/x64 問わずに動かせるコードにしたいということですし、現状の等価コードで考えると、Marshal.ReadIntPtr でしょうね。
    また、このページの Remarks によると、"wParam is a pointer to two PIDLIST_ABSOLUTE pointers" なので、wParam 自身を SHGetPathFromIDList に渡すと問題がありそうです。

    別件ですが、SHGetPathFromIDList の第 2 引数は MAX_PATH 文字数以上のバッファが必要という仕様ですので、StringBuilder を "A:\\" みたいにせず、260 と渡してバッファ確保しておいた方が良いと思います。

    2018年3月20日 13:52
    モデレータ
  • 既に指摘があるようにMarshal.ReadIntPtrを使う必要があります。それ以外にもこのコードにはいくつも問題があります。現在は動作しているかもしれませんが、今後動かなくなる可能性があるため、わかる範囲で指摘しておきます。

    • SHChangeNotifyRegisterの定義がいろいろと間違っています。LastErrorが設定されるとは書かれていないため「SetLastError = true」に意味はありません。EntryPointはWindows 2000以降はデフォルトで動作するため「EntryPoint = "#2"」はむしろ動作しなくなる危険性をはらんでいます。文字列は扱っていないため「CharSet = CharSet.Auto」に意味はありません。戻り値はULONGであり、C#であればintにすべきです。第2引数がSHCNFではなくSHCNRFで、指定すべき値も全く間違っています。確認したところ「SHCNRF_ShellLevel = 0x0002」で動作しました。最終引数はrefではなく配列です。
    • SHChangeNotifyEntryは文字列を扱っていないため「CharSet = CharSet.Auto」に意味はありません。IntPtr.Zeroを与えていますが正しくありません。ドライブの変更を検出するのであればコンピューターフォルダーを監視対象と指定すべきです。(とは言えやっぱりIntPtr.Zero / falseでも動作しますね。うーん)
    • SHGetPathFromIDListWは明示的にUnicodeエントリーポイントを指定していますがCharSetが指定されていないため危険です。
    • WndProcはメッセージを処理したのであればhandledを指定すべきです。
    • WM_SHNOTIFYはWM_USERではなくWM_APPを使う方が安全です。

    以上を踏まえ、Visual Studio 2017を使用されているとのことですのでC# 7.2で記述したコードは次のようになります。

    public partial class MainWindow : Window {
        public MainWindow() {
            InitializeComponent();
            ContentRendered += delegate { RegiterNotify(this); };
        }
    
        [StructLayout(LayoutKind.Sequential)]
        struct SHChangeNotifyEntry {
            public IntPtr pidl;
            public bool fRecursive;
        }
    
        [DllImport("shell32.dll", CharSet = CharSet.Unicode)]
        static extern bool SHGetPathFromIDList(IntPtr pidl, StringBuilder pszPath);
        [DllImport("shell32.dll", PreserveSig = false)]
        static extern void SHGetKnownFolderIDList(in Guid rfid, int dwFlags, IntPtr hToken, out IntPtr ppidl);
        [DllImport("shell32.dll")]
        static extern int SHChangeNotifyRegister(IntPtr hwnd, int fSources, int fEvents, int wMsg, int cEntries, SHChangeNotifyEntry[] pshcne);
    
        const int WM_APP = 0x8000;
        const int WM_SHNOTIFY = WM_APP + 1;
        const int SHCNRF_ShellLevel = 0x0002;
        const int SHCNE_MEDIAINSERTED = 0x00000020;
        const int SHCNE_MEDIAREMOVED = 0x00000040;
        static Guid FOLDERID_ComputerFolder = new Guid("{0AC0837C-BBF8-452A-850D-79D08E667CA7}");
    
        static IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) {
            if (msg == WM_SHNOTIFY)
                switch ((int)lParam) {
                    case SHCNE_MEDIAINSERTED:
                    case SHCNE_MEDIAREMOVED:
                        var path = new StringBuilder(260);
                        var result = SHGetPathFromIDList(Marshal.ReadIntPtr(wParam), path);
                        Debug.Assert(result);
                        var driveName = path.ToString(0, 1);
                        Debug.WriteLine($"Drive: {driveName}");
                        handled = true;
                        return IntPtr.Zero;
                }
            return IntPtr.Zero;
        }
    
        static void RegiterNotify(Window window) {
            var source = HwndSource.FromHwnd(new WindowInteropHelper(window).Handle);
            source.AddHook(WndProc);
            SHGetKnownFolderIDList(FOLDERID_ComputerFolder, 0, IntPtr.Zero, out var computer);
            var entries = new[] { new SHChangeNotifyEntry { pidl = computer, fRecursive = false } };
            var SHChangeNotifyRegisterHndle = SHChangeNotifyRegister(source.Handle, SHCNRF_ShellLevel, SHCNE_MEDIAINSERTED | SHCNE_MEDIAREMOVED, WM_SHNOTIFY, entries.Length, entries);
            Marshal.FreeCoTaskMem(computer);
        }
    }

    あとはSHChangeNotifyDeregisterしないとですね。

    • 回答としてマーク kitunechan 2018年3月22日 3:38
    2018年3月21日 1:35
  • たくさんの回答有り難うございます。
    大変参考になりました。

    Marshal.ReadIntPtr(wParam)を渡すことで動作することを確認しました。
    wParamを直接ではダメでした。

    >佐祐理 様
    >戻り値はULONGであり、C#であればintにすべきです。
    符号の有無の話なので、どちらでも良いとは思いますが、
    uintだと思うのですがintにしたほうが良いでしょうか?

    SHChangeNotifyUnregisterを使っていました。
    SHChangeNotifyDeregisterのほうが良さそうですね。

    [DllImport( "shell32.dll" )]
    static extern uint SHChangeNotifyRegister( IntPtr hwnd, int fSources, int fEvents, int wMsg, int cEntries, SHChangeNotifyEntry[] pshcne );

    [DllImport( "shell32.dll" )]
    static extern Boolean SHChangeNotifyDeregister( uint hNotify );

    2018年3月22日 4:11
  • >戻り値はULONGであり、C#であればintにすべきです。
    符号の有無の話なので、どちらでも良いとは思いますが、
    uintだと思うのですがintにしたほうが良いでしょうか?
    プロトタイプ宣言はULONGとあるのに対し質問文にはIntPtrとありサイズが誤っているために指摘しました。
    符号については支障がない限りsigned型を使うようです(uintでなくint、UIntPtrでなくIntPtr等)。がこのことに触れているドキュメントを見つけられませんでした。
    SHChangeNotifyUnregisterを使っていました。
    SHChangeNotifyDeregisterのほうが良さそうですね。
    SHChangeNotifyUnregisterという関数は存在しなさそうです。もしかしてSHChangeNotifyRegisterと同様に「EntryPoint = "#4"」を指定されていたということでしょうか? 再度の指摘となりますが、SHChangeNotifyRegisterSHChangeNotifyDeregister共にordinal(序数)はドキュメントで定められていないため今後変更になる可能性があります。ordinalでの関数アクセスはお勧めできません。
    2018年3月22日 6:14