Как сделать .NET COM-объект квартирной резьбой?

Объекты .NET по умолчанию свободны. Если они маршируются в другой поток через COM, они всегда становятся маршалированными для себя, независимо от того, был ли поток создателя STA или нет, и независимо от их значения реестра ThreadingModel. Я подозреваю, что они объединяют Free Threaded Marshaler (более подробную информацию о потоке COM можно найти здесь).

Я хочу, чтобы мой .NET COM-объект использовал стандартный прокси-сервер marshaller COM, когда он маршализирован в другой поток. Проблема:

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<System.Threading.Tasks.TaskScheduler>();

            // 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 в другой. Я хочу, чтобы оба вызова Test() вызывались в том же исходном потоке, например. 11, как это было бы в случае с типичным объектом STA COM, реализованным на С++.

Одним из возможных решений является отключить интерфейс IMarshal на объекте .NET COM:

[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 }
{ equal = False }
{ CurrentManagedThreadId = 11 }

Это работает, но похоже, что это взлом для конкретной реализации. Есть ли более достойный способ сделать это, как какой-то специальный атрибут interop, который я мог бы упустить? Обратите внимание, что в реальной жизни ComObject используется (и получает маршалирование) устаревшим неуправляемым приложением.

Ответ 1

Вы можете наследовать от StandardOleMarshalObject или ServicedComponent для этого эффекта:

Управляемые объекты, которые подвергаются воздействию COM, ведут себя так, как если бы они объединили маркер с бесплатной резьбой. Другими словами, они могут быть вызваны из любой COM-квартиры свободно-поточным способом. Единственными управляемыми объектами, которые не демонстрируют это свободно-потоковое поведение, являются те объекты, которые получены из ServicedComponent или StandardOleMarshalObject.

Ответ 2

Отличный ответ Paulo Madeira обеспечивает отличное решение, когда управляемый класс, подвергающийся воздействию COM, может быть получен из StandardOleMarshalObject.

Я подумал, как справиться с случаями, когда уже существует базовый класс, например say System.Windows.Forms.Control, который не имеет StandardOleMarshalObject в цепочке наследования?

Оказывается, можно объединить стандартный маршалер COM. Как и в 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);
    }
}