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

Marshal.ReleaseComObject does not account for ComWrappers #54317

Closed
kant2002 opened this issue Jun 17, 2021 · 24 comments
Closed

Marshal.ReleaseComObject does not account for ComWrappers #54317

kant2002 opened this issue Jun 17, 2021 · 24 comments

Comments

@kant2002
Copy link
Contributor

I try to make WPF work using ComWrappers only, and hit followin road block. Currently RCW in WPF released using Marshal.ReleaseComObject. This method does not take into account that some code is using ComWrappers

I think in spirit of #50500 that behaviour should change.
/cc @AaronRobinsonMSFT

@dotnet-issue-labeler dotnet-issue-labeler bot added area-Interop-coreclr untriaged New issue has not been triaged by the area owner labels Jun 17, 2021
@AaronRobinsonMSFT
Copy link
Member

@kant2002 This is by design. The appropriate API to call instead is Marshal.Release or some other variant to do the decrement. The Marshal.ReleaseComObject API is targeted for manipulating the built-in COM interop wrappers not user defined wrappers (that is, ComWrappers). What is the intent of calling this API on a ComWrappers instance?

@AaronRobinsonMSFT AaronRobinsonMSFT added needs more info and removed untriaged New issue has not been triaged by the area owner labels Jun 17, 2021
@AaronRobinsonMSFT
Copy link
Member

/cc @jkoritzinsky @elinor-fung

@AaronRobinsonMSFT AaronRobinsonMSFT added this to the Future milestone Jun 17, 2021
@kant2002
Copy link
Contributor Author

I'm hit that when experimenting how far can I go with ComWrappers in WPF (to see NativeAOT ability potentially)

WPF retreive object using DllImport, and it is created using ComWrappers, then it is assumed that this is __ComObject and using Marshal.ReleaseComObject to release instance. I assume this is would be common pattern for existing COM codebases.

This is quick summary on the libraries which use this functions. I count only files, because it's easier to do in the browser.

https://github.com/dotnet/winforms/search?q=ReleaseComObject 6 non test files
https://github.com/dotnet/wpf/search?q=ReleaseComObject 30 no test files

https://github.com/dotnet/runtime/search?q=ReleaseComObject
System.DirectoryServices - 1 file
System.Data.OleDb - 7 file
System.Speech - 9 file
System.Drawing.Common - 1 file (Icon class)
System.DirectoryServices.AccountManagement - 1 file
System.Management - 4 file

Other files.
libraries/Common/src/System/Runtime/InteropServices/ComEventsSink.cs - 1
tools/Common/TypeSystem/Ecma/SymbolReader/UnmanagedPdbSymbolReader.cs - 1

WinForms almost not impacted if not using ActiveX. It works perfectly fine for me in NativeAOT in relatively complex app.
WPF not even starts with ComWrappers attached.
DirectoryServices, Management, Speech based on previous discussion are less supported. I would cry only about Management because admin work, and Speech because it's fun. I always can contribute if would be desperate.

@AaronRobinsonMSFT
Copy link
Member

AaronRobinsonMSFT commented Jun 17, 2021

@kant2002 Rather than updating this API I would recommend converting entirely to ComWrappers and the returned wrapper could implement IDisposable. From within that I would release as desired. I don't think updating that API to handle ComWrappers wrappers is appropriate because there isn't an obvious way to convert a user defined wrapper into something we could release – it is an implementation detail of a ComWrappers instance.

@jkotas
Copy link
Member

jkotas commented Jun 18, 2021

I agree that the ultimate goal should be to get to 100% ComWrappers, but I think it would make sense to update APIs like Marshal.ReleaseComObject to handle ComWrappers to make migration to ComWrappers easier.

Supporting ComWrappers in APIs like Marshal.ReleaseComObject costs very little and it is in the same spirit as ComWrappers.RegisterForMarshalling API that was also introduce to ease the migration.

@AaronRobinsonMSFT
Copy link
Member

Supporting ComWrappers in APIs like Marshal.ReleaseComObject costs very little and it is in the same spirit as ComWrappers.RegisterForMarshalling API that was also introduce to ease the migration.

