2015-02-20 15 views
12

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.

+1

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. –

+0

@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

+0

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. –

risposta

5

È possibile ereditare da StandardOleMarshalObject or ServicedComponent per quell'effetto:

Gestito oggetti che sono esposti a COM comportarsi come se avessero aggregato il gestore di marshalling a thread libero. In altre parole, possono essere chiamati da qualsiasi appartamento COM in modalità a thread libero. Gli unici oggetti gestiti che non mostrano questo comportamento a thread libero sono quegli oggetti che derivano da ServicedComponent o StandardOleMarshalObject.

+0

'StandardOleMarshalObject' sembra essere esattamente quello che sto cercando! una grande risposta, tks. – Noseratio

+1

Ho aggiunto la citazione dalla pagina collegata. In realtà, la tua domanda è ottima, codice e tutto, ho appena risposto con un riferimento. – acelent

+0

Grazie ancora. La tua risposta mi ha portato a [un'altra soluzione] (http://stackoverflow.com/a/28654583/1768303), adatta a quando la classe non può essere derivata da "StandardOleMarshalObject". – Noseratio

4

Paulo Madeira's excellent answer fornisce una grande soluzione per quando la classe gestita essere esposti a COM può essere derivato da StandardOleMarshalObject.

Mi ha fatto pensare, tuttavia, come gestire i casi in cui esiste già una classe di base, ad esempio System.Windows.Forms.Control, che non ha StandardOleMarshalObject nella catena di ereditarietà?

Si scopre che è possibile aggregare lo standard COM Marshaler. Simile a quello di Marshaler con threading gratuito CoCreateFreeThreadedMarshaler, esiste un'API per questo: CoGetStdMarshalEx. Ecco come può essere fatto:

[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); 
    } 
} 
+0

Si dovrebbe dare un'occhiata all'applicazione di riferimento [StandardOleMarshalObject] (http://referencesource.microsoft.com/#System/compmod/System/Runtime/InteropServices/StandardOleMarshalObject.cs,c00f99eb10426274). Implementa 'IMarshal' e delega a' CoGetStandardMarshal', molto più vicino allo scopo di ciò che si desidera, anche se sembra che 'CoGetStdMarshalEx' dovrebbe funzionare, anche se è stato progettato per i gestori lato client. – acelent

+0

@PauloMadeira, l'ho guardato, richiede che tutti i metodi di 'IMarshal' siano esplicitamente implementati dove non posso derivare da' StandardOleMarshalObject'. Non del tutto riutilizzabile, IMO. 'CoGetStdMarshalEx' offre 'SMEXF_SERVER' appositamente per l'uso da parte dei server COM. – Noseratio

+0

Sì, ma si noti che nel modo in cui lo si utilizza, .NET restituirà 'QueryInterface' il puntatore restituito (un RCW) per alcune interfacce, possibilmente' AddRef'solo e creando un ciclo indistruttibile. Supponendo che il marshaler standard non implementa alcuna delle interfacce interrogate di .NET ** è ** un dettaglio di implementazione. Tuttavia, è possibile attivare l'implementazione di 'StandardOleMarshalObject' per lavorare su un oggetto diverso da' this' passando qualche altro oggetto a 'Marshal.GetIUnknownForObject' in' GetStdMarshaler'. Oppure usa "IntPtr" invece di semplici interfacce con l'approccio di questa risposta. – acelent

Problemi correlati