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

Use ComWrappers for Clipboard #7087

Closed
wants to merge 9 commits into from

Conversation

kant2002
Copy link
Contributor

@kant2002 kant2002 commented Apr 26, 2022

Create missing RCW and CCW
Testing happens using existing test suite which is quite good.

Microsoft Reviewers: Open in CodeFlow

@kant2002 kant2002 requested a review from a team as a code owner April 26, 2022 15:09
@ghost ghost assigned kant2002 Apr 26, 2022
@kant2002
Copy link
Contributor Author

@RussKie can I ask you start looking into this masterpiece.

@RussKie
Copy link
Member

RussKie commented Apr 29, 2022 via email

@RussKie RussKie self-requested a review May 3, 2022 00:08
@RussKie RussKie added the waiting-review This item is waiting on review by one or more members of team label May 3, 2022
Comment on lines 162 to 166
if (dataObject is IDataObject ido && !Marshal.IsComObject(dataObject))
{
return ido;
}

Copy link
Member

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?

Copy link
Contributor Author

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.


internal IntPtr Instance => _wrappedInstance;

public void Dispose()
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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

@RussKie RussKie removed the waiting-review This item is waiting on review by one or more members of team label May 6, 2022
@RussKie
Copy link
Member

RussKie commented May 6, 2022

Looks good. I've run some local manual tests too.

@AaronRobinsonMSFT @JeremyKuhne I'd appreciate it if you could review as well.

@RussKie RussKie added 📭 waiting-author-feedback The team requires more information from the author waiting-review This item is waiting on review by one or more members of team labels May 6, 2022
@ghost ghost removed the 📭 waiting-author-feedback The team requires more information from the author label May 6, 2022
@@ -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);
Copy link
Member

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.

Copy link
Member

Choose a reason for hiding this comment

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

As in like this?

Suggested change
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);
}

Copy link
Contributor Author

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?

Copy link
Member

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.

Copy link
Contributor Author

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?

Copy link
Member

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

Copy link
Contributor Author

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?

Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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.


internal partial class Interop
{
internal unsafe partial class WinFormsComWrappers
Copy link
Member

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?

Copy link
Member

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.

Copy link
Contributor Author

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

Copy link
Member

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.

Copy link
Member

@AaronRobinsonMSFT AaronRobinsonMSFT May 9, 2022

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.

Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Contributor Author

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.

Copy link
Member

@RussKie RussKie left a comment

Choose a reason for hiding this comment

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

Few questions.

(_wrappedInstance, formatPtr, &mediumRaw).ThrowIfFailed();
medium = new()
{
pUnkForRelease = mediumRaw.pUnkForRelease == IntPtr.Zero ? null : Marshal.GetObjectForIUnknown(mediumRaw.pUnkForRelease),
Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Member

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

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

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?

Copy link
Contributor Author

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.

Comment on lines 24 to 28
~EnumFORMATETCWrapper()
{
this.DisposeInternal();
}

private void DisposeInternal()
{
Marshal.Release(_wrappedInstance);
_wrappedInstance = IntPtr.Zero;
}
Copy link
Member

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?

Copy link
Contributor Author

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.

kant2002 and others added 4 commits May 16, 2022 11:59
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>
@kant2002 kant2002 force-pushed the kant/comwrappers-clipboard branch from f7b52b9 to 0c812d2 Compare May 16, 2022 06:00

public int QueryGetData(ref FORMATETC format)
{
using var dataObject = Unwrap();
Copy link
Member

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

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

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
        }

Copy link
Contributor Author

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?

@JeremyKuhne
Copy link
Member

@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 [ComImport] interfaces on the types. I haven't had the time to deep-dive into mapping this out, but I suspect we may have to have AppContext switches in some places. I'll capture this in more detail as I get our tracking issues into shape.

@JeremyKuhne JeremyKuhne added 📭 waiting-author-feedback The team requires more information from the author and removed waiting-review This item is waiting on review by one or more members of team labels Jul 5, 2023
@kant2002
Copy link
Contributor Author

kant2002 commented Jul 5, 2023

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

@ghost ghost removed the 📭 waiting-author-feedback The team requires more information from the author label Jul 5, 2023
@JeremyKuhne JeremyKuhne closed this Jul 5, 2023
@ghost ghost locked as resolved and limited conversation to collaborators Aug 4, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants