Как сделать .NET COM-объект многопоточным?

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

Я хочу, чтобы мой COM-объект .NET использовал стандартный прокси-маршаллер 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 }

Обратите внимание, что я использую _3 _ / _ 4_ для маршалинга ComObject от одного потока STA к другому. Я хочу, чтобы оба вызова Test() вызывались в одном и том же исходном потоке, например 11, как это было бы в случае с типичным COM-объектом STA, реализованным на C ++.

Одно из возможных решений - отключить интерфейс 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 }

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


person noseratio    schedule 20.02.2015    source источник
comment
У вас всегда есть самые интересные вопросы по цепочке. Я не знаю ответа на этот вопрос, но я обязательно буду смотреть этот вопрос, чтобы узнать, какой будет ответ.   -  person Scott Chamberlain    schedule 20.02.2015
comment
@ScottChamberlain, спасибо :) Там, где COM не задействован, я доволен использованием таких методов, как await WpfApartment.InvokeAsync(()=>DoSomething()), для маршалинга вызовов между потоками STA.   -  person noseratio    schedule 20.02.2015
comment
Это очень искусственный тест, реальные COM-серверы, конечно, так себя не ведут. У них есть правильный ThreadModel, CLR пытается найти его в вашей тестовой программе, но, конечно, обречена на провал. Без какой-либо попытки найти прокси-сервер он вообще не выполняет маршалинг интерфейса. В этом нет особого смысла, просто не беспокойтесь об использовании COM, если вы не собираетесь использовать настоящий COM-сервер.   -  person Hans Passant    schedule 20.02.2015
comment
@HansPassant, фактический сервер действительно использует ComRegisterFunctionAttribute для регистрации объекта как ThreadingModel=Apartment. Тем не менее, как я уже упоминал выше, это не меняет описанного мною маршалинга. Неуправляемый клиент вызывает CoMarshalInterThreadInterfaceInStream в одном потоке STA, затем CoGetInterfaceAndReleaseStream в другом потоке STA и получает тот же объект, не прокси. Это потому, что любая .NET CCW реализует IMarshal, а имеет свободную многопоточность, независимо от ее ThreadingModel в реестре. Не верьте мне на слово, попробуйте сами.   -  person noseratio    schedule 20.02.2015
comment
Из любопытства: почему вы определили GUID IComObjects как IDispatch?   -  person acelent    schedule 23.02.2015
comment
@PauloMadeira, в данном конкретном случае это трюк, который позволяет мне использовать маршалинг в стиле OLE Automation IDispatch без RegAsm. COM использует IDispatch::Invoke, а .NET имеет достаточно метаданных, чтобы сделать эти IDispatch::Invoke вызовы жестко типизированными.   -  person noseratio    schedule 23.02.2015


Ответы (2)


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

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

person acelent    schedule 20.02.2015
comment
StandardOleMarshalObject кажется именно то, что я ищу! отличный ответ, спасибо. - person noseratio; 21.02.2015
comment
Я добавил цитату со связанной страницы. На самом деле, ваш вопрос отличный, код и все, я только что ответил ссылкой. - person acelent; 21.02.2015
comment
Еще раз спасибо. Ваш ответ привел меня к еще одному решению, подходящему для случаев, когда класс не может быть производным от StandardOleMarshalObject. - person noseratio; 22.02.2015

Превосходный ответ Пауло Мадейры предоставляет отличное решение, когда управляемый класс, доступный для COM, может быть получен из StandardOleMarshalObject.

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

Оказывается, можно агрегировать Standard COM Marshaler. Подобно CoCreateFreeThreadedMarshaler Free Threaded Marshaler, для этого есть API: _ 5_. Вот как это можно сделать:

[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);
    }
}
person noseratio    schedule 22.02.2015
comment
Вам следует взглянуть на StandardOleMarshalObject эталонная реализация. Он реализует IMarshal и делегирует CoGetStandardMarshal, что намного ближе к тому, что вы хотите, хотя кажется, CoGetStdMarshalEx должно работать, даже несмотря на то, что он был разработан для обработчиков на стороне клиента. - person acelent; 22.02.2015
comment
@PauloMadeira, я смотрел на это, он требует, чтобы все IMarshal методы были явно реализованы там, где я не могу наследовать от StandardOleMarshalObject. Не совсем многоразовый, ИМО. CoGetStdMarshalEx предлагает SMEXF_SERVER специально для использования COM-серверами. - person noseratio; 23.02.2015
comment
Да, но учтите, что при вашем использовании .NET QueryInterface будет возвращать указатель (RCW) для некоторых интерфейсов, возможно, AddRef изменяя его и создавая неразрывный цикл. Предполагая, что стандартный маршалер не реализует ни один из запрашиваемых интерфейсов .NET, это деталь реализации. Однако вы можете включить реализацию StandardOleMarshalObject для работы с объектом, отличным от this, передав какой-либо другой объект Marshal.GetIUnknownForObject в GetStdMarshaler. Или используйте IntPtr вместо простых интерфейсов с этим подходом ответа. - person acelent; 23.02.2015
comment
@PauloMadeira, нет, это не проблема. CoGetStdMarshalEx предназначен для агрегации COM. Фактически он подчиняется правилам агрегирования., и это не AddRef или Release пройденный pUnkOuter. Он также правильно делегирует _5 _ / _ 6 _ / _ 7_ на pUnkOuter на всех интерфейсах, кроме своего собственного IUnknown (включая IMarshal, тоже). - person noseratio; 23.02.2015
comment
Возможно, вы правы, но более простой вариант, который вы указали в своем вопросе (отказ QueryInterface для IMarshal), заставит COM делать свое дело, минуя FTM (из CoCreateFreeThreadedMarshaler или пользовательского .NET). В любом случае, имея RCW для объекта, который указывает на нас ... Я думаю, .NET использует интерфейс (IManagedObject, я думаю), чтобы иметь возможность развернуть RCW в CCW, но полагаться на это опасно. Для меня адаптация StandardOleMarshalObject для произвольных объектов .NET была бы подходящим вариантом с не очень большими затратами на делегирование методов IMarshal (поместите это в #region). - person acelent; 23.02.2015
comment
Но помните, я говорил о том, что .NET создает RCW и запрашивает неделегированный внутренний IUnknown для недавно созданного стандартного упаковщика. - person acelent; 23.02.2015
comment
@PauloMadeira, .NET создает RCW для и запрашивает неделегированный внутренний IUnknown для недавно созданного стандартного маршаллера. - не уверен, что следую (не спорю, я действительно хочу понять вашу точку зрения). AFAIU, нет отдельного RCW для агрегированного маршалера, возвращаемого CoGetStdMarshalEx. Есть один единственный CCW для внешнего (агрегирующего) объекта ComObject, который запрашивается для IMarshal. Затем вы можете вызвать это IMarshal QueryInterface для IManagedObject (или любого другого iface), и вы получите тот же правильный внешний объект CCW из управляемого или неуправляемого кода. - person noseratio; 23.02.2015
comment
В агрегации COM вы обычно передаете this IUnknown в качестве управляющего объекта агрегатора, и вы получаете IUnknown внутреннего агрегатора. Последнее является узким путем, только агрегатор должен запросить у агрегатора один из его интерфейсов. Сразу после этого вы получаете указатель на интерфейс, IUnknown методы которого делегируют агрегатору. IUnknowns используются, чтобы убедиться, что вы получаете правильную идентификацию. - person acelent; 23.02.2015
comment
Итак, .NET создаст RCW для внутреннего IUnknown агрегатора и запросит у него некоторые интерфейсы (если вы не используете что-то более непрозрачное, например IntPtr). Я думаю, что стандартный маршалер не реализует ни один из них, но это, безусловно, деталь реализации. Следовательно, возможно, вы сейчас правы. Ваш вопрос действительно велик, поскольку он отвечает сам (с обоснованием) тем, что, вероятно, является лучшим способом достичь того, чего вы хотите. - person acelent; 23.02.2015
comment
И на самом деле, теперь, когда я думаю об этом, используя CoGetStdMarshalEx, вы получите объект, привязанный к квартире. Если вы используете указатель на интерфейс, позволяющий .NET создавать RCW, вы рискуете получить цикл (я признаю, что риск этого действительно низкий), но если вы этого не сделаете, вам придется самостоятельно маршалировать указатель (как объекты FTM в C ++). Использование CoGetStandardMarshal каждый раз, когда вас запрашивают для маршалинга, может работать лучше. Даже в этом случае вы не защищаете .NET-код от вызова методов вашего объекта из других потоков. - person acelent; 23.02.2015
comment
Вероятно, вам лучше просто использовать WPF Threading.Dispatcher, захватить текущий в конструкторе и использовать Invoke или BeginInvoke в методах объекта. Это работает как для COM, так и для .NET и не требует внутрипроцессного маршалинга COM. Для этого вы можете использовать PostSharp, и я думаю, что этот конкретный аспект не слишком сложен для реализации в бесплатной версии. Я думаю, что нет необходимости заставлять объект .NET фактически выполнять работу с MTA, но я думаю, что это тоже возможно, если проверить текущую квартиру и делегировать пулу потоков, если это STA. - person acelent; 23.02.2015
comment
@PauloMadeira, верно, цель действительно состоит в том, чтобы объект ComObject был привязан к конкретному апартаменту (что позволяет безопасно использовать внутри него другие связанные с апартаментами COM-объекты). Если используется из других потоков, управляемых или неуправляемых, всегда используется маршалинг _2 _ / _ 3_ (или таблица глобального интерфейса), и вызываются только IComObject методы. - person noseratio; 23.02.2015
comment
@PauloMadeira, материал WPF здесь используется исключительно для тестирования, для создания квартир STA. Фактический ComObject - это элемент управления WPF, но он используется исключительно из устаревшего неуправляемого приложения (и это приложение выполняет маршалинг в стиле COM). - person noseratio; 23.02.2015
comment
Кажется, что заимствование из ServicedComponent имеет гораздо больше последствий (с 2002 г.) . Это почти просто побочный эффект, заключающийся в том, что он делегирует работу апартаментам объекта, но, похоже, он делает это даже для вызовов .NET, поэтому, если вам не нужно наследовать от какого-либо другого класса или если вы можете делегировать это, это вариант для рассмотрения. - person acelent; 23.02.2015