dotnet / corert

This repo contains CoreRT, an experimental .NET Core runtime optimized for AOT (ahead of time compilation) scenarios, with the accompanying compiler toolchain.
http://dot.net
MIT License
2.91k stars 510 forks source link

ComImportAttribute doesn't seem to work #6252

Open 0x53A opened 6 years ago

0x53A commented 6 years ago

Hi, this project looks really great and is exactly what I need.

I want to create a simple internal tool. This is mostly a console application, but for added convenience, it should be minimally interactive when no parameters are passed to it.

That means MessageBoxes and OpenFileDialogs to gather the information from the user.

Without .NET Core 3, I don't have access to WinForms, so I thought I'd just p/invoke it.

pinvoking MessageBox works good, but I have issues with the Vista File Dialogs, which use COM.


code was adapted from https://stackoverflow.com/a/15386992/1872399.

This works in .NET Core 2.1, but doesn't work with CoreRT.


// https://stackoverflow.com/a/15386992/1872399

using System;
using System.Runtime.InteropServices;

namespace MyNamespace
{
    class Program
    {
        static void Main()
        {
            new FolderBrowser2().ShowDialog(null);
        }
    }

    public enum DialogResult
    {
        Cancel, Abort, OK
    }

    public class FolderBrowser2
    {
        public string DirectoryPath { get; set; }

        public DialogResult ShowDialog(IntPtr? owner)
        {
            IntPtr hwndOwner = owner ?? GetActiveWindow();

            IFileOpenDialog dialog = (IFileOpenDialog)new FileOpenDialog();
            try
            {
                IShellItem item;
                if (!string.IsNullOrEmpty(DirectoryPath))
                {
                    IntPtr idl;
                    uint atts = 0;
                    if (SHILCreateFromPath(DirectoryPath, out idl, ref atts) == 0)
                    {
                        if (SHCreateShellItem(IntPtr.Zero, IntPtr.Zero, idl, out item) == 0)
                        {
                            dialog.SetFolder(item);
                        }
                        Marshal.FreeCoTaskMem(idl);
                    }
                }
                dialog.SetOptions(FOS.FOS_PICKFOLDERS | FOS.FOS_FORCEFILESYSTEM);
                uint hr = dialog.Show(hwndOwner);
                if (hr == ERROR_CANCELLED)
                    return DialogResult.Cancel;

                if (hr != 0)
                    return DialogResult.Abort;

                dialog.GetResult(out item);
                string path;
                item.GetDisplayName(SIGDN.SIGDN_FILESYSPATH, out path);
                DirectoryPath = path;
                return DialogResult.OK;
            }
            finally
            {
                Marshal.ReleaseComObject(dialog);
            }
        }

        [DllImport("shell32.dll")]
        private static extern int SHILCreateFromPath([MarshalAs(UnmanagedType.LPWStr)] string pszPath, out IntPtr ppIdl, ref uint rgflnOut);

        [DllImport("shell32.dll")]
        private static extern int SHCreateShellItem(IntPtr pidlParent, IntPtr psfParent, IntPtr pidl, out IShellItem ppsi);

        [DllImport("user32.dll")]
        private static extern IntPtr GetActiveWindow();

        private const uint ERROR_CANCELLED = 0x800704C7;

        [ComImport]
        [Guid("DC1C5A9C-E88A-4dde-A5A1-60F82A20AEF7")]
        private class FileOpenDialog
        {
        }