I don't think it costs very little. It is a complicated scenario with a fair bit of nuance.

  1. This could only be made to work for a globally registered ComWrappers instance for the marshalling scenario. Which means we would need to detect that one was registered and the object passed in was created with it.
  2. We would then call the ReleaseObjects() on the ComWrappers instance and pass in a single object – which is far more expensive than the current API implementation.

The above would mean (1) when and how this works is complicated and (2) the performance relative to the existing behavior (i.e., built-in) is much more and that defeats the purpose of ComWrappers.

I would argue we should separate them at this point and rely on either an IDisposable contract or perhaps something else. I do not see the value in overloading this API when it has a fair bit of complexity that users are bound to get wrong and shouldn't really be using anyways. The number of times we've had to diagnose early release RCWs is too many to count for me and introducing that outside of the ComWrappers API seems going down a road that has already be tread with many lessons learned.

@AaronRobinsonMSFT
Copy link
Member

AaronRobinsonMSFT commented Jun 18, 2021

@kant2002 I took another look at this and really am concerned about the complexity here. We would need to update both Marshal.ReleaseComObject and Marshal.FinalReleaseComObject – the behavior would basically be identical. Aside from the relative cost of implementing these APIs we also must rely on a user's ComWrappers implementation. The contract for these APIs is rather specific and users have come to rely on an actual release – we can guarantee that with the built-in interop. In this new world we would rely on the ComWrappers implementation to provide a ReleaseObjects() that disables the wrappers safely and doesn't just return.

An argument could be made this is an advanced scenario so that is the cost. I'm sympathetic to this perspective actually. However, the arguments I am hearing are around successfully transitioning from the built-in system. This means we want to guarantee semantics to some degree and I don't see how we can do that. I will say that as we move forward with ComWrappers a convention for how wrappers are written should be defined. I personally like IDisposable but there are a lot of options here.

I would recommend doing the following prior to these calls:

if (Marshal.IsComObject(obj))
    Marshal.ReleaseComObject(obj);

@jkoritzinsky
Copy link
Member

@AaronRobinsonMSFT System.__ComObject is internal to CoreLib, so that’s not possible.

@AaronRobinsonMSFT
Copy link
Member

AaronRobinsonMSFT commented Jun 18, 2021

@AaronRobinsonMSFT System.__ComObject is internal to CoreLib, so that’s not possible.

Doh! All those years of seeing that in the VS debugger and I never realized it was marked internal. Hmmm. Now I think we need to talk about this...

@jkoritzinsky I've updated the example so it will work now but I do agree we should perhaps talk more about the needs here.

@kant2002
Copy link
Contributor Author

I'm fine with either IDisposable, finalizer or other custom interface, but I would like that somehow this function work almost like it is expected in that function.
Maybe behind some switch. Without that, you have to go for ComWrappers and rewrite all P/Invoke calls, and wait for libraries to catch up.
Essentially this prevents global ComWrappers to be useful for existing applications in my opinion.

I personally think about this interface

interface IComReference
{
   int Release();
}

@jkotas
Copy link
Member

jkotas commented Jun 18, 2021

Essentially this prevents global ComWrappers to be useful for existing applications in my opinion.

The global ComWrappers instance is designed to only able to cover the simplest COM interop cases transparently (simple interface marshaling). It won't cover existing libraries and application that use COM interop in more advanced ways. It is very likely that applications or libraries that are using COM interop heavily will require changes to work well with ComWrappers (either global or local instance).

if (Marshal.IsComObject(obj))
Marshal.ReleaseComObject(obj);

This will prevent the app from crashing with ComWrappers, but it won't make it work well. The code was presumably explicitly releasing the object for a reason, and not just because of it can. I think it would need to be:

if (Marshal.IsComObject(obj))
    Marshal.ReleaseComObject(obj);
else
    (obj as IDisposable)?.Dispose();

and the respective ComWrappers would have to implement IDisposable.

@kant2002
Copy link
Contributor Author

I think it would need to be:
That's seems pragmatic. I see how it can be easy applied across codebases.

If nothing would be added/changed, I can provide separate PR's for each library in this repo. Any objections?

