2015-09-02 23 views
26

Sono abbastanza sicuro che mi manca qualche vincolo o avvertenza da qualche parte, ma qui è la mia situazione. Si supponga che ho una classe che voglio avere un proxy per, come il seguente:È possibile utilizzare la riflessione con istanze RealProxy?

public class MyList : MarshalByRefObject, IList<string> 
{ 
    private List<string> innerList; 

    public MyList(IEnumerable<string> stringList) 
    { 
     this.innerList = new List<string>(stringList); 
    } 

    // IList<string> implementation omitted for brevity. 
    // For the sake of this exercise, assume each method 
    // implementation merely passes through to the associated 
    // method on the innerList member variable. 
} 

voglio creare un proxy per quella classe, in modo che possa intercettare le chiamate di metodo e di eseguire alcune elaborazioni sull'oggetto sottostante . Qui è la mia realizzazione:

public class MyListProxy : RealProxy 
{ 
    private MyList actualList; 

    private MyListProxy(Type typeToProxy, IEnumerable<string> stringList) 
     : base(typeToProxy) 
    { 
     this.actualList = new MyList(stringList); 
    } 

    public static object CreateProxy(IEnumerable<string> stringList) 
    { 
     MyListProxy listProxy = new MyListProxy(typeof(MyList), stringList); 
     object foo = listProxy.GetTransparentProxy(); 
     return foo; 
    } 

    public override IMessage Invoke(IMessage msg) 
    { 
     IMethodCallMessage callMsg = msg as IMethodCallMessage; 
     MethodInfo proxiedMethod = callMsg.MethodBase as MethodInfo; 
     return new ReturnMessage(proxiedMethod.Invoke(actualList, callMsg.Args), null, 0, callMsg.LogicalCallContext, callMsg); 
    } 
} 

Infine, ho una classe che consuma la classe Proxied, e ho impostato il valore del membro MyList attraverso la riflessione.

public class ListConsumer 
{ 
    public MyList MyList { get; protected set; } 

    public ListConsumer() 
    { 
     object listProxy = MyListProxy.CreateProxy(new List<string>() { "foo", "bar", "baz", "qux" }); 
     PropertyInfo myListPropInfo = this.GetType().GetProperty("MyList"); 
     myListPropInfo.SetValue(this, listProxy); 
    } 
} 

Ora, se provo a utilizzare il reflection per accedere all'oggetto proxy, mi imbatto in problemi. Ecco un esempio:

class Program 
{ 
    static void Main(string[] args) 
    { 
     ListConsumer listConsumer = new ListConsumer(); 

     // These calls merely illustrate that the property can be 
     // properly accessed and methods called through the created 
     // proxy without issue. 
     Console.WriteLine("List contains {0} items", listConsumer.MyList.Count); 
     Console.WriteLine("List contents:"); 
     foreach(string stringValue in listConsumer.MyList) 
     { 
      Console.WriteLine(stringValue); 
     } 

     Type listType = listConsumer.MyList.GetType(); 
     foreach (Type interfaceType in listType.GetInterfaces()) 
     { 
      if (interfaceType.IsGenericType && interfaceType.GetGenericTypeDefinition() == typeof(ICollection<>)) 
      { 
       // Attempting to get the value of the Count property via 
       // reflection throws an exception. 
       Console.WriteLine("Checking interface {0}", interfaceType.Name); 
       System.Reflection.PropertyInfo propInfo = interfaceType.GetProperty("Count"); 
       int count = (int)propInfo.GetValue(listConsumer.MyList, null); 
      } 
      else 
      { 
       Console.WriteLine("Skipping interface {0}", interfaceType.Name); 
      } 
     } 

     Console.ReadLine(); 
    } 
} 

tentativo di chiamare GetValue dell'immobile Count tramite riflessione genera la seguente eccezione:

Un'eccezione di tipo 'System.Reflection.TargetException' verificata in mscorlib.dll ma non è stato gestito nel codice utente

Ulteriori informazioni: L'oggetto non corrisponde al tipo di destinazione.

Quando si tenta di ottenere il valore della proprietà Count, a quanto pare il quadro sta chiamando giù in System.Runtime.InteropServices.WindowsRuntime.IVector per chiamare il metodo get_Size. Non sto capendo come questa chiamata fallisce sull'oggetto sottostante del proxy (la lista vera e propria) per farlo accadere. Se non sto usando un proxy dell'oggetto, ottenere il valore della proprietà funziona bene tramite riflessione. Che cosa sto facendo di sbagliato? Posso fare anche quello che sto cercando di realizzare?

Modifica: A bug has been opened relativo a questo problema nel sito Microsoft Connect.

+1

Nota questa implementazione non è speculazione inattiva. [Il metodo 'Assert.Count' di MbUnit] (https://github.com/Gallio/mbunit-v3/blob/master/src/MbUnit/MbUnit/Framework/Assert.Count.cs) lo fa per alcune raccolte. Se l'oggetto collection è un proxy, la chiamata a 'Assert.Count' genera. – JimEvans

+0

È possibile rendere generico 'MyListProxy.CreateProxy', in modo che restituisca il tipo reale e non l'oggetto di tipo? Per il test: se questa chiamata nel principale 'interfaceType.GetProperty (" Count ")' è cambiata in '((MyList) interfaceType) .GetProperty (" Count ")' fa quindi la chiamata a 'Count'work? – pasty

+0

problemi simili qui - usando Invoke() sembra causare il marshalling che fa cadere l'esecuzione in questo VectorToCollectionAdapter che poi barfs con il messaggio "L'oggetto non corrisponde al tipo di destinazione". (perché un IVector non è un ICollection). Considero questo un bug. – fusi

risposta

11

Penso che questo potrebbe essere un bug nel framework .Net. In qualche modo, il metodo RuntimePropertyInfo.GetValue sta rilevando un'implementazione errata per la proprietà ICollection<>.Count e sembra avere a che fare con le proiezioni di WindowsRuntime. Forse il codice remoto è stato rifatto quando hanno inserito l'interoperabilità di WindowsRuntime nel framework.

Ho commutato il framework su target .Net 2.0 poiché ho pensato che se si trattasse di un bug, non dovrebbe essere in tale contesto. Durante la conversione, Visual Studio ha rimosso il controllo "Prefer 32 bit" sul mio progetto exe della console (poiché questo non esiste in 2.0). Funziona senza eccezioni quando questo non è presente.

In conclusione, funziona su .Net 2.0 sia a 32 che a 64 bit. Funziona su .Net 4.x in 64 bit. L'eccezione è lanciata solo su .Net 4.x 32 bit. Questo sicuramente sembra un bug. Se è possibile eseguirlo a 64 bit, sarebbe una soluzione alternativa.

Nota che ho installato .Net 4.6 e questo sostituisce gran parte del framework .Net v4.x. Potrebbe essere qui che viene introdotto il problema; Non riesco a testare finché non avrò una macchina che non ha .Net 4.6.

Aggiornamento: 2015-09-08

Succede anche su una macchina con solo .Net 4.5.2 installato (non 4.6).

Aggiornamento: 2015-09-07

Ecco un Repro più piccolo, utilizzando le stesse classi:

static void Main(string[] args) 
{ 
    var myList = MyListProxy.CreateProxy(new[] {"foo", "bar", "baz", "quxx"}); 
    var listType = myList.GetType(); 
    var interfaceType = listType.GetInterface("System.Collections.Generic.ICollection`1"); 
    var propInfo = interfaceType.GetProperty("Count"); 

    // TargetException thrown on 32-bit .Net 4.5.2+ installed 
    int count = (int)propInfo.GetValue(myList, null); 
} 

Ho anche provato la proprietà IsReadOnly, ma sembra funzionare (non eccezione).


Quanto alla fonte del problema, ci sono due strati di indiretto intorno proprietà, uno è il servizio remoto, e l'altro è una mappatura di strutture di metadati chiamato MethodDef s con il metodo di esecuzione effettiva, conosciuto internamente come a MethodDesc. This mapping is specialized for properties (as well as events), where additional MethodDescs to support the property's get/set PropertyInfo instances are known as Associates. Chiamando PropertyInfo.GetValue passiamo attraverso uno di questi collegamenti Associati MethodDesc all'implementazione del metodo sottostante e il servizio remoto esegue un calcolo matematico del puntatore per ottenere il valore corretto MethodDesc sull'altro lato del canale. Il codice CLR è molto complicato qui e non ho abbastanza esperienza del layout in memoria dello MethodTable che contiene questi record MethodDesc quali usi remoti (o il mapping utilizzato per arrivare a MethodTable?), Ma I ' d dire che è ragionevole supporre che il remoting stia afferrando il MethodDesc sbagliato con una cattiva matematica dei puntatori. Ecco perché vediamo una simile, ma non correlati (per quanto riguarda il vostro programma) MethodDesc - UInt32 get_Size di IVector<T> essere invocato la chiamata:

System.Reflection.RuntimeMethodInfo.CheckConsistency(Object target) 
System.Reflection.RuntimeMethodInfo.InvokeArgumentsCheck(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture) 
System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture) 
System.Reflection.MethodBase.Invoke(Object obj, Object[] parameters) 
ConsoleApplication1.MyListProxy.Invoke(IMessage msg) Program.cs: line: 60 
System.Runtime.Remoting.Proxies.RealProxy.PrivateInvoke(MessageData& msgData, Int32 type) 
System.Runtime.InteropServices.WindowsRuntime.IVector`1.get_Size() 
System.Runtime.InteropServices.WindowsRuntime.VectorToCollectionAdapter.Count[T]() 
11

Questa è una piuttosto interessante bug CLR, alcune delle sue viscere stanno mostrando nel contrattempo . È possibile rilevare dallo stack trace che sta tentando di chiamare la proprietà Count di VectorToCollectionAdapter.

Questa classe è piuttosto speciale, nessuna istanza viene mai creata. Fa parte della proiezione di lingua aggiunta in .NET 4.5 che rende i tipi di interfaccia WinRT simili ai tipi di .NET Framework. È abbastanza simile alla classe SZArrayHelper, una classe adattatore che aiuta a realizzare l'illusione che gli array non generici implementino tipi di interfaccia generici come IList<T>.

La mappatura dell'interfaccia al lavoro qui è per l'interfaccia WinRT IVector<T>. Come indicato nell'articolo MSDN, tale tipo di interfaccia è mappato su IList<T>. La classe interna VectorToListAdapter si prende cura dei membri IList<T>, VectorToCollectionAdapter affronta i membri ICollection<T>.

Il codice forza il CLR a trovare l'implementazione di ICollection <> .Count e potrebbe essere una classe .NET che lo implementa normalmente oppure potrebbe essere un oggetto WinRT che lo espone come IVector <> .Size. Chiaramente il proxy che hai creato gli da un mal di testa, ha deciso in modo errato per la versione WinRT.

Come è supposto per capire quale sia la scelta corretta è piuttosto torbida. Dopo tutto, il tuo proxy potrebbe essere un proxy per un oggetto WinRT effettivo e quindi la scelta effettuata sarebbe corretta. Questo potrebbe essere un problema strutturale. Che funzioni in modo casuale, il codice funziona in modalità 64 bit, non è esattamente stimolante. VectorToCollectionAdapter è molto pericoloso, si noti JitHelpers.Chiamate UnsafeCast, questo bug è potenzialmente sfruttabile.

Bene, avvisare le autorità, presentare una segnalazione di errore su connect.microsoft.com. Fammi sapere se non vuoi prenderti il ​​tempo e me ne occuperò io. È difficile trovare una soluzione, usare la classe TypeInfo di WinRT per fare il reflection non ha fatto alcuna differenza. La rimozione del forzamento del jitter in modo che funzioni in modalità a 64 bit è un cerotto ma difficilmente una garanzia.

4

stiamo di hacking risolvere questo problema con questo intervento fragile (scuse per codice):

public class ProxyBase : RealProxy 
{ 
    // ... stuff ... 

    public static T Cast<T>(object o) 
    { 
     return (T)o; 
    } 

    public static object Create(Type interfaceType, object coreInstance, 
     IEnforce enforce, string parentNamingSequence) 
    { 
     var x = new ProxyBase(interfaceType, coreInstance, enforce, 
      parentNamingSequence); 

     MethodInfo castMethod = typeof(ProxyBase).GetMethod(
      "Cast").MakeGenericMethod(interfaceType); 

     return castMethod.Invoke(null, new object[] { x.GetTransparentProxy() }); 
    } 

    public override IMessage Invoke(IMessage msg) 
    { 
     IMethodCallMessage methodCall = (IMethodCallMessage)msg; 
     var method = (MethodInfo)methodCall.MethodBase; 

     if(method.DeclaringType.IsGenericType 
     && method.DeclaringType.GetGenericTypeDefinition().FullName.Contains(
      "System.Runtime.InteropServices.WindowsRuntime")) 
     { 
      Dictionary<string, string> methodMap = new Dictionary<string, string> 
      { // add problematic methods here 
       { "Append", "Add" }, 
       { "GetAt", "get_Item" } 
      }; 

      if(methodMap.ContainsKey(method.Name) == false) 
      { 
       throw new Exception("Unable to resolve '" + method.Name + "'."); 
      } 
      // thanks microsoft 
      string correctMethod = methodMap[method.Name]; 
      method = m_baseInterface.GetInterfaces().Select(
       i => i.GetMethod(correctMethod)).Where(
        mi => mi != null).FirstOrDefault(); 

      if(method == null) 
      { 
       throw new Exception("Unable to resolve '" + method.Name + 
        "' to '" + correctMethod + "'."); 
      } 
     } 

     try 
     { 
      if(m_coreInstance == null) 
      { 
       var errorMessage = Resource.CoreInstanceIsNull; 
       WriteLogs(errorMessage, TraceEventType.Error); 
       throw new NullReferenceException(errorMessage); 
      } 

      var args = methodCall.Args.Select(a => 
      { 
       object o; 

       if(RemotingServices.IsTransparentProxy(a)) 
       { 
        o = (RemotingServices.GetRealProxy(a) 
         as ProxyBase).m_coreInstance; 
       } 
       else 
       { 
        o = a; 
       } 

       if(method.Name == "get_Item") 
       { // perform parameter conversions here 
        if(a.GetType() == typeof(UInt32)) 
        { 
         return Convert.ToInt32(a); 
        } 

        return a;        
       } 

       return o; 
      }).ToArray(); 
      // this is where it barfed 
      var result = method.Invoke(m_coreInstance, args); 
      // special handling for GetType() 
      if(method.Name == "GetType") 
      { 
       result = m_baseInterface; 
      } 
      else 
      { 
       // special handling for interface return types 
       if(method.ReturnType.IsInterface) 
       { 
        result = ProxyBase.Create(method.ReturnType, result, m_enforce, m_namingSequence); 
       } 
      } 

      return new ReturnMessage(result, args, args.Length, methodCall.LogicalCallContext, methodCall); 
     } 
     catch(Exception e) 
     { 
      WriteLogs("Exception: " + e, TraceEventType.Error); 
      if(e is TargetInvocationException && e.InnerException != null) 
      { 
       return new ReturnMessage(e.InnerException, msg as IMethodCallMessage); 
      } 
      return new ReturnMessage(e, msg as IMethodCallMessage); 
     } 
    } 

    // ... stuff ... 
} 

m_coreInstance Ecco l'istanza di oggetto che il proxy è avvolgente.

m_baseInterface è l'interfaccia a cui deve essere utilizzato l'oggetto.

questo codice intercetta le chiamate effettuate in VectorToListAdapter e VectorToCollectionAdapter e le converte nell'originale tramite il dizionario metodoMap.

la parte del condizionale:

method.DeclaringType.GetGenericTypeDefinition().FullName.Contains(
     "System.Runtime.InteropServices.WindowsRuntime") 

si assicura che intercetta solo le chiamate che provengono da roba nello spazio dei nomi System.Runtime.InteropServices.WindowsRuntime - idealmente vorremmo indirizzare direttamente i tipi ma sono inaccessibili - questo dovrebbe probabilmente essere cambiato per bersagliare nomi di classi specifici nello spazio dei nomi.

i parametri vengono quindi convertiti nei tipi appropriati e il metodo viene richiamato. le conversioni dei parametri sembrano essere necessarie in quanto i tipi di parametri in ingresso sono basati sui tipi di parametri del metodo chiamate da gli oggetti nello spazio dei nomi System.Runtime.InteropServices.WindowsRuntime e non i parametri del metodo chiamate a tipi di oggetti originali; cioè i tipi originali prima che gli oggetti nello spazio dei nomi System.Runtime.InteropServices.WindowsRuntime abbiano violato il meccanismo.

ad esempio, la roba di WindowsRuntime intercetta la chiamata originale a get_Item e la converte in una chiamata al metodo Indexer_Get: http://referencesource.microsoft.com/#mscorlib/system/runtime/interopservices/windowsruntime/vectortolistadapter.cs,de8c78a8f98213a0,references. questo metodo chiama quindi il membro GetAt con un tipo di parametro diverso, che quindi chiama GetAt sul nostro oggetto (sempre con un diverso tipo di parametro) - questa è la chiamata che dirottiamo nel nostro Invoke() e la riconvertiamo nella chiamata al metodo originale con i tipi di parametri originali.

Sarebbe bello poter riflettere su VectorToListAdapter e VectorToCollectionAdapter per estrarre tutti i loro metodi e le chiamate nidificate che fanno, ma queste classi sono sfortunatamente contrassegnate come interne.

questo funziona per noi qui, ma sono sicuro che è pieno di buchi - è un caso di tentativi ed errori, eseguirlo per vedere ciò che fallisce e quindi aggiungere le voci del dizionario richiesto/conversioni dei parametri. stiamo continuando la ricerca di una soluzione migliore.

HTH