        [ComImport]
        [Guid("42f85136-db7e-439c-85f1-e4075d135fc8")]
        [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        private interface IFileOpenDialog
        {
            [PreserveSig]
            uint Show([In] IntPtr parent); // IModalWindow
            void SetFileTypes();  // not fully defined
            void SetFileTypeIndex([In] uint iFileType);
            void GetFileTypeIndex(out uint piFileType);
            void Advise(); // not fully defined
            void Unadvise();
            void SetOptions([In] FOS fos);
            void GetOptions(out FOS pfos);
            void SetDefaultFolder(IShellItem psi);
            void SetFolder(IShellItem psi);
            void GetFolder(out IShellItem ppsi);
            void GetCurrentSelection(out IShellItem ppsi);
            void SetFileName([In, MarshalAs(UnmanagedType.LPWStr)] string pszName);
            void GetFileName([MarshalAs(UnmanagedType.LPWStr)] out string pszName);
            void SetTitle([In, MarshalAs(UnmanagedType.LPWStr)] string pszTitle);
            void SetOkButtonLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszText);
            void SetFileNameLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszLabel);
            void GetResult(out IShellItem ppsi);
            void AddPlace(IShellItem psi, int alignment);
            void SetDefaultExtension([In, MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension);
            void Close(int hr);
            void SetClientGuid();  // not fully defined
            void ClearClientData();
            void SetFilter([MarshalAs(UnmanagedType.Interface)] IntPtr pFilter);
            void GetResults([MarshalAs(UnmanagedType.Interface)] out IntPtr ppenum); // not fully defined
            void GetSelectedItems([MarshalAs(UnmanagedType.Interface)] out IntPtr ppsai); // not fully defined
        }

        [ComImport]
        [Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE")]
        [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        private interface IShellItem
        {
            void BindToHandler(); // not fully defined
            void GetParent(); // not fully defined
            void GetDisplayName([In] SIGDN sigdnName, [MarshalAs(UnmanagedType.LPWStr)] out string ppszName);
            void GetAttributes();  // not fully defined
            void Compare();  // not fully defined
        }

        private enum SIGDN : uint
        {
            SIGDN_DESKTOPABSOLUTEEDITING = 0x8004c000,
            SIGDN_DESKTOPABSOLUTEPARSING = 0x80028000,
            SIGDN_FILESYSPATH = 0x80058000,
            SIGDN_NORMALDISPLAY = 0,
            SIGDN_PARENTRELATIVE = 0x80080001,
            SIGDN_PARENTRELATIVEEDITING = 0x80031001,
            SIGDN_PARENTRELATIVEFORADDRESSBAR = 0x8007c001,
            SIGDN_PARENTRELATIVEPARSING = 0x80018001,
            SIGDN_URL = 0x80068000
        }

        [Flags]
        private enum FOS
        {
            FOS_ALLNONSTORAGEITEMS = 0x80,
            FOS_ALLOWMULTISELECT = 0x200,
            FOS_CREATEPROMPT = 0x2000,
            FOS_DEFAULTNOMINIMODE = 0x20000000,
            FOS_DONTADDTORECENT = 0x2000000,
            FOS_FILEMUSTEXIST = 0x1000,
            FOS_FORCEFILESYSTEM = 0x40,
            FOS_FORCESHOWHIDDEN = 0x10000000,
            FOS_HIDEMRUPLACES = 0x20000,
            FOS_HIDEPINNEDPLACES = 0x40000,
            FOS_NOCHANGEDIR = 8,
            FOS_NODEREFERENCELINKS = 0x100000,
            FOS_NOREADONLYRETURN = 0x8000,
            FOS_NOTESTFILECREATE = 0x10000,
            FOS_NOVALIDATE = 0x100,
            FOS_OVERWRITEPROMPT = 2,
            FOS_PATHMUSTEXIST = 0x800,
            FOS_PICKFOLDERS = 0x20,
            FOS_SHAREAWARE = 0x4000,
            FOS_STRICTFILETYPES = 4
        }
    }
}

Compile Time

dotnet publish -r win-x64 -c release

 Generating native code
EXEC : warning : RD.XML processing will change before release (https://github.com/dotnet/corert/issues/5001) [test.csproj]
EXEC : warning : Method `[test]MyNamespace.FolderBrowser2.ShowDialog(Nullable`1<native int>)` will always throw because: [TEMPORARY EXCEPTION MESSAGE] InvalidProgramSpecific: Void FileOpenDialog..ctor() [test.csproj]

Run Time

Unhandled Exception: System.InvalidProgramException: Common Language Runtime detected an invalid program. The body of method 'Void FileOpenDialog..ctor()' is invalid.
   at Test!<BaseAddress>+0x113ce8
   at Internal.Runtime.CompilerHelpers.ThrowHelpers.ThrowInvalidProgramExceptionWithArgument(ExceptionStringID, String) + 0x8
   at MyNamespace.FolderBrowser2.ShowDialog(Nullable`1) + 0x14
   at MyNamespace.Program.Main() + 0x25
   at Test!<BaseAddress>+0x16fed2

Used version

<PackageReference Include="Microsoft.DotNet.ILCompiler" Version="1.0.0-alpha-26820-01" />

.NET Sdk: 2.1.301

jkotas commented 6 years ago

The problem is tracked by #4219. CoreRT does not support COM interop currently and it is unlikely to change anytime soon.

If you need to do a little bit of COM interop, you can workaround this by doing the COM interop manually. I have attached what it looks like for your code sample.

If you need to do a lot of COM interop, you may take a look at https://github.com/SharpGenTools/SharpGenTools to generate the COM interop marshaling code. The code generated by this tool should be compatible with CoreRT.

using System;
using System.Runtime.InteropServices;

namespace MyNamespace
{
    class Program
    {
        static void Main()
        {
            new FolderBrowser2().ShowDialog(null);
        }
    }

    public enum DialogResult
    {
        Cancel, Abort, OK
    }

    public class FolderBrowser2
    {
        public string DirectoryPath { get; set; }

        public DialogResult ShowDialog(IntPtr? owner)
        {
            IntPtr hwndOwner = owner ?? GetActiveWindow();

            using (var dialog = FileOpenDialog.Create())
            {
                if (!string.IsNullOrEmpty(DirectoryPath))
                {
                    IntPtr idl;
                    uint atts = 0;
                    if (SHILCreateFromPath(DirectoryPath, out idl, ref atts) == 0)
                    {
                        using (var item = new ShellItem())
                        {
                            if (SHCreateShellItem(IntPtr.Zero, IntPtr.Zero, idl, out item.Pointer) == 0)
                            {
                                dialog.SetFolder(item);
                            }
                        }
                        Marshal.FreeCoTaskMem(idl);
                    }
                }
                dialog.SetOptions(FOS.FOS_PICKFOLDERS | FOS.FOS_FORCEFILESYSTEM);
                uint hr = dialog.Show(hwndOwner);
                if (hr == ERROR_CANCELLED)
                    return DialogResult.Cancel;

                if (hr != 0)
                    return DialogResult.Abort;

                using (var item = dialog.GetResult())
                {
                    string path;
                    item.GetDisplayName(SIGDN.SIGDN_FILESYSPATH, out path);
                    DirectoryPath = path;
                }
                return DialogResult.OK;
            }
        }

        [DllImport("ole32.dll")]
        private static extern int CoInitializeEx(IntPtr pvReserved, int dwCoInit);

        [DllImport("shell32.dll")]
        private static extern int SHILCreateFromPath([MarshalAs(UnmanagedType.LPWStr)] string pszPath, out IntPtr ppIdl, ref uint rgflnOut);

        [DllImport("shell32.dll")]
        private static extern int SHCreateShellItem(IntPtr pidlParent, IntPtr psfParent, IntPtr pidl, out IntPtr ppsi);

        [DllImport("user32.dll")]
        private static extern IntPtr GetActiveWindow();

        private const uint ERROR_CANCELLED = 0x800704C7;

        private unsafe class MiniComObject : IDisposable
        {
            public IntPtr Pointer;

            delegate int IUnknown_Release(IntPtr thisPtr);
            public void Dispose()
            {
                Marshal.GetDelegateForFunctionPointer<IUnknown_Release>(*((*(IntPtr**)Pointer) + 2))(Pointer);
                Pointer = IntPtr.Zero;
            }
        }

        private unsafe class FileOpenDialog : MiniComObject
        {
            [DllImport("ole32.dll")]
            private static extern int CoCreateInstance(ref Guid rclsid, IntPtr pUnkOuter,
                                       Int32 dwClsContext,
                                       ref Guid riid,
                                       out IntPtr ppv);