@jkotas
Copy link
Member

jkotas commented Jun 18, 2021

The challenge with changes like this is testing - how do we know that it actually works?

I think it would preferable to work towards the 100% ComWrappers solution: Pick one of these libraries, figure out how to make wean it off of the built-in COM support by default, and make it happen. Rinse & repeat.

@kant2002
Copy link
Contributor Author

Let's see. I would pick System.Drawing.Common as easiest target with only 2 interfaces. IStream and IPicture.
How I create RCW for IPicture, and CCW for IStream? Should I create them manually, or should I use codegen of some form? This one seems easy to create manually, but what about other libraries?

@AaronRobinsonMSFT
Copy link
Member

@kant2002 I believe that @eerhardt is also looking into System.Drawing.Common. You two might want to sync.

@eerhardt
Copy link
Member

Yes, I started working on moving System.Drawing.Common over to COM wrappers in order to make it trim compatible. Here are the current ILLink warnings in Drawing:

<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
<argument>ILLink</argument>
<argument>IL2050</argument>
<property name="Scope">member</property>
<property name="Target">M:System.Drawing.Icon.OleCreatePictureIndirect(System.Drawing.Icon.PICTDESC,System.Guid@,System.Boolean)</property>
</attribute>
<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
<argument>ILLink</argument>
<argument>IL2050</argument>
<property name="Scope">member</property>
<property name="Target">M:System.Drawing.SafeNativeMethods.Gdip.GdipCreateBitmapFromStream(Interop.Ole32.IStream,System.IntPtr@)</property>
</attribute>
<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
<argument>ILLink</argument>
<argument>IL2050</argument>
<property name="Scope">member</property>
<property name="Target">M:System.Drawing.SafeNativeMethods.Gdip.GdipCreateBitmapFromStreamICM(Interop.Ole32.IStream,System.IntPtr@)</property>
</attribute>
<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
<argument>ILLink</argument>
<argument>IL2050</argument>
<property name="Scope">member</property>
<property name="Target">M:System.Drawing.SafeNativeMethods.Gdip.GdipCreateMetafileFromStream(Interop.Ole32.IStream,System.IntPtr@)</property>
</attribute>
<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
<argument>ILLink</argument>
<argument>IL2050</argument>
<property name="Scope">member</property>
<property name="Target">M:System.Drawing.SafeNativeMethods.Gdip.GdipGetMetafileHeaderFromStream(Interop.Ole32.IStream,System.IntPtr)</property>
</attribute>
<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
<argument>ILLink</argument>
<argument>IL2050</argument>
<property name="Scope">member</property>
<property name="Target">M:System.Drawing.SafeNativeMethods.Gdip.GdipLoadImageFromStream(Interop.Ole32.IStream,System.IntPtr@)</property>
</attribute>
<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
<argument>ILLink</argument>
<argument>IL2050</argument>
<property name="Scope">member</property>
<property name="Target">M:System.Drawing.SafeNativeMethods.Gdip.GdipLoadImageFromStreamICM(Interop.Ole32.IStream,System.IntPtr@)</property>
</attribute>
<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
<argument>ILLink</argument>
<argument>IL2050</argument>
<property name="Scope">member</property>
<property name="Target">M:System.Drawing.SafeNativeMethods.Gdip.GdipRecordMetafileStream(Interop.Ole32.IStream,System.IntPtr,System.Drawing.Imaging.EmfType,System.Drawing.RectangleF@,System.Drawing.Imaging.MetafileFrameUnit,System.String,System.IntPtr@)</property>
</attribute>
<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
<argument>ILLink</argument>
<argument>IL2050</argument>
<property name="Scope">member</property>
<property name="Target">M:System.Drawing.SafeNativeMethods.Gdip.GdipRecordMetafileStream(Interop.Ole32.IStream,System.IntPtr,System.Drawing.Imaging.EmfType,System.IntPtr,System.Drawing.Imaging.MetafileFrameUnit,System.String,System.IntPtr@)</property>
</attribute>
<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
<argument>ILLink</argument>
<argument>IL2050</argument>
<property name="Scope">member</property>
<property name="Target">M:System.Drawing.SafeNativeMethods.Gdip.GdipRecordMetafileStreamI(Interop.Ole32.IStream,System.IntPtr,System.Drawing.Imaging.EmfType,System.Drawing.Rectangle@,System.Drawing.Imaging.MetafileFrameUnit,System.String,System.IntPtr@)</property>
</attribute>
<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
<argument>ILLink</argument>
<argument>IL2050</argument>
<property name="Scope">member</property>
<property name="Target">M:System.Drawing.SafeNativeMethods.Gdip.GdipSaveImageToStream(System.Runtime.InteropServices.HandleRef,Interop.Ole32.IStream,System.Guid@,System.Runtime.InteropServices.HandleRef)</property>
</attribute>

