2015-09-02 51 views
26

我很確定我錯過了一些限制或警告的地方,但這是我的情況。假設我有,我想有一個代理類,如下所示:我可以在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. 
} 

我想創建該類的代理,這樣我可以攔截方法調用與底層對象進行一些處理。下面是我的實現:

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

最後,我有消耗代理的類的類,我設置爲通過反射MyList成員的值。

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

現在,如果我嘗試使用反射來訪問代理對象,我遇到了問題。下面是一個例子:

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

嘗試調用GetValue經由反射Count屬性引發以下例外:

類型「System.Reflection.TargetException」的異常出現在 mscorlib.dll中但未在用戶代碼中處理

附加信息:對象與目標類型不匹配。

當試圖獲取Count屬性的值,顯然框架調用分解成System.Runtime.InteropServices.WindowsRuntime.IVector調用get_Size方法。我不理解這個調用如何在代理的基礎對象(實際列表)上失敗以實現這一點。如果我不使用對象的代理,通過反射獲取屬性值可以正常工作。我究竟做錯了什麼?我甚至可以做我想要完成的事情?

編輯: A bug has been opened關於Microsoft Connect站點上的此問題。

+1

注意這個實現不是閒置的猜測。 [MbUnit的'Assert.Count'方法](https://github.com/Gallio/mbunit-v3/blob/master/src/MbUnit/MbUnit/Framework/Assert.Count.cs)爲某些集合執行此操作。如果集合對象是代理,則會調用'Assert.Count'。 – JimEvans

+0

是否有可能使MyListProxy.CreateProxy通用,以便返回實型而不是類型對象?對於測試:如果在main'interfaceType.GetProperty(「Count」)'中的這個調用改變爲'((MyList)interfaceType).GetProperty(「Count」)'然後調用'Count'work? – pasty

+0

這裏有類似的問題 - 使用Invoke()似乎會導致編組,導致執行落入此VectorToCollectionAdapter,然後Bar欄中出現消息「Object與目標類型不匹配」。 (因爲IVector不是ICollection)。我認爲這是一個錯誤。 – fusi

回答

11

我認爲這可能是.Net框架中的一個錯誤。不知怎的,RuntimePropertyInfo.GetValue方法正在挑選ICollection<>.Count屬性的錯誤實現,它似乎與WindowsRuntime預測有關。當他們將WindowsRuntime互操作放到框架中時,可能會重做遠程代碼。

我將框架轉換爲.Net 2.0,因爲我認爲如果這是一個錯誤,它不應該在該框架中。轉換時,Visual Studio刪除了我的console exe項目中的「首選32位」檢查(因爲這在2.0中不存在)。當它不存在時,它毫無例外地運行。

總之,它運行在32位和64位的.Net 2.0上。它運行在64位的.Net 4.x上。僅在.Net 4.x 32位上拋出異常。這確實看起來像一個錯誤。如果你可以運行它64位,這將是一個解決方法。

請注意,我安裝了.Net 4.6,這取代了許多.Net框架v4.x.這可能是問題出現的地方;我無法測試,直到我得到一臺沒有.Net 4.6的機器。

更新:2015-09-08

這也恰好的機器上只安裝了.NET 4.5.2(無4.6)。

更新:2015年9月7日

這裏有一個小的攝製,使用相同的類:

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

我也試過IsReadOnly屬性,但它似乎工作(沒有例外)。


至於錯誤的來源,有圍繞特性間接兩層,其中之一是遠程處理,另一個被稱爲MethodDef s的實際運行時間的方法,元數據結構的映射內部已知一個MethodDescThis 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。通過調用PropertyInfo.GetValue,我們通過其中一個Associate MethodDesc指向底層方法實現的指針,遠程處理會執行一些指針數學操作,以便在通道的另一側獲得正確的MethodDesc。 CLR代碼在這裏非常複雜,我沒有足夠的MethodTable的內存佈局經驗,它包含遠程處理使用的這些MethodDesc記錄(或者它用於獲取MethodTable的映射?), D說這是一個公平的猜測,遠程處理是通過一些錯誤的指針數學抓錯了MethodDesc。這就是爲什麼我們看到了類似但不相關的(只要你的程序)MethodDesc - UInt32 get_SizeIVector<T>被調用上的呼叫:

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

這是一個非常有趣的CLR的錯誤,它的一些膽量都出現在事故。您可以從堆棧跟蹤中知道它正試圖調用VectorToCollectionAdapter的Count屬性。

這個類比較特殊,沒有創建它的實例。它是在.NET 4.5中添加的語言投影的一部分,它使WinRT接口類型看起來像.NET Framework類型。它與SZArrayHelper類非常相似,它是一個適配器類,它幫助實現非泛型數組實現泛型接口類型的錯覺,如IList<T>

這裏的工作接口映射是用於WinRT IVector<T>接口。正如在MSDN文章中指出的那樣,該接口類型被映射到IList<T>。內部VectorToListAdapter類負責IList<T>成員,VectorToCollectionAdapter處理ICollection<T>成員。

您的代碼強制CLR查找ICollection的實現> .Count,它可以是一個.NET類,像普通實現它,或者它可以是一個WinRT對象,它將它公開爲IVector <> .Size。很明顯,你創建的代理給它一個頭痛的問題,它錯誤地決定了WinRT的版本。

假設找出哪個是正確的選擇是非常模糊的。畢竟,您的代理可能是是實際WinRT對象的代理,然後它所做的選擇是正確的。這可能是一個結構性問題。它的行爲如此隨機,代碼在64位模式下工作,並不令人鼓舞。 VectorToCollectionAdapter非常危險,請注意JitHelpers。UnsafeCast調用,這個bug有潛在的可利用性。

那麼,警告當局,在connect.microsoft.com上提交一個錯誤報告。如果你不想花時間,我會照顧它,讓我知道。解決方法很難,使用以WinRT爲中心的TypeInfo類來完成反射沒有任何區別。消除抖動強迫因此它運行在64位模式是一種創可貼,但幾乎不能保證。

4

我們目前黑客解決這個問題,這個脆弱的干預(道歉代碼):這裏

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的對象實例的代理包裝。

m_baseInterface是對象被用作的接口。

該代碼攔截在VectorToListAdapter和VectorToCollectionAdapter中所做的調用,並通過該methodMap字典將其轉換回原始。

條件的組成部分:

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

確保只截獲來自東西在System.Runtime.InteropServices.WindowsRuntime命名空間中調用 - 理想,我們會直接針對類型,但它們都無法訪問 - 這應該更改爲在命名空間中定位特定的類名稱。

然後將這些參數轉換爲適當的類型並調用該方法。參數轉換似乎是必需的,因爲傳入參數類型基於方法調用的參數類型 System.Runtime.InteropServices.WindowsRuntime命名空間中的對象,而不是方法調用的參數原始對象類型;即System.Runtime.InteropServices.WindowsRuntime命名空間中的對象之前的原始類型劫持該機制。

例如,WindowsRuntime的東西攔截原來的調用get_Item,並將其轉換爲對Indexer_Get方法的調用:http://referencesource.microsoft.com/#mscorlib/system/runtime/interopservices/windowsruntime/vectortolistadapter.cs,de8c78a8f98213a0,references。然後這個方法用不同的參數類型調用GetAt成員,然後在我們的對象上調用GetAt(再次使用不同的參數類型) - 這是我們在Invoke()中劫持的調用,並將它轉換回原始方法調用原始參數類型。

能夠反映VectorToListAdapter和VectorToCollectionAdapter以提取它們的所有方法和它們所做的嵌套調用會很好,但這些類不幸被標記爲內部。

這適用於我們這裏,但我相信它充滿了漏洞 - 這是一個試錯的例子,運行它來查看失敗,然後添加所需的字典條目/參數轉換。我們正在繼續尋找更好的解決方案。

HTH

相關問題