-
Notifications
You must be signed in to change notification settings - Fork 998
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
Use ComWrappers for Clipboard #7087
Conversation
@RussKie can I ask you start looking into this masterpiece. |
I’ll put aside some time next week.
|
src/System.Windows.Forms.Primitives/src/Interop/Ole32/Interop.OleGetClipboard.cs
Outdated
Show resolved
Hide resolved
src/System.Windows.Forms.Primitives/src/Interop/Ole32/Interop.OleGetClipboard.cs
Outdated
Show resolved
Hide resolved
src/System.Windows.Forms.Primitives/src/Interop/Ole32/Interop.OleSetClipboard.cs
Outdated
Show resolved
Hide resolved
if (dataObject is IDataObject ido && !Marshal.IsComObject(dataObject)) | ||
{ | ||
return ido; | ||
} | ||
|
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.
Could you please explain this removal?
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 effectively dead code after ComVisible
removal. My current plan is provide for DataObject
CCW's which supports 2 interfaces IComDataObject
and IWinFormsDataObject
which resurrect code in some form.
dataObject
is always DataObjectWrapper
here which implements only IComDataObject
for now. And previously we have _ComObject
which react just to IComDataObject
, that why @filipnavara create issue in first place.
src/System.Windows.Forms.Primitives/src/Interop/WinFormsComWrappers.cs
Outdated
Show resolved
Hide resolved
src/System.Windows.Forms.Primitives/src/Interop/WinFormsComWrappers.cs
Outdated
Show resolved
Hide resolved
src/System.Windows.Forms.Primitives/src/Interop/WinFormsComWrappers.cs
Outdated
Show resolved
Hide resolved
|
||
internal IntPtr Instance => _wrappedInstance; | ||
|
||
public void Dispose() |
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.
How is this called? I didn't see any direct callers.
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 add finalizer after this comment. Also I leave this method for consistency with other wrappers.
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 don't think the Dispose
pattern shouldn't be used in this case. Consistency isn't the priority here, but rather correctness. See the note under https://docs.microsoft.com/dotnet/standard/native-interop/tutorial-comwrappers#step-4--handle-native-object-wrapper-lifetime-details.
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 attribute my ignorance to the fact that there now documentation when I start 😉 , but thank for keeping me responsible.
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.
And crap. I add finalizer in different place. In the DataObject....
src/System.Windows.Forms.Primitives/src/Interop/WinFormsComWrappers.DataObjectWrapper.cs
Show resolved
Hide resolved
Looks good. I've run some local manual tests too. @AaronRobinsonMSFT @JeremyKuhne I'd appreciate it if you could review as well. |
@@ -10,6 +10,16 @@ internal static partial class Interop | |||
internal static partial class Ole32 | |||
{ | |||
[DllImport(Libraries.Ole32, ExactSpelling = true)] | |||
public static extern HRESULT OleSetClipboard(IDataObject? pDataObj); | |||
private static extern HRESULT OleSetClipboard(IntPtr pDataObj); |
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.
Entirely up to the WinForms owners, @RussKie and @JeremyKuhne, but this could be declared within the method body below as a static local function.
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.
As in like this?
private static extern HRESULT OleSetClipboard(IntPtr pDataObj); | |
public static HRESULT OleSetClipboard(IDataObject? pDataObj) | |
{ | |
if (pDataObj is null) | |
{ | |
return OleSetClipboard(IntPtr.Zero); | |
} | |
return OleSetClipboard(WinFormsComWrappers.Instance.GetComPointer(pDataObj, IID.IDataObject)); | |
[DllImport(Libraries.Ole32, ExactSpelling = true)] | |
static extern HRESULT OleSetClipboard(IntPtr pDataObj); | |
} |
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 that's what Aaron means. Do you want me apply this change?
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.
As in like this?
Yep.
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.
How will it play if we switch to LibraryImport
later on? Will source gen produce code for 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.
I guess we'll cross that bridge when we get to it :)
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.
Isn’t #7172 practically done. And it will land definitely faster then this one. Am I miss something?
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.
Judging by the @JeremyKuhne's comments there are perf implications that may need to be addressed first.
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.
My understanding on this, that this comment applied only if custom marshaller would be used with HandleRef
or IHandle
, but with IntPtr
these concerns are not applicable. So that PR looks like it can be merged as is right now.
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.
How will it play if we switch to LibraryImport later on? Will source gen produce code for this?
If we're just switching to LibraryImport
it won't necessitate changing anything here.
src/System.Windows.Forms.Primitives/src/Interop/WinFormsComWrappers.IEnumFORMATETCVtbl.cs
Outdated
Show resolved
Hide resolved
|
||
internal partial class Interop | ||
{ | ||
internal unsafe partial class WinFormsComWrappers |
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 don't see anything here around apartment marshalling. If I recall, the entire clipboard stack is required to be on the STA. This doesn't enforce that or even bother to check, which means we can get into data corruption or random crashes. This type only ever going to be used on the STA and if so, how is that is that enforced?
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.
In the clipboard we check:
if (Application.OleRequired() != ApartmentState.STA) | |
{ | |
throw new ThreadStateException(SR.ThreadMustBeSTA); | |
} |
Perhaps we should be doing the same here as well.
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 all for multilayered checks, but I wonder, how .NET already handling that? Also My understnading was that I should use IAgileReference
like it is here https://github.com/RussKie/ClipboardRedux
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 .NET objects are free-threaded so COM servers implemented in .NET can run on any apartment. For built-in RCWs, this is incredibly complex and handled in the RCW implementation - see UnmarshalIUnknownForCurrContext
for an example of the marshalling across apartments.
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.
Yes, IAgileReference
is the best way to do this on Windows 8+. However, it doesn't work on Windows 7, which means we need to have a fallback if Winforms is still required to support that OS. On pre-Windows 8 using the Global Interface Table (GIT) is required.
I believe this is mentioned in the ClipboardRedux example above.
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.
Independent of the answer on Windows 7 support, can we just implement new Windows Forms features the new best way and say that we'll document which features are not supported on Windows 7 going forward if that proves necessary?
Put another way, if we decide to support Windows 7 again -- which I really hope not -- it's not really intended as a first-class citizen.
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 and @RussKie can we consider current implementation as "limited support" for Win7? I'm not sure that Clipboard
can be considered new feature
. Or maybe for Win7 I can revert back to built-in Com Interop, but how all this playout with LibraryImport work. Just throwing ideas.
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.
My take on this is that we add OS checks to use the new functionality on Win8+ and use the original implementations for Win7.
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.
Seems to be Did not matter. I have to somehow drag to that point and I need old implementation hanging around.
Still looking for approximate list of test cases for clipboard.
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.
Few questions.
src/System.Windows.Forms.Primitives/src/Interop/WinFormsComWrappers.DataObjectWrapper.cs
Show resolved
Hide resolved
src/System.Windows.Forms.Primitives/src/Interop/WinFormsComWrappers.DataObjectWrapper.cs
Show resolved
Hide resolved
src/System.Windows.Forms.Primitives/src/Interop/WinFormsComWrappers.DataObjectWrapper.cs
Show resolved
Hide resolved
(_wrappedInstance, formatPtr, &mediumRaw).ThrowIfFailed(); | ||
medium = new() | ||
{ | ||
pUnkForRelease = mediumRaw.pUnkForRelease == IntPtr.Zero ? null : Marshal.GetObjectForIUnknown(mediumRaw.pUnkForRelease), |
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.
Could you please help me understand why we need to marshal again, and can't just copy mediumRaw.pUnkForRelease
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.
what if somebody replace value, stored in the mediumRaw.pUnkForRelease
. We do not know if somebody change value of pUnkForRelease
. That's highly unlikely, but who knows.
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.
Since there was a question here consider a clarifying comment
|
||
internal partial class Interop | ||
{ | ||
internal unsafe partial class WinFormsComWrappers |
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.
In the clipboard we check:
if (Application.OleRequired() != ApartmentState.STA) | |
{ | |
throw new ThreadStateException(SR.ThreadMustBeSTA); | |
} |
Perhaps we should be doing the same here as well.
IntPtr enumAdvisePtr; | ||
var result = ((delegate* unmanaged<IntPtr, IntPtr*, HRESULT>)(*(*(void***)_wrappedInstance + 11))) | ||
(_wrappedInstance, &enumAdvisePtr); | ||
enumAdvise = result.Succeeded() ? null : (IEnumSTATDATA)Marshal.GetObjectForIUnknown(enumAdvisePtr); |
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 @kant2002 could you please help me understand the best practices for Marshal.GetObjectForIUnknown
? According to the docs it increments the ref count. Does it mean we need to release enumAdvisePtr
before we exit?
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.
Release is responsibility of RCW finalizer.
~EnumFORMATETCWrapper() | ||
{ | ||
this.DisposeInternal(); | ||
} | ||
|
||
private void DisposeInternal() | ||
{ | ||
Marshal.Release(_wrappedInstance); | ||
_wrappedInstance = IntPtr.Zero; | ||
} |
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.
Any reason we can't inline these together?
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.
Technically we can. That was needed because I thought that having Dispose method is valuable, but right now there no reason.
src/System.Windows.Forms.Primitives/src/Interop/WinFormsComWrappers.IEnumFORMATETCVtbl.cs
Outdated
Show resolved
Hide resolved
Create missing RCW and CCW Testing happens using existing test suite which is quite good.
Co-authored-by: Igor Velikorossov <RussKie@users.noreply.github.com>
Add finalizer
f7b52b9
to
0c812d2
Compare
|
||
public int QueryGetData(ref FORMATETC format) | ||
{ | ||
using var dataObject = Unwrap(); |
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.
If this is how apartment support for IDataObject
is being provided, then I'm not convinced we should permit it to be IDisposable
. That pattern has non-trivial performance impact and is likely to be noticeable in large applications. We should instead defer to the built-in lifetime management provided by the runtime.
{ | ||
internal unsafe partial class WinFormsComWrappers | ||
{ | ||
internal sealed class DataObjectWrapper : IDataObject, IDisposable |
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 would expect the IAgileReference
to be an implementation detail of DataObjectWrapper
.
@@ -138,13 +138,13 @@ public static void SetDataObject(object data, bool copy, int retryTimes, int ret | |||
/// </remarks> | |||
private static IDataObject? GetDataObject(int retryTimes, int retryDelay) | |||
{ | |||
IComDataObject? dataObject = 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.
Since W7 support is still in scope, we need to retain the original implementation, and conditionally invoke the new implementation. E.g., using OsVersion.IsWindows8OrGreater
or doing something like:
private static IDataObject? GetDataObject(int retryTimes, int retryDelay)
{
// new code
}
private static IDataObject? GetDataObjectWin7(int retryTimes, int retryDelay)
{
// original code
}
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! does agility an issue for OleSetClipboard
in same way as for OleGetClipboard
?
@kant2002 unfortunately resource constraints have pushed this out of .NET 8. We hope to tackle the rest of the COM interop for .NET 9. I appreciate all of the effort here and if you're interested, I'll keep you looped in. As the COM code has changed significantly, I think it's better to close this one and use it as a reference in a new PR. Note: One particular difficulty here is that we need to make sure we've fully understood and documented changes in castability. Without built-in COM interop you don't get the "magic" casting of pointers back to their original objects or out to arbitrary implemented |
@JeremyKuhne I agree with you on this one. And since I did not push this in time we should rethink how it can be done in .NET 9. I still interested in this area, but cannot allocate cycles for pushing this forward. |
Create missing RCW and CCW
Testing happens using existing test suite which is quite good.
Microsoft Reviewers: Open in CodeFlow