            public static FileOpenDialog Create()
            {
                Marshal.ThrowExceptionForHR(CoInitializeEx(IntPtr.Zero, 0 /* COINIT_APARTMENTTHREADED */), new IntPtr(-1));

                FileOpenDialog result = new FileOpenDialog();

                Guid CLSID_FileOpenDialog = new Guid("DC1C5A9C-E88A-4dde-A5A1-60F82A20AEF7");
                Guid IID_IFileOpenDialog = new Guid("42f85136-db7e-439c-85f1-e4075d135fc8");
                int hr = CoCreateInstance(ref CLSID_FileOpenDialog, IntPtr.Zero,
                                       1, // CLSCTX_INPROC_SERVER,
                                       ref IID_IFileOpenDialog,
                                       out result.Pointer);
                Marshal.ThrowExceptionForHR(hr, new IntPtr(-1));

                return result;
            }

            delegate uint IFileOpenDialog_Show(IntPtr thisPtr, IntPtr parent);
            public uint Show([In] IntPtr parent)
            {
                return Marshal.GetDelegateForFunctionPointer<IFileOpenDialog_Show>(*((*(IntPtr**)Pointer) + 3))(Pointer, parent);
            }

            delegate int IFileOpenDialog_SetOptions(IntPtr thisPtr, FOS fos);
            public void SetOptions(FOS fos)
            {
                Marshal.ThrowExceptionForHR(Marshal.GetDelegateForFunctionPointer<IFileOpenDialog_SetOptions>(*((*(IntPtr**)Pointer) + 9))(Pointer, fos), new IntPtr(-1));
            }

            delegate int IFileOpenDialog_SetFolder(IntPtr thisPtr, IntPtr psi);
            public void SetFolder(ShellItem psi)
            {
                Marshal.ThrowExceptionForHR(Marshal.GetDelegateForFunctionPointer<IFileOpenDialog_SetFolder>(*((*(IntPtr**)Pointer) + 12))(Pointer, psi.Pointer), new IntPtr(-1));
            }

            delegate int IFileOpenDialog_GetResult(IntPtr thisPtr, out IntPtr ppsi);
            public ShellItem GetResult()
            {
                var result = new ShellItem();
                Marshal.ThrowExceptionForHR(Marshal.GetDelegateForFunctionPointer<IFileOpenDialog_GetResult>(*((*(IntPtr**)Pointer) + 20))(Pointer, out result.Pointer), new IntPtr(-1));
                return result;
            }
        }

        private unsafe class ShellItem : MiniComObject
        {
            delegate int IShellItem_GetDisplayName(IntPtr thisPtr, SIGDN sigdnName, [MarshalAs(UnmanagedType.LPWStr)] out string ppszName);
            public void GetDisplayName(SIGDN sigdnName, out string ppszName)
            {
                Marshal.ThrowExceptionForHR(Marshal.GetDelegateForFunctionPointer<IShellItem_GetDisplayName>(*((*(IntPtr**)Pointer) + 5))(Pointer, sigdnName, out ppszName), new IntPtr(-1));
            }
        }

#if false
        [ComImport]
        [Guid("42f85136-db7e-439c-85f1-e4075d135fc8")]
        [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        private interface IFileOpenDialog
        {
            [PreserveSig]
            uint Show([In] IntPtr parent); // IModalWindow
            void SetFileTypes();  // not fully defined
            void SetFileTypeIndex([In] uint iFileType);
            void GetFileTypeIndex(out uint piFileType);
            void Advise(); // not fully defined
            void Unadvise();
            void SetOptions([In] FOS fos);
            void GetOptions(out FOS pfos);
            void SetDefaultFolder(IShellItem psi);
            void SetFolder(IShellItem psi);
            void GetFolder(out IShellItem ppsi);
            void GetCurrentSelection(out IShellItem ppsi);
            void SetFileName([In, MarshalAs(UnmanagedType.LPWStr)] string pszName);
            void GetFileName([MarshalAs(UnmanagedType.LPWStr)] out string pszName);
            void SetTitle([In, MarshalAs(UnmanagedType.LPWStr)] string pszTitle);
            void SetOkButtonLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszText);
            void SetFileNameLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszLabel);
            void GetResult(out IShellItem ppsi);
            void AddPlace(IShellItem psi, int alignment);
            void SetDefaultExtension([In, MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension);
            void Close(int hr);
            void SetClientGuid();  // not fully defined
            void ClearClientData();
            void SetFilter([MarshalAs(UnmanagedType.Interface)] IntPtr pFilter);
            void GetResults([MarshalAs(UnmanagedType.Interface)] out IntPtr ppenum); // not fully defined
            void GetSelectedItems([MarshalAs(UnmanagedType.Interface)] out IntPtr ppsai); // not fully defined
        }

