2015-09-02 48 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

相关问题