Gli oggetti .NET sono a thread libero per impostazione predefinita. Se il marshalling su un altro thread tramite COM, viene sempre effettuato il marshalling a se stessi, indipendentemente dal fatto che il thread del creatore sia STA o meno e indipendentemente dal loro valore di registro ThreadingModel
. Sospetto, essi aggregano lo Free Threaded Marshaler (ulteriori dettagli sulla creazione di thread COM potrebbero essere trovati here).Come fare rendere un oggetto COM .NET apartment-threaded?
Voglio rendere il mio oggetto COM .NET utilizzare il proxy di marshalling COM standard quando il marshalling su un altro thread. Il 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<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();
}
}
uscita:
{ CurrentManagedThreadId = 11 } { equal = True } { CurrentManagedThreadId = 12 }
Nota che uso CoMarshalInterThreadInterfaceInStream
/CoGetInterfaceAndReleaseStream
al maresciallo ComObject
da un thread STA ad un altro. Desidero che entrambe le chiamate Test()
vengano richiamate sullo stesso thread originale, ad es. 11
, come sarebbe stato il caso con un tipico oggetto COM STA implementato in C++.
Una possibile soluzione è quella di disabilitare IMarshal
interfaccia sull'oggetto 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;
}
}
uscita (come desiderato):
{ CurrentManagedThreadId = 11 } { equal = False } { CurrentManagedThreadId = 11 }
Questo funziona, ma sembra un'implementazione specifica mod. C'è un modo più decente per fare questo, come qualche attributo interop speciale che avrei potuto trascurare? Si noti che nella vita reale ComObject
viene utilizzato (e viene eseguito il marshalling) da un'applicazione legacy non gestita.
Hai sempre le domande più interessanti sul threading. Non conosco la risposta a questo, ma sicuramente osserverò questa domanda per vedere quale sarà la risposta. –
@ScottChamberlain, grazie :) Dove COM non è coinvolto, sono contento di usare tecniche come 'attendere WpfApartment.InvokeAsync (() => DoSomething())' per effettuare il marshalling delle chiamate tra i thread STA. – Noseratio
Si tratta di un * test * molto * artificiale, i veri server COM, naturalmente, non si comportano in questo modo. Hanno un ThreadModel adeguato, il CLR fa uno sforzo per trovarlo nel tuo programma di test ma, naturalmente, è destinato a fallire. Senza alcun tentativo di trovare un proxy, punisce e non esegue il marshalling dell'interfaccia. Non ha molto senso perseguire questo, basta non preoccuparsi di usare COM se non si intende usare un vero server COM. –