They are all:

IL2050: Trim analysis: Correctness of COM interop cannot be guaranteed
P/invoke method 'method' declares a parameter with COM marshalling. Correctness of COM interop cannot be guaranteed after trimming. Interfaces and interface members might be removed.

Moving them to use COM wrappers will resolve these ILLink warnings.

@eerhardt
Copy link
Member

@kant2002 - if you want, you could pick up one of the other libraries?

https://github.com/dotnet/runtime/search?q=IL2050

  • System.Management
  • System.DirectoryServices
  • System.Data.Odbc
  • System.Data.OleDb

@kant2002
Copy link
Contributor Author

@eerhardt For now I would like to shadow you, to see how ComWrappers should be implemented. Can you tag me if you have PR or draft? Is there general consensus how it should be done? I can start poking into System.Data.Odbc or System.Management based on what should be done. Want to limit scope to easiest target first.

@eerhardt
Copy link
Member

For sure! I'll tag you when I have a PR ready. Probably sometime next week.

@eerhardt
Copy link
Member

FYI - the first round of converting System.Drawing.Common to ComWrappers: #54636.

@kant2002
Copy link
Contributor Author

Does System.Speech not appear in search for IL2050 because it uses ComImport attribute for object creation and no built-in COM marshalling is used, just built-in object creation?

kant2002 added a commit to kant2002/runtime that referenced this issue Jun 28, 2021
Remove unused functions which trigger IL2050
As per discussed in dotnet#54317
@eerhardt
Copy link
Member

eerhardt commented Jun 28, 2021

Does System.Speech not appear in search for IL2050 because it uses ComImport attribute for object creation and no built-in COM marshalling is used, just built-in object creation?

No, we choose not to analyze System.Speech for trimmability yet, because it isn't a very used assembly (it only got supported on .NET Core in the 5.0 time frame). Here is the list of assemblies we ignore for trimmability (for now):

<!-- The following is the list of all the OOBs we will ignore for now -->
<_OOBsToIgnore Include="System.CodeDom" />
<_OOBsToIgnore Include="System.ComponentModel.Composition" />
<_OOBsToIgnore Include="System.ComponentModel.Composition.Registration" />
<_OOBsToIgnore Include="System.Composition.AttributedModel" />
<_OOBsToIgnore Include="System.Composition.Convention" />
<_OOBsToIgnore Include="System.Composition.Hosting" />
<_OOBsToIgnore Include="System.Composition.Runtime" />
<_OOBsToIgnore Include="System.Composition.TypedParts" />
<_OOBsToIgnore Include="System.Configuration.ConfigurationManager" />
<_OOBsToIgnore Include="System.Speech" />

Some of that reasoning is listed here: #52272 (comment)

marek-safar pushed a commit that referenced this issue Jul 13, 2021
* Remove IL2050 in System.Management
Remove unused functions which trigger IL2050
As per discussed in #54317

* Fix location of variables
@AaronRobinsonMSFT AaronRobinsonMSFT modified the milestones: Future, 7.0.0 Aug 11, 2021
@AaronRobinsonMSFT
Copy link
Member

I think the follow up here should be documentation regarding contracts and how implementers should consume ComWrappers.

@AaronRobinsonMSFT
Copy link
Member

@ghost ghost locked as resolved and limited conversation to collaborators Jul 17, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

5 participants