Skip to content

[API Proposal]: Support getting IUnknown for ComWrappers RCWs #79679

Closed

Description

@JeremyKuhne

Background and motivation

For COM interop we expect to be able to cast RCWs to arbitrary [ComImport] interfaces. We use this and Type.IsComObject to implement TypeDescriptor functionality.

We can reimplement to support both legacy COM Interop and ComWrapppers if we have the ability to get IUnknown* for any ComWrappers generated RCWs.

Without this support we cannot enable trimming for WinForms without breaking critical features such as drag/drop/cut/paste and accessibility. This also prevents providing COM ComWrappers support with TypeDescriptor (which WinForms owns).

API Proposal

namespace System.Runtime.InteropServices;

public abstract class ComWrappers
{
+    public static bool TryGetComInstance(object obj, out void* unknown);
}

API Usage

public static bool TryCreateComTypeDescriptor(object obj, out ComTypeDescriptor? descriptor)
{
    void* unknown;
    if (obj.GetType().IsCOMObject)
    {
        unknown = (void*)Marshal.GetIUnknownForObject(obj);
    }
    else
    {
        ComWrappers.TryGetComInstance(obj, out unknown);
    }

    return unknown is not null ? new ComTypeDescriptor(unknown) : null;
}

Alternative Designs

One alternative (or addition) is to change all possible Marshal APIs that provide interface pointers to support ComWrappers RCWs:

namespace System.Runtime.InteropServices;

// Existing interface pointer methods
public static class Marshal
{
    public static IntPtr GetComInterfaceForObject(object! o, Type! T);
    public static IntPtr GetComInterfaceForObject(object! o, Type! T, CustomQueryInterfaceMode mode);
    public static IntPtr GetComInterfaceForObject<T, TInterface>([DisallowNull] T o);
    public static IntPtr GetComInterfaceForObjectInContext(object o, Type t);
    public static IntPtr GetIDispatchForObject(object! o);
    public static IntPtr GetIDispatchForObjectInContext(object o);
    public static IntPtr GetIUnknownForObject(object! o);
    public static IntPtr GetIUnknownForObjectInContext(object o);
}

I don't know how plausible this is, but it would make writing consuming code much easier.

Risks

No response

Activity

  1. added
    api-suggestionEarly API idea and discussion, it is NOT ready for implementation
    on Dec 14, 2022
  2. ghost added
    untriagedNew issue has not been triaged by the area owner
    on Dec 14, 2022
  3. JeremyKuhne commented on Dec 14, 2022

    @JeremyKuhne
    MemberAuthor
  4. AaronRobinsonMSFT commented on Dec 14, 2022

    @AaronRobinsonMSFT
    Member

    This is similar to my comment in #79674. This would throw if there wasn't a globally registered ComWrappers instance. Which if there is one and it is known, this isn't an issue because one can call ComWrappers.GetOrCreateObjectForComInstance().

  5. JeremyKuhne commented on Dec 14, 2022

    @JeremyKuhne
    MemberAuthor

    Which if there is one and it is known, this isn't an issue because one can call ComWrappers.GetOrCreateObjectForComInstance().

    How does this help? ComWrappers RCWs implement specific interfaces, not arbitrary [ComImport] interfaces with matching guids. We'd presumably have to publish all interfaces we use as "interchange types". We only have a few defined interfaces in ComTypes and I don't believe IUnknown or IDispatch are defined anywhere. How would we publish these interfaces? With HRESULTs explicitly exposed or not? With "marshalling" or not?

    For ActiveX Controls there are dozens of interfaces involved- and we allow providing your own instance of the class. In order for this to work (in the same way as before) we'd have to publish every interface we get off the root object and they would have to implement every single one of them. In this case I can't imagine that anyone is doing anything other than working with a wrapped IUnknown.

    Beyond that there is a composition problem. We don't register our ComWrappers and I don't know that we ever could as users would not be able to register their own. How do we expose an RCW that either we created ourselves (with our non-registered ComWrappers) or the user provided for us (through ComWrappers or existing RCWs)?

    Because we can't register our own ComWrappers I don't see how we can implement anything around COM interfaces without directly using COM pointers.

  6. AaronRobinsonMSFT commented on Dec 15, 2022

    @AaronRobinsonMSFT
    Member

    Spoke to @JeremyKuhne offline. I get this scenario now. The implementation rather trivial since we already query for this information when constructing a WeakReference. The proposed alternative options aren't tenable due to semantic problems. The current semantics all guarantee they return a non-null value because the runtime would generate an CCW on the fly. The expectation here is the API will only return one if it exists, hence the "try" semantics.

  7. added
    api-ready-for-reviewAPI is ready for review, it is NOT ready for implementation
    and removed
    api-suggestionEarly API idea and discussion, it is NOT ready for implementation
    untriagedNew issue has not been triaged by the area owner
    on Dec 15, 2022
  8. added this to the 8.0.0 milestone on Dec 15, 2022
  9. added
    blockingMarks issues that we want to fast track in order to unblock other important work
    on Dec 15, 2022
  10. AaronRobinsonMSFT commented on Jan 6, 2023

    @AaronRobinsonMSFT
    Member

    @JeremyKuhne Based on a private implementation here is what I would propose for the API along with details on use.

    /// <summary>
    /// Given a managed object, determine if it is a ComWrappers created
    /// managed wrapper and if so, return the wrapped unmanaged pointer.
    /// </summary>
    /// <param name="wrapperMaybe">A managed wrapper</param>
    /// <param name="externalComObject">An unmanaged COM object</param>
    /// <returns>True if the wrapper was resolved to an external COM object, otherwise false.</returns>
    /// <remarks>
    /// If a COM object is returned, the caller is expected to call Release() on the object.
    /// This can be done through an API like Marshal.Release(). Since this API is required to interact
    /// directly with the external COM object, QueryInterface(), it is important for the caller to
    /// understand the COM object may have apartment affinity and therefore if the current thread is not
    /// in the correct apartment or the COM object is not a proxy this call may fail.
    /// </remarks>
    public static unsafe bool TryGetComInstance(object wrapperMaybe, out void* externalComObject);
  11. JeremyKuhne commented on Jan 6, 2023

    @JeremyKuhne
    MemberAuthor

    @AaronRobinsonMSFT that looks good to me.

  12. ghost added
    in-prThere is an active PR which will close this issue when it is merged
    on Jan 9, 2023
  13. terrajobst commented on Jan 10, 2023

    @terrajobst
    Contributor

    Video

    Looks good as proposed.

    namespace System.Runtime.InteropServices;
    
    public partial class ComWrappers
    {
        public static bool TryGetComInstance(object? obj, out void* unknown);
    }
  14. added
    api-approvedAPI was approved in API review, it can be implemented
    and removed
    api-ready-for-reviewAPI is ready for review, it is NOT ready for implementation
    on Jan 10, 2023
  15. ghost removed
    in-prThere is an active PR which will close this issue when it is merged
    on Jan 20, 2023
  16. ghost locked as resolved and limited conversation to collaborators on Feb 19, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    api-approvedAPI was approved in API review, it can be implementedarea-System.Runtime.InteropServicesblockingMarks issues that we want to fast track in order to unblock other important work

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions