dotnet/corert

COM Interop Guidance

wjk opened this issue ยท 49 comments

wjk commented

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!

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

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

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 commented

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.

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.

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.

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

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.

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?

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.

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.

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

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

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

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

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.

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.

What information do you think would be helpful here?

The user provides the desired type in some cases, e.g. Marshal.GetTypedObjectForIUnknown that is getting lost now. But I agree that this won't fix the more fundamental problem.

How do you envision optimizing this scenario without knowing what the IUnknown provides

ICastable is the best tech we have invented for that.

Interfaces for which I should create RCW to fully cover

A large fraction of these seems to be for ActiveX hosting and the HTML Control hosting. These two are not very relevant anymore, and likely full of other problems. It may be interesting to get the list with these two excluded.

did a quick count through "find references" on ComImport (120 total):

  • 12 Interfaces for Shell Dialogs
  • 18 interfaces accessibility
  • 26 web browser
  • 5 drag'n'drop / clipboard
  • 9 other non-activex control interfaces (likely to be used in normal operation)

the rest (~50) is most likely ActiveX (but note that the web browser may need many of those as well)

@jkotas when CreateObject called, flags has value TrackerObject. Does that mean I should implement IReferenceTracker interface ?

@kant2002 That is typically what that means. However, I don't know why that would be occurring in your scenario. What version of .NET are you using?

@kant2002 Okay. I see what is going on here. Yes, we always pass that when a call is triggered through the built-in Marshal APIs. This can be ignored if you know that the scenario being executed isn't a WinRT based.

The reason we pass this is because the built-in system technically would add that interface to any object implicitly if WinRT was active so in order to adhere to the same semantics we pass the same thing to indicate it is possible WinRT is in play. WinForms clearly doesn't have that so you can ignore it.

The code in question - recently updated in dotnet/runtime#35681 - can be viewed here.

@AaronRobinsonMSFT I observe strange behaviour (in Debug build)
I copy ComWrappersImpl with IRawElementProviderSimple as CCW target instead of IDispatch.

I have following code for building ComInterfaceEntry

var comInterfaceEntryMemory = RuntimeHelpers.AllocateTypeAssociatedMemory(typeof(IRawElementProviderSimpleVtbl), sizeof(ComInterfaceEntry));
var entry = (ComInterfaceEntry*)comInterfaceEntryMemory.ToPointer();
entry->IID = typeof(IRawElementProviderSimple).GUID;
entry->Vtable = vtblRaw;

entry has non-null value. But when I attempt save to static variable it just do not updated.

// private static ComInterfaceEntry* wrapperEntry;
wrapperEntry = entry;

After these line wrapperEntry still null. I'm quite puzzled now.

Looks like everything working, just value does not updated in the Debuger. Not big deal then.

I implement ComputeVtables and decide to return just single interface IRawElementProviderSimple which is used by UiaRaiseAutomationPropertyChangedEvent.
Application start crashing somewhere in non-managed code related to UiaRaiseAutomationPropertyChangedEvent. I may be wrong, but transitions between [Native to Manager]` and vice versa does not helps. If somebody of you have idea how I can test my code in more simple manner I would be appreciating. So far I have following ideas.

  • Build CoreCLR and attempt to run using debug version.
  • Open WinDBG and try to debug there.
  • think about more options, since both mentioned earlier seems to be excessive work.

Basically I would like to jump into UIA code and look what is broken right now.

I would use WinDbg myself. If you share stacktrace of the crash or link to a branch with the code, we may be able to give you some more tips.

@kant2002 Along with @jkotas's suggestion, it would be useful to see your vtable layouts - just as a sanity check.

@kant2002 a gist would work perfectly.

@AaronRobinsonMSFT https://github.com/kant2002/CoreRTWinFormsTestBed/tree/master/WinFormsComInterop it is slightly more involved then gist.

  1. Startup project is WindowsFormsApp1
  2. Open DropDown in the top right part of screen
  3. This will trigger call to CreateObject 2 times (works ok seems to be)
  4. Then call to ComputeVtables after exit it fails.

Other parts of controls may behave more different and I do not test them.

@jkotas thanks for suggestion. I was trying to avoid that route, but seems to be no other way.

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.

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.

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

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.

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)

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.

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

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.

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.

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; }

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

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;
}
``

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.

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?

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.

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 ?