¿Cómo hacer que un objeto COM .NET tenga un subproceso de apartamento?

Los objetos .NET son de subproceso libre por defecto. Si se clasifica a otro subproceso a través de COM, siempre se calculan para sí mismos, independientemente de si el subproceso del creador fue STA o no, e independientemente de su valor de registro ThreadingModel . Sospecho que agregan el Marshaler de hilos libres (más detalles sobre el enhebrado COM se pueden encontrar aquí ).

Quiero hacer que mi objeto .NET COM use el proxy COM marshaller estándar cuando se compara con otro hilo. El problema:

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

Salida:

 {CurrentManagedThreadId = 11}
 {igual = verdadero}
 {CurrentManagedThreadId = 12}

Nota: uso CoMarshalInterThreadInterfaceInStream / CoGetInterfaceAndReleaseStream para ComObject de un hilo STA a otro. Quiero que se invoquen ambas llamadas Test() en el mismo hilo original, por ejemplo , 11 , como hubiera sido el caso con un objeto STA COM típico implementado en C ++.

Una posible solución es deshabilitar la interfaz IMarshal en el objeto COM de .NET:

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

Salida (como se desee):

 {CurrentManagedThreadId = 11}
 {igual = falso}
 {CurrentManagedThreadId = 11}

Esto funciona, pero se siente como un hack específico de implementación. ¿Hay alguna manera más decente para hacer esto, como algún atributo especial de interoperabilidad que podría haber pasado por alto? Tenga en cuenta que en la vida real ComObject se usa (y se calcula) mediante una aplicación heredada no administrada.

Puede heredar de StandardOleMarshalObject o ServicedComponent para ese efecto:

Los objetos administrados que están expuestos a COM se comportan como si hubieran agregado el contador de referencias sin hilos. En otras palabras, pueden ser llamados desde cualquier departamento COM de forma libre. Los únicos objetos gestionados que no muestran este comportamiento de subproceso libre son los objetos que se derivan de ServicedComponent o StandardOleMarshalObject .

La excelente respuesta de Paulo Madeira proporciona una excelente solución para cuando la clase administrada que se expone a COM puede derivarse de StandardOleMarshalObject .

Sin embargo, me hizo pensar en cómo tratar los casos en los que ya existe una clase base, como por ejemplo System.Windows.Forms.Control , que no tiene StandardOleMarshalObject en su cadena de herencia.

Resulta que es posible agregar el Marshaler COM estándar. Al igual que el CoCreateFreeThreadedMarshaler de Marshaler de Threaded Marshaler, hay una API para eso: CoGetStdMarshalEx . Así es como se puede hacer:

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