-
Notifications
You must be signed in to change notification settings - Fork 980
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Convert file dialog COM usage to CsWin32 #7933
Conversation
Directly uses COM pointers when accessing the native dialog. This also rewrites the CCW for the dialog options to use the CsWin32 generated interface and moves the statics into the CsWin32 COM struct (which is partial). Adds better helpers to initialize vtables and find the CCW for a given object. Removes our DECIMAL and BSTR definitions. Adds an enum helper for setting/clearing flags based on a boolean value.
CC: @kant2002 |
FYI: @AArnott, @AaronRobinsonMSFT, @jkoritzinsky, @jacdavis, @elinor-fung, @vitek-karas, @LakshanF, @tannergooding We're getting enough in place from CsWin32 to start basing our ComWrappers on it. Putting our statics in the partial structs generated by CsWin32, |
|
||
namespace Windows.Win32.UI.Shell; | ||
|
||
internal unsafe partial struct IFileDialogEvents : INativeGuid, IPopulateVTable<IFileDialogEvents.Vtbl> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is our ComWrappers "CCW" implementation. Pretty boilerplate stuff now.
src/System.Windows.Forms.Primitives/src/Windows/Win32/System/Com/CLSID.cs
Show resolved
Hide resolved
@@ -93,8 +94,16 @@ private static HRESULT Clone(Com.IEnumFORMATETC* @this, Com.IEnumFORMATETC** ppe | |||
{ | |||
IEnumFORMATETC instance = ComInterfaceDispatch.GetInstance<IEnumFORMATETC>((ComInterfaceDispatch*)@this); | |||
instance.Clone(out var cloned); | |||
*ppenum = (Com.IEnumFORMATETC*)WinFormsComWrappers.Instance.GetComPointer(cloned, IID.IEnumFORMATETC); | |||
return HRESULT.S_OK; | |||
*ppenum = null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we check if ppenum
is null before this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can add that when we convert this one over.
...m.Windows.Forms/src/System/Windows/Forms/ComponentModel/COM2Interop/COM2TypeInfoProcessor.cs
Show resolved
Hide resolved
src/System.Windows.Forms/src/System/Windows/Forms/FolderBrowserDialog.cs
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🥳
Thanks to @lonitra for sitting through a good chunk of this with me. |
If there are any other comments please leave them. I'll be keeping an eye on this. Getting it in as we have further changes backed behind it. :) |
vtblRaw[9] = (IntPtr)(delegate* unmanaged<IntPtr, FORMATETC*, ADVF, IntPtr, int*, HRESULT>)&DAdvise; | ||
vtblRaw[10] = (IntPtr)(delegate* unmanaged<IntPtr, int, HRESULT>)&DUnadvise; | ||
vtblRaw[11] = (IntPtr)(delegate* unmanaged<IntPtr, IntPtr*, HRESULT>)&EnumDAdvise; | ||
IDataObject.Vtbl* vtblRaw = (IDataObject.Vtbl*)RuntimeHelpers.AllocateTypeAssociatedMemory(typeof(IDataObject.Vtbl), sizeof(IDataObject.Vtbl)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This represents a substantial increase in unused metadata if the Vtbl
is simply being projected into managed by yet another type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is only done for projecting to native from managed. In that case we need to keep all of our callbacks so I'm not sure what value it adds in this case?
{ | ||
var instance = ComInterfaceDispatch.GetInstance<IDataObject>((ComInterfaceDispatch*)thisPtr); | ||
if (pMedium is null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to check for format
here too? It is dereferenced below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, when we convert this one we're going to make sure we validate all of these out dereferences.
{ | ||
var instance = ComInterfaceDispatch.GetInstance<IDataObject>((ComInterfaceDispatch*)thisPtr); | ||
return (HRESULT)instance.QueryGetData(ref *format); | ||
var instance = ComInterfaceDispatch.GetInstance<ComTypes.IDataObject>((ComInterfaceDispatch*)@this); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Check format
is null
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll stop commenting.
@@ -93,8 +94,16 @@ private static HRESULT Clone(Com.IEnumFORMATETC* @this, Com.IEnumFORMATETC** ppe | |||
{ | |||
IEnumFORMATETC instance = ComInterfaceDispatch.GetInstance<IEnumFORMATETC>((ComInterfaceDispatch*)@this); | |||
instance.Clone(out var cloned); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this return an HRESULT
or is this using the built-in exception throwing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We'll be porting this one over to the new format shortly.
{ | ||
try | ||
{ | ||
TInterface? @object = ComInterfaceDispatch.GetInstance<TInterface>((ComInterfaceDispatch*)@this); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will never return null
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does it return when something has been deref'ed to 0?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Calling a function on a zero ref CCW is a bug and undefined.
// https://github.com/microsoft/win32metadata/issues/1300 | ||
internal static partial class PInvoke | ||
{ | ||
[DllImport("COMDLG32.dll", EntryPoint = "GetOpenFileNameW", ExactSpelling = true, PreserveSig = false)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the more common pattern would be naming the enclosing class COMDLG32
and then specifying the file name using nameof(COMDLG32)
. I'd also consider using LibraryImport
for these definitions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't use LibraryImport
in many cases as we reference types from CsWin32. This one is a temporary hack as the Windows metadata doesn't project a platform agnostic type (the issue is linked above it). It will be moved to CsWin32 once the issue is resolved.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You shouldn't need to declare this method. If you just declare the struct (which is the arch-specific API), then CsWin32 should be willing to generate the method that depends on it.
internal static partial class PInvoke | ||
{ | ||
[DllImport("COMDLG32.dll", EntryPoint = "GetOpenFileNameW", ExactSpelling = true, PreserveSig = false)] | ||
public static unsafe extern BOOL GetOpenFileName([In][Out] OPENFILENAME* param0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The [In][Out]
have no functional value here. If they are being used to express indent, that seems fine, but I would avoid them and rely on a comment instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Noted. This is another temporary because of the Win32 metadata issue linked.
|
||
[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })] | ||
public static HRESULT OnFileOk(IFileDialogEvents* @this, IFileDialog* pfd) | ||
=> ComWrappers.UnwrapAndInvoke<IFileDialogEvents, Interface>(@this, o => o.OnFileOk(pfd)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure this is a good idea. A delegate allocation is occurring for each invocation. Based on the pointer usage and all the other unsafe C# going on here, this is out of place.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought there were improvements made here regarding caching these? In any case, the hope is to move this to source generation (in CsWin32) so we might be able to entirely avoid it. I knew this wasn't as efficient but decided that it is more important (for our usages) that we're easily made consistent and the code is easy to maintain.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The compiler cannot cache a delegate that captures local variables. This one captures pfd
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A bit of related info can be found here: https://devblogs.microsoft.com/dotnet/performance_improvements_in_net_7/ in the paragraph that starts with "Let’s start with IDE0200".
src/System.Windows.Forms/src/System/Windows/Forms/FolderBrowserDialog.cs
Show resolved
Hide resolved
src/System.Windows.Forms/src/System/Windows/Forms/FolderBrowserDialog.cs
Show resolved
Hide resolved
src/System.Windows.Forms/src/System/Windows/Forms/FolderBrowserDialog.cs
Show resolved
Hide resolved
...m.Windows.Forms/src/System/Windows/Forms/ComponentModel/COM2Interop/COM2TypeInfoProcessor.cs
Show resolved
Hide resolved
...m.Windows.Forms/src/System/Windows/Forms/ComponentModel/COM2Interop/COM2TypeInfoProcessor.cs
Show resolved
Hide resolved
...m.Windows.Forms/src/System/Windows/Forms/ComponentModel/COM2Interop/COM2TypeInfoProcessor.cs
Show resolved
Hide resolved
result = RegisterDragDrop(hwnd.Handle, dropTargetPtr); | ||
Marshal.Release(dropTargetPtr); | ||
HRESULT result = RegisterDragDrop(hwnd.Handle, (nint)(void*)dropTargetPtr); | ||
Marshal.Release((nint)(void*)dropTargetPtr); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one should only fail if there was something truly catastrophic (AV or OOM).
...System.Windows.Forms.Primitives/src/System/Windows/Forms/FileDialogCustomPlacesCollection.cs
Show resolved
Hide resolved
/// <summary> | ||
/// Returns the length of the native BSTR. | ||
/// </summary> | ||
public unsafe uint Length => Value is null ? 0 : *(((uint*)Value) - 1) / 2; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This assumption is only correct if the BSTR is utf16. It is ALWAYS utf16 on 32 bit windows, but if we were ever to do a cross plat version of winforms this would be a bug (most posix os's use utf8 for the in-memory representation). In native code SysStringLen abstracts out this dependency and is a fast invocation. Would it be better to pinvoke that with a BSTR*? Note that in other cross plat implementations of com code bases, we've relied on PAL conversion of platform dependencies and run into lots of issues with dependencies like this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The runtime does this directly as well: https://github.com/dotnet/runtime/blob/c6c8bb8b73ff4f7a686ca5a350fbb0093b3b708b/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/Marshal.cs#L1263
The generated code also does it, but we can get that changed. If we do we'd probably want to get CsWin32 to add the [SuppressGCTransition]
attribute to make it actually speedy.
Fwiw, the odds of doing an x-plat WinForms are astronomically low so we take Win32 as a given.
@AaronRobinsonMSFT, any thoughts on this from the Marshal
perspective? Would exposing a GetBSTRLength()
method on Marshal
be plausible?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair enough, but I mention it because this has been a very costly issue for me in the past. Using the length prefix ties to what is essentially an implementation detail. In native code, invoking SysStringLen is only a few instructions longer where the abstraction exists and avoids the problem. For native, avoiding the pinvoke may be reason enough to accept the risk here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using the length prefix ties to what is essentially an implementation detail
BSTR's length prefix is not only documented, but hard-coded in several runtimes. It couldn't possibly change, nor would I feel bad about depending on it given it's documented. Why would you hesitate? Or did I misunderstand what the impl detail was that you were talking about, @jacdavis ?
The pointer arithmetic is complex, but if it works, fine. I wonder though why the hesitation to just reuse Marshal.PtrToStringBSTR
?
Also, what is [SuppressGCTransition]
and why would its use be important here? Our own method would just be calling the Marshal
method, which itself is managed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is only documented as being the length of the string in bytes. If the internal representation of a character changes as it can do on posix based systems that use UTF8 (1-4 byte variable length chars), then the conversion from bytes to characters changes becomes much more complex. I believe we finally standardized on utf16 for bstrs in the PAL even cross plat, but this means we constantly have to convert to and from utf8 when calling apis. I have no issue with leaving this as-is if we think we'll never hit this. However, I will say that I never expected to be porting the debugger cross plat either where I have hit this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All are reasons why I believe we standardized on utf16 for bstrs in the PAL. However, it becomes very problematic as soon as you start passing to os apis etc... I'm fine with leaving it as-is. The pinvoke cost is probably too high to justify the change here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jacdavis what does PAL stand for?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@AaronRobinsonMSFT, any thoughts on this from the Marshal perspective? Would exposing a GetBSTRLength() method on Marshal be plausible?
We could do it. I am dubious it is worth the effort though. Feel free to submit an api-proposal. It is likely this has already come up before though. I think the current approach is likely fine. My only minor quibble would be to use sizeof(char)
.
@jacdavis what does PAL stand for?
"Platform Abstraction Layer" or "Platform Adaptation Layer". The .NET runtime has one to help paper over platform differences. Unfortunately ours is sort of an Win32 emulation layer instead of a nicely abstracted PAL.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FWIW CsWin32 will use sizeof(char)
in its version of this, which is:
unsafe int Length => this.Value is null ? 0 : checked((int)(*(((uint*)this.Value) - 1) / sizeof(char)));
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another data point that might be helpful is that in .NET Core/.NET 5+, the following marshalling scenarios have been marked obsolete: UnmanagedType.AnsiBStr
and UnmanagedType.TBStr
. We've not removed them yet, but since they've been marked this way for so long with perhaps one or two questions, I think we are close to a point where they could be removed entirely. For the new LibraryImport
source generator, they aren't supported in any form.
@@ -92,8 +91,8 @@ public unsafe void ITypeInfo_GetDllEntry_Invoke_Success() | |||
ushort wOrdinal = ushort.MaxValue; | |||
hr = typeInfo.GetDllEntry((DispatchID)6, INVOKEKIND.FUNC, &dllName, &name, &wOrdinal); | |||
Assert.Equal(HRESULT.TYPE_E_BADMODULEKIND, hr); | |||
Assert.Empty(dllName.String.ToString()); | |||
Assert.Empty(name.String.ToString()); | |||
Assert.True(dllName.Length == 0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
tip: using Assert.Equal(0, dllName.Length)
is better than using True ==
because when it fails, the exception will tell you more about what the actual value was. In fact if you use Assert.Equal(string.Empty, dllName)
here, you'd get the actual string in a failure case, which would be optimal.
// C0B4E2F3-BA21-4773-8DBA-335EC946EB8B | ||
internal static Guid FileSaveDialog = new(0xC0B4E2F3, 0xBA21, 0x4773, 0x8D, 0xBA, 0x33, 0x5E, 0xC9, 0x46, 0xEB, 0x8B); | ||
|
||
// DC1C5A9C-E88A-4DDE-A5A1-60F82A20AEF7 | ||
internal static Guid FileOpenDialog = new(0xDC1C5A9C, 0xE88A, 0x4DDE, 0xA5, 0xA1, 0x60, 0xF8, 0x2A, 0x20, 0xAE, 0xF7); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[nit] Please keep these lexographically sorted.
|
||
[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvStdcall) })] | ||
public static HRESULT OnFileOk(IFileDialogEvents* @this, IFileDialog* pfd) | ||
=> ComWrappers.UnwrapAndInvoke<IFileDialogEvents, Interface>(@this, o => o.OnFileOk(pfd)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A bit of related info can be found here: https://devblogs.microsoft.com/dotnet/performance_improvements_in_net_7/ in the paragraph that starts with "Let’s start with IDE0200".
Directly uses COM pointers when accessing the native dialog. This also rewrites the CCW for the dialog options to use the CsWin32 generated interface and moves the statics into the CsWin32 COM struct (which is partial). Adds better helpers to initialize vtables and find the CCW for a given object. Removes our DECIMAL and BSTR definitions. Adds an enum helper for setting/clearing flags based on a boolean value.
Directly uses COM pointers when accessing the native dialog.
This also rewrites the CCW for the dialog options to use the CsWin32 generated interface and moves the statics into the CsWin32 COM struct (which is partial).
Adds better helpers to initialize vtables and find the CCW for a given object.
Removes our DECIMAL and BSTR definitions.
Adds an enum helper for setting/clearing flags based on a boolean value.
Microsoft Reviewers: Open in CodeFlow