如何使.NET COM对象成为单元线程?

默认情况下,.NET对象是自由线程的。 如果通过COM封送到另一个线程,它们总是被封送到自己,无论创建者线程是否是STA,并且无论其ThreadingModel注册表值如何。 我怀疑,他们聚合了Free Threaded Marshaler (有关COM线程的更多细节可以在这里找到)。

我想让我的.NET COM对象在封送到另一个线程时使用标准的COM marshaller代理。 问题:

 using System; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using System.Windows.Threading; namespace ConsoleApplication { class Program { static void Main(string[] args) { var apt1 = new WpfApartment(); var apt2 = new WpfApartment(); apt1.Invoke(() => { var comObj = new ComObject(); comObj.Test(); IntPtr pStm; NativeMethods.CoMarshalInterThreadInterfaceInStream(NativeMethods.IID_IUnknown, comObj, out pStm); apt2.Invoke(() => { object unk; NativeMethods.CoGetInterfaceAndReleaseStream(pStm, NativeMethods.IID_IUnknown, out unk); Console.WriteLine(new { equal = Object.ReferenceEquals(comObj, unk) }); var marshaledComObj = (IComObject)unk; marshaledComObj.Test(); }); }); Console.ReadLine(); } } // ComObject [ComVisible(true)] [Guid("00020400-0000-0000-C000-000000000046")] // IID_IDispatch [InterfaceType(ComInterfaceType.InterfaceIsIDispatch)] public interface IComObject { void Test(); } [ComVisible(true)] [ClassInterface(ClassInterfaceType.None)] [ComDefaultInterface(typeof(IComObject))] public class ComObject : IComObject { // IComObject methods public void Test() { Console.WriteLine(new { Environment.CurrentManagedThreadId }); } } // WpfApartment - a WPF Dispatcher Thread internal class WpfApartment : IDisposable { Thread _thread; // the STA thread public System.Threading.Tasks.TaskScheduler TaskScheduler { get; private set; } public WpfApartment() { var tcs = new TaskCompletionSource(); // start the STA thread with WPF Dispatcher _thread = new Thread(_ => { NativeMethods.OleInitialize(IntPtr.Zero); try { // post a callback to get the TaskScheduler Dispatcher.CurrentDispatcher.InvokeAsync( () => tcs.SetResult(System.Threading.Tasks.TaskScheduler.FromCurrentSynchronizationContext()), DispatcherPriority.ApplicationIdle); // run the WPF Dispatcher message loop Dispatcher.Run(); } finally { NativeMethods.OleUninitialize(); } }); _thread.SetApartmentState(ApartmentState.STA); _thread.IsBackground = true; _thread.Start(); this.TaskScheduler = tcs.Task.Result; } // shutdown the STA thread public void Dispose() { if (_thread != null && _thread.IsAlive) { InvokeAsync(() => System.Windows.Threading.Dispatcher.ExitAllFrames()); _thread.Join(); _thread = null; } } // Task.Factory.StartNew wrappers public Task InvokeAsync(Action action) { return Task.Factory.StartNew(action, CancellationToken.None, TaskCreationOptions.None, this.TaskScheduler); } public void Invoke(Action action) { InvokeAsync(action).Wait(); } } public static class NativeMethods { public static readonly Guid IID_IUnknown = new Guid("00000000-0000-0000-C000-000000000046"); public static readonly Guid IID_IDispatch = new Guid("00020400-0000-0000-C000-000000000046"); [DllImport("ole32.dll", PreserveSig = false)] public static extern void CoMarshalInterThreadInterfaceInStream( [In, MarshalAs(UnmanagedType.LPStruct)] Guid riid, [MarshalAs(UnmanagedType.IUnknown)] object pUnk, out IntPtr ppStm); [DllImport("ole32.dll", PreserveSig = false)] public static extern void CoGetInterfaceAndReleaseStream( IntPtr pStm, [In, MarshalAs(UnmanagedType.LPStruct)] Guid riid, [MarshalAs(UnmanagedType.IUnknown)] out object ppv); [DllImport("ole32.dll", PreserveSig = false)] public static extern void OleInitialize(IntPtr pvReserved); [DllImport("ole32.dll", PreserveSig = true)] public static extern void OleUninitialize(); } } 

输出:

 {CurrentManagedThreadId = 11}
 {equal = True}
 {CurrentManagedThreadId = 12}

注意我使用CoMarshalInterThreadInterfaceInStream / CoGetInterfaceAndReleaseStream来将ComObject从一个STA线程编组到另一个STA线程。 我希望在相同的原始线程(例如11 Test()调用两个Test()调用 ,就像在C ++中实现的典型STA COM对象一样。

一种可能的解决方案是禁用.NET COM对象上的IMarshal接口:

 [ComVisible(true)] [ClassInterface(ClassInterfaceType.None)] [ComDefaultInterface(typeof(IComObject))] public class ComObject : IComObject, ICustomQueryInterface { // IComObject methods public void Test() { Console.WriteLine(new { Environment.CurrentManagedThreadId }); } public static readonly Guid IID_IMarshal = new Guid("00000003-0000-0000-C000-000000000046"); public CustomQueryInterfaceResult GetInterface(ref Guid iid, out IntPtr ppv) { ppv = IntPtr.Zero; if (iid == IID_IMarshal) { return CustomQueryInterfaceResult.Failed; } return CustomQueryInterfaceResult.NotHandled; } } 

输出(根据需要):

 {CurrentManagedThreadId = 11}
 {等于=假}
 {CurrentManagedThreadId = 11}

这有效,但感觉就像特定于实现的黑客。 是否有更好的方法来完成这项工作,就像我可能忽略了一些特殊的互操作属性? 请注意,在现实生活中,旧的非托管应用程序使用ComObject (并进行封送处理)。

您可以从StandardOleMarshalObjectServicedComponentinheritance该效果:

暴露给COM的托管对象的行为就像聚合了自由线程编组程序一样。 换句话说,它们可以以任意线程方式从任何COM公寓调用。 唯一没有出现此自由线程行为的托管对象是从ServicedComponent或StandardOleMarshalObject派生的对象。

Paulo Madeira的优秀答案提供了一个很好的解决方案,可以从StandardOleMarshalObject OleMarshalObject派生出公开给COM的托管类。

它让我思考,如何处理已经存在基类的情况,比如System.Windows.Forms.Control ,它的inheritance链中没有StandardOleMarshalObject

事实certificate,可以聚合标准COM Marshaler。 类似于Free Threaded Marshaler的CoCreateFreeThreadedMarshaler ,它有一个API: CoGetStdMarshalEx 。 以下是它的完成方式:

 [ComVisible(true)] [ClassInterface(ClassInterfaceType.None)] [ComDefaultInterface(typeof(IComObject))] public class ComObject : IComObject, ICustomQueryInterface { IntPtr _unkMarshal; public ComObject() { NativeMethods.CoGetStdMarshalEx(this, NativeMethods.SMEXF_SERVER, out _unkMarshal); } ~ComObject() { if (_unkMarshal != IntPtr.Zero) { Marshal.Release(_unkMarshal); _unkMarshal = IntPtr.Zero; } } // IComObject methods public void Test() { Console.WriteLine(new { Environment.CurrentManagedThreadId }); } // ICustomQueryInterface public CustomQueryInterfaceResult GetInterface(ref Guid iid, out IntPtr ppv) { ppv = IntPtr.Zero; if (iid == NativeMethods.IID_IMarshal) { if (Marshal.QueryInterface(_unkMarshal, ref NativeMethods.IID_IMarshal, out ppv) != 0) return CustomQueryInterfaceResult.Failed; return CustomQueryInterfaceResult.Handled; } return CustomQueryInterfaceResult.NotHandled; } static class NativeMethods { public static Guid IID_IMarshal = new Guid("00000003-0000-0000-C000-000000000046"); public const UInt32 SMEXF_SERVER = 1; [DllImport("ole32.dll", PreserveSig = false)] public static extern void CoGetStdMarshalEx( [MarshalAs(UnmanagedType.IUnknown)] object pUnkOuter, UInt32 smexflags, out IntPtr ppUnkInner); } }