如何使一个.NET COM对象成为单元线程模式?

17

.NET对象默认是自由线程的。如果通过COM编组到另一个线程,它们总是编组到它们自己,而不管创建线程是否为STA以及它们的ThreadingModel注册表值如何。我怀疑它们聚合了Free Threaded Marshaler(有关COM线程的更多详细信息,请参见此处)。

我想让我的.NET COM对象在编组到另一个线程时使用标准的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,就像在 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 }
{ equal = False }
{ CurrentManagedThreadId = 11 }

这个方法可以实现,但感觉像是一种特定于实现的hack。是否有更好的方法来完成这个操作,比如我可能忽略了某些特殊的interop属性?请注意,在现实生活中,ComObject由一个遗留的非托管应用程序使用,并且被marshal。


1
你总是提出最有趣的线程问题。我不知道答案,但我一定会关注这个问题,看看答案是什么。 - Scott Chamberlain
@ScottChamberlain,谢谢 :) 在没有涉及到COM的情况下,我很乐意使用像await WpfApartment.InvokeAsync(()=>DoSomething())这样的技术来在STA线程之间进行调用。 - noseratio - open to work
这只是一个非常人为的测试,真正的COM服务器当然不会表现出这种方式。它们有适当的ThreadModel,CLR会努力在您的测试程序中找到它,但当然注定会失败。没有任何机会找到代理,它就会放弃,并且根本不会进行接口封送。追求这个没有什么意义,如果您不打算使用真正的COM服务器,那就根本不要使用COM。 - Hans Passant
1
@HansPassant,实际服务器确实使用ComRegisterFunctionAttribute将对象注册为ThreadingModel=Apartment。然而,正如我上面提到的,这并不会改变我所描述的封送行为。非托管客户端在一个STA线程上调用CoMarshalInterThreadInterfaceInStream,然后在另一个STA线程上调用CoGetInterfaceAndReleaseStream,并获得相同的对象,而不是代理。这是因为任何.NET CCW都实现了IMarshal并且是自由线程的,无论其在注册表中的ThreadingModel如何。不要相信我的话,亲自试一试。 - noseratio - open to work
出于好奇,为什么您将IComObject的GUID定义为IDispatch的GUID? - acelent
1
@PauloMadeira,在这种特殊情况下,这是一个技巧,允许我在不执行“RegAsm”的情况下使用OLE Automation IDispatch风格的封送。COM使用IDispatch::Invoke,而.NET具有足够的元数据使这些IDispatch::Invoke调用成为硬编码。 - noseratio - open to work
2个回答

7

“StandardOleMarshalObject” 看起来正是我要找的!非常好的答案,谢谢。 - noseratio - open to work
1
我从链接页面中添加了引用。实际上,你的问题很好,包括代码,我只是用一个参考回答了它。 - acelent
再次感谢。你的回答让我找到了另一种解决方案,适用于不能从StandardOleMarshalObject派生类的情况。 - noseratio - open to work

6
Paulo Madeira的出色回答为暴露给COM的托管类提供了一个很好的解决方案,但是当已有一个基类(如System.Windows.Forms.Control)时,该基类的继承链中没有StandardOleMarshalObject,该如何处理呢?
事实证明,可以聚合标准COM Marshaler。类似于自由线程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);
    }
}

是的,但请注意,您正在使用的方式,.NET将为某些接口QueryInterface返回的指针(一个RCW),可能会AddRef并创建不可打破的循环。假设标准的编组器没有实现.NET查询的任何接口实现细节。但是,您可以通过在GetStdMarshaler中向Marshal.GetIUnknownForObject传递其他对象来将StandardOleMarshalObject的实现转换为适用于除this之外的其他对象。或者使用IntPtr而不是纯接口与此答案的方法。 - acelent
@PauloMadeira,不,这不是问题。CoGetStdMarshalEx被指定用于COM聚合。它实际上遵守聚合规则,并且不会AddRefRelease传递的pUnkOuter。除了自己的IUnknown接口(包括IMarshal),它还正确地将AddRef/Release/QueryInterface委托给pUnkOuter的所有其他接口。 - noseratio - open to work
@PauloMadeira,“为.NET创建一个RCW,并查询新创建的标准封送程序的非委托内部IUnknown。” - 不确定我是否理解了(不是在争论,我真的想理解你的观点)。据我所知,对于CoGetStdMarshalEx返回的聚合封送程序,没有单独的RCW。对于外部(聚合)对象ComObject,有一个单一的CCW,可以查询其IMarshal。然后,您可以调用该IMarshalQueryInterface以获取IManagedObject(或任何其他接口),并且无论是托管代码还是非托管代码,都将获得相同的正确的外部对象的CCW。 - noseratio - open to work
1
因此,.NET将创建一个RCW来访问被聚合对象的内部IUnknown并查询一些接口(除非您使用更不透明的东西,例如IntPtr)。我认为标准的marshaler没有实现它们中的任何一个,但这肯定是一个实现细节。因此,你现在可能是正确的。你的问题真的很好,因为它用最好的方式回答了自己(带有理由),以实现你想要的目标。 - acelent
1
似乎从ServicedComponent派生有更多的含义(来自2002年的https://msdn.microsoft.com/en-us/library/ms973847.aspx)。它几乎只是将工作委托给对象的公寓的副作用,但它似乎即使对于.NET调用也这样做,因此如果您不必从其他类派生或者如果您可以委托它,那么这是一个值得考虑的选项。 - acelent
显示剩余9条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接