Open wjk opened 7 years ago
cc @yizhang82 @tijoytom
@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
@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.
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.
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 https://github.com/dotnet/runtime/issues/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 (https://github.com/dotnet/samples/pull/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 https://github.com/dotnet/runtime/issues/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.
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();
....
Set breakpoints at ComputeVtables and CreateObject and run your WinForms app. You will see that this is now getting called from WinForms.
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.
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).
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
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.
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? 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.
@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
ComImport
attributesand I really hope this would not be needed count of CCW
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):
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 https://github.com/dotnet/runtime/pull/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.
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.
CreateObject
2 times (works ok seems to be)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
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:
dotnet/runtime
in Debug mode. Works fine. I use commit before split for ComWrappers for WinRT and COM (before this - dotnet/runtime@e3c744461d05b891d3ed3b4bcb4821663f20aa90)dotnet/installer
(Which also does not have 2 global COM wrappers).Then I try
EEFileLoadException
on WindowsFormsApp1.exe
and VS debugger stop working.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 https://github.com/dotnet/runtime/pull/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.
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:
*i
into QueryInterface directly, like: Marshal.QueryInterface(pUnk, ref targetInterface, out *i)
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.
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 ?
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:
ComImportAttribute
-decorated types toCoCreateInstance
. Wrap the return value of that P/Invoke into something like this.ComImportAttribute
-decorated interfaces to code that takes the slot number of the method called, gets the corresponding vtable entry and performs acalli
.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!