        [ComImport]
        [Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE")]
        [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        private interface IShellItem
        {
            void BindToHandler(); // not fully defined
            void GetParent(); // not fully defined
            void GetDisplayName([In] SIGDN sigdnName, [MarshalAs(UnmanagedType.LPWStr)] out string ppszName);
            void GetAttributes();  // not fully defined
            void Compare();  // not fully defined
        }
#endif

        private enum SIGDN : uint
        {
            SIGDN_DESKTOPABSOLUTEEDITING = 0x8004c000,
            SIGDN_DESKTOPABSOLUTEPARSING = 0x80028000,
            SIGDN_FILESYSPATH = 0x80058000,
            SIGDN_NORMALDISPLAY = 0,
            SIGDN_PARENTRELATIVE = 0x80080001,
            SIGDN_PARENTRELATIVEEDITING = 0x80031001,
            SIGDN_PARENTRELATIVEFORADDRESSBAR = 0x8007c001,
            SIGDN_PARENTRELATIVEPARSING = 0x80018001,
            SIGDN_URL = 0x80068000
        }

        [Flags]
        private enum FOS
        {
            FOS_ALLNONSTORAGEITEMS = 0x80,
            FOS_ALLOWMULTISELECT = 0x200,
            FOS_CREATEPROMPT = 0x2000,
            FOS_DEFAULTNOMINIMODE = 0x20000000,
            FOS_DONTADDTORECENT = 0x2000000,
            FOS_FILEMUSTEXIST = 0x1000,
            FOS_FORCEFILESYSTEM = 0x40,
            FOS_FORCESHOWHIDDEN = 0x10000000,
            FOS_HIDEMRUPLACES = 0x20000,
            FOS_HIDEPINNEDPLACES = 0x40000,
            FOS_NOCHANGEDIR = 8,
            FOS_NODEREFERENCELINKS = 0x100000,
            FOS_NOREADONLYRETURN = 0x8000,
            FOS_NOTESTFILECREATE = 0x10000,
            FOS_NOVALIDATE = 0x100,
            FOS_OVERWRITEPROMPT = 2,
            FOS_PATHMUSTEXIST = 0x800,
            FOS_PICKFOLDERS = 0x20,
            FOS_SHAREAWARE = 0x4000,
            FOS_STRICTFILETYPES = 4
        }
    }
}
CoffeeParser commented 5 years ago

Hi, I tested the provided sample in a netcore 3.0 console app and it worked only for opening the Dialog with the Show method, but failed at GetResult with an AccessViolationException. I might have fixed it by changing the pointer offset from +12 to +20... but then it crashes at GetDisplayName, which for me looks like is completly ignoring the given SIGDN sigdnName. I am not sure how the offset is calculated. to me it looks like first member is at base +3.

@jkotas Can you please provide a guide on how you calculated the pointer offsets? I tried to build SharpGenTools but the build fails during header parsing of ShObjIdl_core.h (...) and there is nowhere a working sample of SharpGenTools to be found.

jkotas commented 5 years ago

fixed it by changing the pointer offset from +12 to +20

That's the right fix. Thanks for fixing it.

GetDisplayName, which for me looks like is completely ignoring the given SIGDN sigdnName

The sigdnName argument was missing in the delegate signature. I have fixed it above (you can look at the comment history to see the delta). It should work now.

tried to build SharpGenTools but the build fails during header parsing of ShObjIdl_core.h

Maybe open an issue on this in SharpGenTools repo. cc @jkoritzinsky