Skip to content
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

Merged
merged 1 commit into from
Oct 12, 2022

Conversation

JeremyKuhne
Copy link
Member

@JeremyKuhne JeremyKuhne commented Oct 12, 2022

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

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.
@JeremyKuhne JeremyKuhne requested a review from a team as a code owner October 12, 2022 21:11
@ghost ghost assigned JeremyKuhne Oct 12, 2022
@elachlan
Copy link
Contributor

CC: @kant2002

@JeremyKuhne
Copy link
Member Author

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, IFileDialogEvents is the example here. The implementation is pretty simple- hopefully we can move more to the code generators. We'll continue to convert things to build confidence in our patterns.


namespace Windows.Win32.UI.Shell;

internal unsafe partial struct IFileDialogEvents : INativeGuid, IPopulateVTable<IFileDialogEvents.Vtbl>
Copy link
Member Author

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.

@@ -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;
Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member

@lonitra lonitra left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🥳

@JeremyKuhne
Copy link
Member Author

Thanks to @lonitra for sitting through a good chunk of this with me.

@JeremyKuhne
Copy link
Member Author

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. :)

@JeremyKuhne JeremyKuhne merged commit abbee61 into dotnet:main Oct 12, 2022
@ghost ghost added this to the 8.0 Preview1 milestone Oct 12, 2022
@JeremyKuhne JeremyKuhne deleted the filedialog branch October 12, 2022 22:26
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));
Copy link
Member

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.

Copy link
Member Author

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)
Copy link
Member

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.

Copy link
Member Author

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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check format is null?

Copy link
Member

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);
Copy link
Member

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?

Copy link
Member Author

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);
Copy link
Member

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.

Copy link
Member Author

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?

Copy link
Member

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)]
Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Contributor

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);
Copy link
Member

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.

Copy link
Member Author

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));
Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Contributor

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.

Copy link
Member

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".

result = RegisterDragDrop(hwnd.Handle, dropTargetPtr);
Marshal.Release(dropTargetPtr);
HRESULT result = RegisterDragDrop(hwnd.Handle, (nint)(void*)dropTargetPtr);
Marshal.Release((nint)(void*)dropTargetPtr);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Marshal.Release((nint)(void*)dropTargetPtr);

this leaks if RegisterDragDrop fails. Put in finally?

Copy link
Member Author

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).

/// <summary>
/// Returns the length of the native BSTR.
/// </summary>
public unsafe uint Length => Value is null ? 0 : *(((uint*)Value) - 1) / 2;
Copy link

@jacdavis jacdavis Oct 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/ 2;

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.

Copy link
Member Author

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?

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.

Copy link
Contributor

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.

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.

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.

Copy link
Member

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?

Copy link
Member

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.

Copy link
Contributor

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)));

Copy link
Member

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);
Copy link
Contributor

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.

Comment on lines +15 to +19
// 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);
Copy link
Member

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));
Copy link
Member

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".

v-elnovikova pushed a commit to v-elnovikova/winforms that referenced this pull request Oct 18, 2022
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.
@ghost ghost locked as resolved and limited conversation to collaborators Nov 12, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants