Skip to content
This repository has been archived by the owner on Nov 1, 2020. It is now read-only.

COM Interop Guidance #4219

Open
wjk opened this issue Jul 22, 2017 · 49 comments
Open

COM Interop Guidance #4219

wjk opened this issue Jul 22, 2017 · 49 comments
Labels

Comments

@wjk
Copy link
Contributor

wjk commented Jul 22, 2017

I am considering using CoreRT with my .NET Core compatible GUI framework. However, to fully implement the GUI feature support I require, I will need to do some COM interop. Having studied the CoreRT source code, I can tell that COM support is not currently implemented. To make this usable, here is what I would do:

  1. Redirect creation of ComImportAttribute-decorated types to CoCreateInstance. Wrap the return value of that P/Invoke into something like this.
  2. Redirect calls to methods of ComImportAttribute-decorated interfaces to code that takes the slot number of the method called, gets the corresponding vtable entry and performs a calli.

The problem is, though, this is not compatible in the slightest with the extensive but nonfunctional COM interop code already in the CoreRT repo. This COM interop code is only partially open-source (it references .NET Native functionality, as well as the MCG tool which is still proprietary), and is entirely WinRT-specific on top of that. Modifying the existing COM interop code to be compatible with desktop-style COM is currently far above my pay grade. Could anyone please give some pointers on how I or others might start working on implementing this? Thanks so much!

@jkotas jkotas added the Interop label Jul 23, 2017
@jkotas
Copy link
Member

jkotas commented Jul 23, 2017

cc @yizhang82 @tijoytom

@tijoytom-zz
Copy link
Contributor

@wjk Thanks for your interest , as you mentioned most of the COM specific code in System.Private.Interop is tailored to work with the internal MCG tool. But the good new is that we are working on making the MCG tool public , we don't have any time lines yet. With MCG tooling you should be able to do Desktop style COM interop. I will keep you posted on the progress and once it's out in public you should be able to contribute.
@yizhang82

@wjk
Copy link
Contributor Author

wjk commented Oct 18, 2018

@tijoytom @jkotas I had a thought on how we might go about implementing CoreCLR-style COM interop on CoreRT. Rather than try to bring up the MCG-specific functionality in System.Private.Interop, I would instead use a subclass of CallInterceptor to redirect calls to a COM interface or class to CoreCLR, which would then do the COM interop as it always does. This is similar to what System.Private.Jit does, but for COM calls instead of ECMA-based reflection. Does this sound like a plausible approach?

@jkotas
Copy link
Member

jkotas commented Oct 18, 2018

Does this sound like a plausible approach?

Hosting both CoreCLR and CoreRT in the same process sounds pretty non-trivial.

You may want to take a look at https://github.com/SharpGenTools/SharpGenTools . It is interop generator for COM that is very similar to MCG. I am not sure whether anybody tried it with CoreRT, but it should just work or it should be pretty easy to make it work. I believe that SharpGenTools are the easiest way to make COM work in CoreRT at this point.

@wjk
Copy link
Contributor Author

wjk commented Oct 18, 2018

SharpGenTools isn't an option for two reasons: One, I haven't found any good, real-world examples on how to marshal COM types using it. Two, Windows Forms/WPF don't use it, and I take dependencies on those in all my projects.

@jkotas
Copy link
Member

jkotas commented Oct 18, 2018

Agree - making SharpGenTools work for Windows Forms/WPF would need some work. Still, I think it is easier to make SharpGenTools work than to make both CoreCLR and CoreRT run in the same process to reuse COM interop.

@jjxtra
Copy link

jjxtra commented Jun 9, 2019

Unable to use corert until COM is built in, I manipulate Windows firewall and do not want to launch netsh process for each little rule change.

@kant2002
Copy link
Contributor

@jkotas @MichalStrehovsky
It is still not clear how COM can be implemented into CoreRT. SharpGenTools seems to be useful for CppCodeGen, but not for RiyJit codegeneration.

  1. As the @tijoytom mention MCG can be made public. Is this tool public already?
  2. What if some proxy can be automatically generated by ILC or some tooling? and using some plumbing to make it work as in original proposal. Even if it will be rough implementation, some enthusiasts can improve it after. But some directions should be clarified.
  3. Do marshalling needed in CoreRT between COM Proxy and "managed" code?
  4. How this task can be simplified? What's easier to implement IDispatch or IUnknown interfaces? If IDispatch will make Office work it will be big win. If IUnknown only easier, this still make many people happy. I'm not sure that this is valid distinction, but just want throw some questions how work can be split into manageable chunks.

@jkotas
Copy link
Member

jkotas commented Feb 18, 2020

We have abandoned the MCG tool. We do not have plans to open source the MCG tool anymore.

Yes, generating the interop marshaling code using build-time tooling is the way to solve this.
IL rewriting (e.g. https://github.com/Fody/Fody) can be used to wire it in without actually changing the code.

IDispatch brings additional complications. Starting with IUnknown makes sense.

@kant2002
Copy link
Contributor

Yes, generating the interop marshaling code using build-time tooling is the way to solve this.

Does this code can be part of CoreRT tooling? or it is assumed that any program to be run under CoreRT has to manually manage COM objects?

@weltkante
Copy link

IL rewriting can be used to wire it in without actually changing the code.

Does this kind of tooling preserve debugging (PDBs) and edit-and-continue support in VS? Last time I checked IL rewriting was not usable for working on large products because they degrade the development tools experience massively. If having COM interop support (which is pretty common for Windows applications) requires sacrificing development tools thats a no-go.

@MichalStrehovsky
Copy link
Member

SharpGenTools seems to be useful for CppCodeGen, but not for RiyJit codegeneration.

SharpGenTools take C++ header files that define the prototypes/layouts of COM classes and produce .NET code that can call the APIs in the headers. The generated .NET code can run on any runtime. CppCodegen doesn't have any advantage in this respect. The key thing is that the generated .NET code doesn't rely on runtime's built-in COM support.

Does this code can be part of CoreRT tooling?

It should be an external tool that runs before the CoreRT compiler - it's easier to test it that way - the generated code should still run on all .NET runtimes (including CoreCLR), but won't rely on the internal COM handling anymore. It's how the closed source MCG tool operates as well.

It would be beneficial for .NET in general - COM interop cannot be pregenerated by any of the .NET Core ahead of time technologies right now (neither CoreCLR nor Mono can do COM without doing a bunch of JITting). Having a tool would enable pregeneration on all runtimes (CoreCLR with ReadyToRun, Mono AOT, and CoreRT).

Does this kind of tooling preserve debugging (PDBs) and edit-and-continue support in VS?

Debugging info will typically be preserved by the rewriter. I don't think edit and continue is supported on COM interfaces so that limitation would stay in place.

@kant2002
Copy link
Contributor

kant2002 commented May 3, 2020

@jkotas I trying to understand what do you think needed for COM support in CoreRT. I see dotnet/runtime#1845 and other issues which you mention landed in .NET 5. I imagine that .NET 5 would be requirement to start playing with COM support in CoreRT.

Does this sample (dotnet/samples#2873) can be used for starting poking hole in the COM support? Seems to be this is for exposing .NET object as IDispatch, so not so valuable for short term tests.

If I take example how IExternalObject wrapped in dotnet/runtime#1845, and then attempt to create in similar style (even if it is not fully properly implemented in that example) can this be the path?

My first goal is to have basic controls working, and only function which is holding me for now, is

[DllImport(Libraries.Oleacc, ExactSpelling = true, CharSet = CharSet.Auto)]
public static extern int CreateStdAccessibleObject(HandleRef hWnd, int objID, ref Guid refiid, [In, Out, MarshalAs(UnmanagedType.Interface)] ref object pAcc);

so maybe I can implement very simple holder class for object marshalling?

I'm trying to limit amount of work in that area, so I can manage learning and implementation.

@jkotas
Copy link
Member

jkotas commented May 3, 2020

Here is how to start on this.

  1. Install latest .NET 5 SDK preview from https://github.com/dotnet/installer
  2. Create .NET 5 WinForms app
  3. Add the following to Program.cs and the Main method:
class WinFormsComWrappers : ComWrappers
{
    protected override unsafe ComInterfaceEntry* ComputeVtables(object obj, CreateComInterfaceFlags flags, out int count)
    {
        count = 0;
        return null;
    }

    protected override object CreateObject(IntPtr externalComObject, CreateObjectFlags flags)
    {
        return null;
    }

    protected override void ReleaseObjects(IEnumerable objects)
    {
    }
}

...
        static void Main()
        {
            // This will be renamed to RegisterForMarshalling soon
            new WinFormsComWrappers().RegisterAsGlobalInstance();
....
  1. Set breakpoints at ComputeVtables and CreateObject and run your WinForms app. You will see that this is now getting called from WinForms.

  2. Add implementation for ComputeVtables and CreateObject (the samples should help) so that you are providing the COM interop wrappers, without depending on the COM interop built-in into the runtime.


  1. Once you have that working, you can go back to CoreRT. Add ComWrappers type to CoreRT and hook it up to the COM marshallers (ie replace the throw PlatformNotSupportedException you are adding in Add simple marshaller which marshal only null COM interfaces #8128 with call to ComWrappers).

  2. Your WinForms app should work now!

@AaronRobinsonMSFT or me will happy to help with any problems you hit along the way. We have not really validated whether the step 5 is doable for something like WinForms, so there may be unexpected issues along the way.

@kant2002
Copy link
Contributor

kant2002 commented May 3, 2020

@jkotas @AaronRobinsonMSFT

  1. And here my first question. I copy IExternalObject from the original proposal, trim it to support just IUnknown interface. Then I return that instance from CreateObject. Application immediately stop with exception
Unable to cast object of type 'WindowsFormsApp1.IExternalObject' to type 'Accessibility.IAccessible'.'

I make dummy implementation which just throw. and then I immediately hit another issue.

Unable to cast object of type 'WindowsFormsApp1.IExternalObject' to type 'IEnumVariant'.'

That interface is internal to WinForms. See https://github.com/dotnet/winforms/blob/5d7ad6eb0eac45d01407d512bb4fef86d1ecd800/src/System.Windows.Forms.Primitives/src/Interop/OleAut32/Interop.IEnumVariant.cs#L15

If I just drag it to project, it does not helps too. So this is first bottleneck.

  1. Another feedback is following. When I implement WinFormsComWrappers.CreateObject how do I know what kind of proxy to create? What kind of interfaces proxy should it support? Does my proxy should implement all interfaces which appears inside application?

@jkotas
Copy link
Member

jkotas commented May 3, 2020

  1. You can create you own fake WinForms.dll to compile the COM wrappers against. E.g. the projects can be structures like this:
  • MainWinForms app
    • References real WinForms.dll
    • WinFormsComInterop.dll wrappers
      • References fake WinForms.dll with the private COM interfaces exposed. It really just needs what you need to compile WinFormsComInterop.dll wrappers.
      • Has IgnoresAccessChecksToAttribute for the real WinForms.dll so that the access checks against the real WinForms .dll do not fail at runtime

Ideally, Roslyn would have support for IgnoresAccessChecksToAttribute to make this easier. This is a workaround to use to compensate for not having it.

I do not think you want to take IExternalObject from the sample. It looks specific to what the sample was doing, not applicable here.

  1. I think we can try to start with a proxy that implements all interfaces to see how far it is going to go. You are right that there are potential scenarios that it may not handle well. @AaronRobinsonMSFT I believe that we have talked about having extra arguments for CreateObject that may make this better, but I am not sure where we landed on it.

@AaronRobinsonMSFT
Copy link
Member

@AaronRobinsonMSFT I believe that we have talked about having extra arguments for CreateObject that may make this better, but I am not sure where we landed on it.

I don't recall discussions about CreateObject() that would help with this scenario. There isn't any context here since the API is more than likely being called with an opaque IUnknown. I assume the originating call is from Marshal.GetObjectForIUnknown(), but there could be another entry vector. Either way, we just don't know what type is expected so I don't know what we could plumb through to the API.

On the other hand, there is the option to handle this at the original callsite. The caller may know what type is expected and instead of calling Marshal.GetObjectForIUnknown(), I would call ComWrappers.GetOrCreateObjectForComInstance() on a specialized version of ComWrappers for the specific case.

The globally registered version is going to have to QI for all types it can project. The set is finite but can be large if the desire is to have a truly universal ComWrappers for any COM interface. The first use case of this API is for WinRT scenarios and there the entire world is known. But even there the universal fallback does have an option to query for everything when nothing is known. In WinRT we can typically avoid the worst-case look up by leveraging the IInspectable interface which helps with determining the class and therefore the implemented interfaces to expose. COM doesn't have this so for the truly unknown scenario a vast QI inspection will need to occur.

@jkotas
Copy link
Member

jkotas commented May 4, 2020

On the other hand, there is the option to handle this at the original callsite.

Agree that is possible to address all of this by changing the calling code. I was hoping that RegisterForMarshalling can be capable enough to supply the COM interop wrappers for common scenarios like WinForms, without changing the calling code.

COM doesn't have this so for the truly unknown scenario a vast QI inspection will need to occur.

Or we need to look at bringing back ICastable in some form...

@AaronRobinsonMSFT
Copy link
Member

I was hoping that RegisterForMarshalling can be capable enough to supply the COM interop wrappers for common scenarios like WinForms, without changing the calling code.

What information do you think would be helpful here? It could be done for specific scenarios. In this case the globally registered version could know about each and every WinForms interface, but it doesn't really help with the look up. We need to know the calling context - not supplied at the callsite typically - and the finite set of interfaces to consider. The latter is possible if we know that WinForms is the target.

How do you envision optimizing this scenario without knowing what the IUnknown provides? Is there some subset of classes we know can come through? Perhaps having the WinForms codebase register pointers with some details about what it is when it enters the runtime? Basically implement IInspectable in some runtime-centric way?

Or we need to look at bringing back ICastable in some form...

I really need to look into that tech more. I wish I knew more about it.

@kant2002
Copy link
Contributor

kant2002 commented May 4, 2020

Just to give some stats
Interfaces for which I should create RCW to fully cover

  • 120 interfaces (Count of ComImport attributes
  • 90 specified as [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
  • 21 Dual interfaces [InterfaceType(ComInterfaceType.InterfaceIsDual)]

and I really hope this would not be needed
count of CCW

  • 65 controls exposed as COM objects.

My small experiment stuck after I implement 2 RCW for (IAssesible and IEnumVariant). Next step would be create CCW for Control class and I do not found time yet for that. Just give you idea about how much limited is WinForms.

@kant2002
Copy link
Contributor

kant2002 commented May 5, 2020

Native point of failure

Exception thrown at 0x00007FF9DF92C51E (ntdll.dll) in WindowsFormsApp1.exe: 0xC0000005: Access violation reading location 0xFFFFFFFFFFFFFFFF.

and just in case

image
Sorry for image, cannot get proper stack trace for mixed managed and native code. not sure is it helps or not.

@AaronRobinsonMSFT
Copy link
Member

That address looks like a sentinel value ((void*)-1) of some kind. I think @jkotas is right here. You may have to spin up WinDBG and set some break points around to see where the object instance is being used.

@jkotas
Copy link
Member

jkotas commented May 6, 2020

@AaronRobinsonMSFT @elinor-fung It looks like a bug here: https://github.com/dotnet/runtime/blob/master/src/coreclr/src/vm/interopconverter.cpp#L467

We QueryInterface for the requested interface, but then we throw it away and return IUnknown. The bogus pointer that we are crashing on came from IUnknown being used instead of the requested interface.

@jkotas
Copy link
Member

jkotas commented May 6, 2020

Opened dotnet/runtime#35883

@kant2002
Copy link
Contributor

kant2002 commented May 7, 2020

I was trying to run application under locally built .NET 5, but seems to be to no avail.
What I was try:

  • Build dotnet/runtime in Debug mode. Works fine. I use commit before split for ComWrappers for WinRT and COM (before this - dotnet/runtime@e3c7444)
  • I try to use latest Preview 5 SDK grabbed from dotnet/installer (Which also does not have 2 global COM wrappers).

Then I try

  1. to run application build with Preview 5 SDK using CoreRun.exe - fail due to some EEFileLoadException on WindowsFormsApp1.exe and VS debugger stop working.
  2. locally build dotnet/windowsdesktop and install from MSI dotnet/runtime and dotnet/windowsdesktop.
    But have followin warning
warning NU1603: WinFormsComInterop depends on runtime.win-x64.Microsoft.NETCore.App (>= 5.0.0-dev) but runtime.win-x64.Microsoft.NETCore.App 5.0.0-dev was not found. An approximate best match of runtime.win-x64.Microsoft.NETCore.App 5.0.0-preview.1.20112.8 was resolved.

and following error

error NU1101: Unable to find package Microsoft.AspNetCore.App.Runtime.win-x64

Error from missing locally built ASP.NET Core, but I do not expect it to be included when run WindowsForms and don't know how to out out.

I suspect that issue caused by the differences in ComWrappers between nightly SDK and local CoreCLR. If you have any suggestions how I can setup debugging, I would appreciate that.

@jkotas
Copy link
Member

jkotas commented May 7, 2020

The easiest way to use your locally built runtime with a WinForms app is to publish the app as self-contained and then copy over your locally build CoreCLR (ie copy over everything from artifacts\bin\coreclr\Windows_NT.x64.Debug)

@kant2002
Copy link
Contributor

kant2002 commented May 8, 2020

Okay, I manage to make some progress. I do not done with debugging, but seems to be I can get stuck in the middle, so will go for a walk.

I have local CoreCLR from dotnet/runtime#36054
Here the exception

************** Exception Text **************
System.Runtime.InteropServices.MarshalDirectiveException: Cannot marshal 'parameter #2': Invalid managed/unmanaged type combination (Interfaces must be paired with Interface).
   at Interop.UiaCore.UiaRaiseAutomationEvent(IRawElementProviderSimple provider, UIA id)
   at System.Windows.Forms.ComboBox.OnDropDownClosed(EventArgs e)
   at System.Windows.Forms.ComboBox.WmReflectCommand(Message& m)
   at System.Windows.Forms.ComboBox.WndProc(Message& m)
   at System.Windows.Forms.Control.ControlNativeWindow.WndProc(Message& m)
   at System.Windows.Forms.NativeWindow.Callback(IntPtr hWnd, WM msg, IntPtr wparam, IntPtr lparam)

Location where originate that exception.

https://github.com/dotnet/runtime/blob/ab7ef9b2924e4056680c1fd9372923a00bb91199/src/coreclr/src/vm/mlinfo.cpp#L2315-L2330

So far seems to be this is issue on CoreCLR side or I screw my application in major way.

@kant2002
Copy link
Contributor

kant2002 commented May 8, 2020

UIA type here was declared like this: public enum UIA : int in case this is interesting to know

@kant2002
Copy link
Contributor

kant2002 commented May 8, 2020

Okay. Walking on fresh air help slightly. The actual error happens during marshalling of _HostRawElementProvider.Invoke delegate. Which I declare as

public delegate int _HostRawElementProvider(
IntPtr thisPtr,
[MarshalAs(UnmanagedType.IUnknown)]out IRawElementProviderSimple i);

So not sure if this is me which plug [MarshalAs(UnmanagedType.IUnknown)] or this is some obscure case.

After I remove [MarshalAs(UnmanagedType.IUnknown)] dropdown starts working. Please clarify that this is proper fix, and not just workaround.
Now I can move to with CoreRT part.

@jkotas
Copy link
Member

jkotas commented May 8, 2020

Since you are doing the marshalling, it would be best to do all of it. Change the delegate to:

public delegate int _HostRawElementProvider(
IntPtr thisPtr,
IntPtr* pIRawElementProviderSimple);

And convert the IntPtr to IRawElementProviderSimple yourself.

@kant2002
Copy link
Contributor

kant2002 commented May 8, 2020

Would this pattern works?

public unsafe static int HostRawElementProviderInternal(IntPtr thisPtr, IntPtr* i)
{
	i = null;
	try
	{
		Interop.UiaCore.IRawElementProviderSimple inst = ComWrappers.ComInterfaceDispatch.GetInstance<Interop.UiaCore.IRawElementProviderSimple>((ComWrappers.ComInterfaceDispatch*)(void*)thisPtr);
		*i = WinFormsComWrappers.Instance.GetOrCreateComInterfaceForObject(inst.HostRawElementProvider, CreateComInterfaceFlags.None);
	}
	catch (Exception e)
	{
		return e.HResult;
	}
	return 0;
}

I want to return unmanaged COM interface from that method, which wraps call to

IRawElementProviderSimple HostRawElementProvider { get; }

@jkotas
Copy link
Member

jkotas commented May 8, 2020

You also need to do QueryInterface for the interface with the right GUID.

@kant2002
Copy link
Contributor

kant2002 commented May 8, 2020

So it would be something like that?

public static int HostRawElementProviderInternal(IntPtr thisPtr, IntPtr* i)
{
    i = null;
    try
    {
        var inst = ComInterfaceDispatch.GetInstance<IRawElementProviderSimple>((ComInterfaceDispatch*)thisPtr);
        IntPtr pUnk = Marshal.GetIUnknownForObject(inst.HostRawElementProvider);
        Guid targetInterface = typeof(IRawElementProviderSimple).GUID;
        int result = Marshal.QueryInterface(pUnk, ref targetInterface, out IntPtr ppv);
        if (result == 0)
        {
            *i = ppv;
        }

        return result;
    }
    catch (Exception e)
    {
        return e.HResult;
    }
    return 0; // S_OK;
}
``

@jkotas
Copy link
Member

jkotas commented May 8, 2020

You also need to release the pUnk once you are one with it.

Performance:

  • You should be able to pass *i into QueryInterface directly, like: Marshal.QueryInterface(pUnk, ref targetInterface, out *i)
  • It is better to call WinFormsComWrappers directly, avoids extra layers of indirection
  • It is better to have the GUID inline in your wrappers (e.g. in a static variable). Computing the GUID property from Type each time is not free.

@kant2002
Copy link
Contributor

kant2002 commented May 8, 2020

Seems to be I'm close

public static int HostRawElementProviderInternal(IntPtr thisPtr, IntPtr* i)
{
    i = null;
    try
    {
        var inst = ComInterfaceDispatch.GetInstance<IRawElementProviderSimple>((ComInterfaceDispatch*)thisPtr);
        IntPtr pUnk = WinFormsComWrappers.Instance.GetOrCreateComInterfaceForObject(inst.HostRawElementProvider, CreateComInterfaceFlags.None);
        Guid targetInterface = WinFormsComWrappers.IRawElementProviderSimple_GUID;
        int result = Marshal.QueryInterface(pUnk, ref targetInterface, out *i);
        Marshal.Release(pUnk);
        return result;
    }
    catch (Exception e)
    {
        return e.HResult;
    }
}

Next set of questions while I have your attention. There need to bring ComWrappers into CoreRT. My understanding that you and @MichalStrehovsky copy code from dotnet/runtime by moving existing commits and preserve authorship. Can you share some snippets how I can do that. and how I can select which commit to choose.

  1. Find commit1..commit2 range
  2. Create patch for subtree?
  3. Apply patch to another directory location?

What's way to move forward on this?

@jkotas
Copy link
Member

jkotas commented May 8, 2020

Can you share some snippets how I can do that

git format-patch -1 <commit hash>, manually edit paths in the patch, git am

I do not think it applies here. The implementation in CoreRT is going to be sufficiently different (it should be 99+% C#) that you can start from scratch, not worrying about preserving history.

@kant2002
Copy link
Contributor

kant2002 commented May 9, 2020

I plan to add tests, to guide implementation, but since they are targeting .NET Core 2.1 I have compilation errors that ComWrappers is not defined. Does that means that CoreRT should be moved to run only on .NET 5.0 ?

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests

8 participants