如何使 .NET COM 对象成为单元线程? [英] How to make make a .NET COM object apartment-threaded?

查看:13
本文介绍了如何使 .NET COM 对象成为单元线程?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

.NET 对象默认是自由线程的.如果通过 COM 编组到另一个线程,它们总是被编组给自己,无论创建者线程是否是 STA,也不管它们的 ThreadingModel 注册表值如何.我怀疑,他们聚合了 Free Threaded Marshaler(可以找到关于 COM 线程的更多详细信息 这里).

我想让我的 .NET COM 对象在编组到另一个线程时使用标准 COM 编组器代理.问题:

使用系统;使用 System.Runtime.InteropServices;使用 System.Threading;使用 System.Threading.Tasks;使用 System.Windows.Threading;命名空间控制台应用程序{课堂节目{静态无效主要(字符串 [] 参数){var apt1 = new WpfApartment();var apt2 = new WpfApartment();apt1.Invoke(() =>{var comObj = 新的 ComObject();comObj.Test();IntPtr pStm;NativeMethods.CoMarshalInterThreadInterfaceInStream(NativeMethods.IID_IUnknown, comObj, out pStm);apt2.Invoke(() =>{对象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(真)][Guid("00020400-0000-0000-C000-000000000046")]//IID_IDispatch[接口类型(ComInterfaceType.InterfaceIsIDispatch)]公共接口 IComObject{无效测试();}[ComVisible(真)][ClassInterface(ClassInterfaceType.None)][ComDefaultInterface(typeof(IComObject))]公共类 ComObject : IComObject{//IComObject 方法公共无效测试(){Console.WriteLine(new { Environment.CurrentManagedThreadId });}}//WpfApartment - WPF 调度程序线程内部类 WpfApartment : IDisposable{线程_thread;//STA线程公共 System.Threading.Tasks.TaskScheduler TaskScheduler { get;私人套装;}公共 WpfApartment(){var tcs = new TaskCompletionSource();//使用 WPF Dispatcher 启动 STA 线程_thread = 新线程(_ =>{NativeMethods.OleInitialize(IntPtr.Zero);尝试{//发布回调以获取 TaskSchedulerDispatcher.CurrentDispatcher.InvokeAsync(() =>tcs.SetResult(System.Threading.Tasks.TaskScheduler.FromCurrentSynchronizationContext()),DispatcherPriority.ApplicationIdle);//运行 WPF Dispatcher 消息循环调度程序.Run();}最后{NativeMethods.OleUninitialize();}});_thread.SetApartmentState(ApartmentState.STA);_thread.IsBackground = true;_thread.Start();this.TaskScheduler = tcs.Task.Result;}//关闭STA线程公共无效处置(){if (_thread != null && _thread.IsAlive){InvokeAsync(() => System.Windows.Threading.Dispatcher.ExitAllFrames());_thread.Join();_thread = null;}}//Task.Factory.StartNew 包装器公共任务 InvokeAsync(操作动作){返回Task.Factory.StartNew(动作,CancellationToken.None, TaskCreationOptions.None, this.TaskScheduler);}公共无效调用(动作动作){InvokeAsync(action).Wait();}}公共静态类 NativeMethods{公共静态只读 Guid IID_IUnknown = new Guid("00000000-0000-0000-C000-000000000046");公共静态只读 Guid IID_IDispatch = new Guid("00020400-0000-0000-C000-000000000046");[DllImport("ole32.dll", PreserveSig = false)]公共静态外部无效CoMarshalInterThreadInterfaceInStream([在,MarshalAs(UnmanagedType.LPStruct)] Guid riid,[MarshalAs(UnmanagedType.IUnknown)] 对象 pUnk,出 IntPtr ppStm);[DllImport("ole32.dll", PreserveSig = false)]公共静态外部无效CoGetInterfaceAndReleaseStream(IntPtr pStm,[在,MarshalAs(UnmanagedType.LPStruct)] Guid riid,[MarshalAs(UnmanagedType.IUnknown)] 出对象 ppv);[DllImport("ole32.dll", PreserveSig = false)]公共静态外部无效OleInitialize(IntPtr pvReserved);[DllImport("ole32.dll", PreserveSig = true)]公共静态外部无效OleUninitialize();}}

输出:

<上一页>{ CurrentManagedThreadId = 11 }{ 等于 = 真 }{ CurrentManagedThreadId = 12 }

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

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

[ComVisible(true)][ClassInterface(ClassInterfaceType.None)][ComDefaultInterface(typeof(IComObject))]公共类 ComObject : IComObject, ICustomQueryInterface{//IComObject 方法公共无效测试(){Console.WriteLine(new { Environment.CurrentManagedThreadId });}公共静态只读 Guid IID_IMarshal = new Guid("00000003-0000-0000-C000-000000000046");公共 CustomQueryInterfaceResult GetInterface(ref Guid iid, out IntPtr ppv){ppv = IntPtr. 零;如果(iid == IID_IMarshal){返回 CustomQueryInterfaceResult.Failed;}返回 CustomQueryInterfaceResult.NotHandled;}}

输出(根据需要):

<上一页>{ CurrentManagedThreadId = 11 }{ 等于 = 假 }{ CurrentManagedThreadId = 11 }

这行得通,但感觉就像是特定于实现的 hack.有没有更体面的方法来完成这项工作,比如我可能忽略的一些特殊互操作属性?请注意,在现实生活中,ComObject 由旧版非托管应用程序使用(并被封送).

解决方案

你可以继承自 StandardOleMarshalObjectServicedComponent 用于该效果:

<块引用>

向 COM 公开的托管对象的行为就像它们聚合了自由线程封送拆收器一样.换句话说,它们可以以自由线程的方式从任何 COM 单元中调用.唯一没有表现出这种自由线程行为的托管对象是那些派生自 ServicedComponentStandardOleMarshalObject.

.NET objects are free-threaded by default. If marshaled to another thread via COM, they always get marshaled to themselves, regardless of whether the creator thread was STA or not, and regardless of their ThreadingModel registry value. I suspect, they aggregate the Free Threaded Marshaler (more details about COM threading could be found here).

I want to make my .NET COM object use the standard COM marshaller proxy when marshaled to another thread. The problem:

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

Output:

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

Note I use CoMarshalInterThreadInterfaceInStream/CoGetInterfaceAndReleaseStream to marshal ComObject from one STA thread to another. I want both Test() calls to be invoked on the same original thread, e.g. 11, as it would have been the case with a typical STA COM object implemented in C++.

One possible solution is to disable IMarshal interface on the .NET COM object:

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

Output (as desired):

{ CurrentManagedThreadId = 11 }
{ equal = False }
{ CurrentManagedThreadId = 11 }

This works, but it feels like an implementation-specific hack. Is there a more decent way to get this done, like some special interop attribute I might have overlooked? Note that in real life ComObject is used (and gets marshaled) by a legacy unmanaged application.

解决方案

You can inherit from StandardOleMarshalObject or ServicedComponent for that effect:

Managed objects that are exposed to COM behave as if they had aggregated the free-threaded marshaler. In other words, they can be called from any COM apartment in a free-threaded manner. The only managed objects that do not exhibit this free-threaded behavior are those objects that derive from ServicedComponent or StandardOleMarshalObject.

这篇关于如何使 .NET COM 对象成为单元线程?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