发信人: flier (小海 [渴望并不存在的完美]), 信区: DotNET
标 题: 用WinDbg探索CLR世界 [10] 透明代理实现原理浅析
发信站: BBS 水木清华站 (Sat Oct 16 22:15:56 2004), 站内
原文:http://www.blogcn.com/User8/flier_lu/blog/4290857.html
在 CLR 世界中最神奇的一族类型应该就是 TransparentProxy/RealProxy (TP/RP) 这一对孪生兄弟,以及和他们相随左右的 MarshalByRefObject (MBRO) 和 ContextBoundObject (CBO) 等等。无论是本地跨 AppDomain 调用还是 Remoting,无论是基于 Context 的 AOP 实现还是企业级 COM+ 对象 (ServicedComponent),无不活跃着 TP/RP 的身影。而与尚有少许文档的 RP、MBRO、CBO 不同,TP 是完全基于 CLR 内部实现的全动态类型,在 BCL 耀眼光芒背后的影子中默默无闻的起着无法替代的重要作用。好在通过 cbrumme 的深入介绍文章 TransparentProxy,以及使用 WinDbg/Rotor 的探索,能让我们从不同侧面了解这个幕后英雄。
注:本文的目标是介绍透明代理的实现原理,不涉及其使用方法,对其使用感兴趣的朋友可参看 Don Box 的《.NET 本质论》第 7 章:高级方法;或者阅读 MSDN 中相关文章,如 Juval Lowy 的 CONTEXTS IN .NET Decouple Components by Injecting Custom Services into Your Object's Interception Chain 一文。
对大多数 CLR 对象来说,指向他们的引用直接保存着其实现的内存地址,这样的使用效率最高,而且 GC 也可以很容易对其生命周期进行跟踪,并在进行垃圾回收和堆栈压缩时更新对象引用。不过对于从 MarshalByRefObject (MBRO) 类型继承出来的类型,对象的实例很可能根本不在本地,所有的访问可能都是通过代理向远端的服务器发送的。而在本地调用和远端实现之间起到转发作用的代理,就是本文的主角 TP/RP。
这里的 MarshalByRefObject (MBRO) 与其说是一个用于实现继承的基类,不如说是一个标记用的接口继承抽象类。就如cbrumme 在 Inheriting from MarshalByRefObject 一文中介绍的,强制要求支持远程调用的对象从 MBRO 继承,更多是出于性能方面的考虑。CLR 在处理非 MBRO 对象时,会使用各种优化方法,如内联方法、直接访问对象字段等等;但对于 MBRO 对象,考虑到需要通过透明代理与远端实现交互,则大部分优化会被停用。不过个人觉得这种说法过于牵强了,通过接口和特性 (Attribute) 完全可以达到类似的效果,只不过用 MBRO 的模式实现可能更简单一些。好在如果实在有什么无法从 MBRO 继承的需求,还可以通过 Adapter/Bridge 等模式层面的方法来解决。
与标准 Proxy 模式中的独立代理类不同,CLR 中对代理模式的实现思路是与 Java 的动态代理实现类似的接口/实现分离模型。代理实现者从 RealProxy (RP) 对象继承出真实代理,并实现 Invoke 方法完成实际的对象方法调用;而代理使用者则通过与 RP 绑定的 TransparentProxy (TP),以与实际被代理对象完全等同的方式访问 TP 的接口。在 Java 从 1.3 开始提供的动态代理支持机制中,与之对应的是实现 InvocationHandler 接口的 RP 类,并由工具类 Proxy 从 RP 中动态生成 TP 类。只不过 Java 中一般是通过动态生成 bytecode 提供代理支持,而 CLR 则通过内建支持完成,效率更高且功能更完整。对 Java 的动态代理感兴趣的朋友可以参看 Bob Tarr 的 Dynamic Proxies In Java 一文。
这样的接口/实现分离模型虽然会受到一定程度的性能损失,如需要实例化 TP/RP 两个对象,而且所有调用都需要通过“对象引用 -> TP -> RP -> 实际对象” 的流程实现。但因为 TP/RP 所扮演的角色具有较大的区别,这种损失还是值得的。
TP 的目标是在 CLR 中在 IL 层面最大程度扮演被代理的远端对象,从类型转换到类型获取,从字段访问到方法调用。对 CLR 的使用者来说 TP 和被其代理的对象完全没有任何区别,只有通过 RemotingServices.IsTransparentProxy 才能区分两者的区别。
RP 则是提供给 CLR 使用者扩展代理机制的切入点,通过从 RP 继承并实现 Invoke 方法,用户自定义代理实现可以自由的处理已经被从栈调用转换为消息调用的目标对象方法调用,如实现缓存、身份验证、安全检测、延迟加载等等。例如 .NET Remoting 的架构就是建立在这个基础上的,通过 RemotingProxy 实现基于 Channel 的远程调用。
要理解这些感念并分析其实现,需要考察一个实际的例子。示例代码中使用通常的模式,定义了一个 ICalculator 接口和一个从 MBRO 继承出来的参考实现类型,如下:
public interface ICalculator
{
int add(int l, int r);
int dec(int l, int r);
}
public class CalculatorImpl : MarshalByRefObject, ICalculator
{
public int add(int l, int r)
{
return l + r;
}
public int dec(int l, int r)
{
return l - r;
}
}
对这种接口和对象的使用模式我们已经非常熟悉,因此可以编写一个简单的测试用例来验证对象实现接口的有效性,如
[TestFixture]
public class ProxyTester
{
private void doTest(Object obj)
{
Assert.IsTrue(typeof(ICalculator).IsAssignableFrom(obj.GetType()));
Assert.IsTrue(obj is ICalculator);
Assert.IsNotNull(obj as ICalculator);
ICalculator calc = (ICalculator)obj;
Assert.IsNotNull(calc);
}
[Test]
public void testImpl()
{
doTest(new CalculatorImpl());
}
}
在这个测试用例中,我们通过断言确认传入对象必须是实现了 ICalculator 接口,并按照定义实现提供了接口的实现。并在实际测试中将参考实现的实例传入测试方法。毫无疑问这个测试是直接通过的。
而如果要进一步使用代理,则需要构建一个 RP 的实现类,如
public class TestProxy : RealProxy
{
private MarshalByRefObject _target;
public TestProxy(Type classToProxy, MarshalByRefObject target)
: base(classToProxy)
{
_target = target;
}
public override IMessage Invoke(IMessage msg)
{
throw new NotImplementedException();
}
}
[TestFixture]
public class ProxyTester
{
[Test]
public void testProxy()
{
RealProxy rp = new TestProxy(typeof(ICalculator), new CalculatorImpl());
Object tp = rp.GetTransparentProxy();
Assert.IsTrue(RemotingServices.IsTransparentProxy(tp));
Assert.IsFalse(rp == tp);
Assert.IsFalse(rp.Equals(tp));
doTest(tp);
}
}
testProxy 测试方法中演示了如何通过自定义 RP 构造并返回 TP。而将返回的 TP 对象传入测试方法后,我们获得了一个 NotImplementedException 异常。
以下为引用:
ProxyDemo.ProxyTester.testProxy : System.NotImplementedException : 未实现该方法或操作。
at ProxyDemo.TestProxy.Invoke(IMessage msg) in f:\study\dotnet\proxydemo\entrypoint.cs:line 42
at System.Runtime.Remoting.Proxies.RealProxy.PrivateInvoke(MessageData& msgData, Int32 type)
at System.Object.GetType()
at ProxyDemo.ProxyTester.doTest(Object obj) in f:\study\dotnet\proxydemo\entrypoint.cs:line 51
at ProxyDemo.ProxyTester.testProxy() in f:\study\dotnet\proxydemo\entrypoint.cs:line 80
通过调用堆栈我们可以看到,在 doTest 方法调用 GetType 时,TestProxy.Invoke 方法被调用并抛出异常。而通过 testProxy 中的断言可以确认,被作为接口调用的时 TP 对象而非最终实现的 RP,也就是说 TP 上的调用被转发到了 RP 上。
而在提供了一个简单的 Invoke 方法实现后,TestProxy 的代码通过了测试,代码如下:
public class TestProxy : RealProxy
{
private MarshalByRefObject _target;
public TestProxy(Type classToProxy, MarshalByRefObject target)
: base(classToProxy)
{
_target = target;
}
public override IMessage Invoke(IMessage msg)
{
if(msg is IConstructionCallMessage)
{
IConstructionCallMessage ctor = msg as IConstructionCallMessage;
RealProxy rp = RemotingServices.GetRealProxy(_target);
MarshalByRefObject tp = (MarshalByRefObject)this.GetTransparentProxy();
rp.InitializeServerObject(ctor);
return EnterpriseServicesHelper.CreateConstructionReturnMessage(ctor, tp);
}
else
{
return RemotingServices.ExecuteMessage(_target, msg as IMethodCallMessage);
}
}
}
所有 RP 需要做得事情,就是分别将对象构造和方法调用两类消息,路由到相应的辅助处理函数去。这里的辅助函数的使用和实现,因为不影响本文分析故而暂且略过,回头有空我再单独写篇文章讨论一下 TP/RP 以及 MBRO/CBO 的使用和实现原理。
下面我们开始分析一下这个简单但完整的 TP/RP 实现的背后,CLR 是如何让这个无中生有的 TP 通过 doTest 方法所有的断言的。
首先我来看看 TP 是如何被创建和获取的。在实例化 RP 后,通过 RealProxy.GetTransparentProxy 方法可以获得与 RP 实例绑定的 TP 实例。而这一实例是在 RP 被创建时同步创建的:
namespace System.Runtime.Remoting.Proxies
{
abstract public class RealProxy
{
protected RealProxy(Type classToProxy) : this(classToProxy, (IntPtr)0, null)
{
}
protected RealProxy(Type classToProxy, IntPtr stub, Object stubData)
{
if(!classToProxy.IsMarshalByRef && !classToProxy.IsInterface)
throw new ArgumentException();
if((IntPtr)0 == stub)
{
stub = _defaultStub;
stubData = _defaultStubData;
}
_tp = null;
if (stubData == null)
throw new ArgumentNullException("stubdata"[img]/images/wink.gif[/img];
_tp = RemotingServices.CreateTransparentProxy(this, classToProxy, stub, stubData);
}
public virtual Object GetTransparentProxy()
{
return _tp;
}
}
}
可以看到 TP 是在 RP 构造函数中,调用 RemotingServices.CreateTransparentProxy 方法动态创建的,而 RP 的构造函数起到获取并验证构造 TP 参数的作用。
RemotingServices.CreateTransparentProxy 方法将把被代理的类型强制转换为统一的由 CLR 在运行时创建的 RuntimeType 类型,进而调用 Internal 方法完成 TP 的创建。关于运行时类型信息和 Interanl 方法的使用和实现,可以参看我另外两篇文章《Type, RuntimeType and RuntimeTypeHandle》和《用WinDbg探索CLR世界 [8] InternalCall 的使用与实现》。
namespace System.Runtime.Remoting
{
public sealed class RemotingServices
{
[MethodImplAttribute(MethodImplOptions.InternalCall)]
internal static extern Object CreateTransparentProxy(RealProxy rp, RuntimeType typeToProxy, IntPtr stub, Object stubData);
internal static Object CreateTransparentProxy(RealProxy rp, Type typeToProxy, IntPtr stub, Object stubData)
{
RuntimeType rTypeToProxy = typeToProxy as RuntimeType;
if (rTypeToProxy == null)
throw new ArgumentException();
return CreateTransparentProxy(rp, rTypeToProxy, stub, stubData);
}
}
}
在 CLR 中实现创建 TP 的 CRemotingServices::CreateTransparentProxy 函数 (vm/remoting.cpp:381) 会进一步调用 CTPMethodTable::CreateTPOfClassForRP 函数创建一个新的 CTPMethodTable 实例,并将参数中传入的 stub/stubData 绑定在此 TPMT 实例上。因为在 CLR 这个层面,每个类型实际上都是由一个 MethodTable (MT) 实例描述的,对类型的操作最终也都落实到对 MT 的操作上。
关于 MT 的讨论,可以参看我另外几篇文章:
用WinDbg探索CLR世界 [3] 跟踪方法的 JIT 过程
用WinDbg探索CLR世界 [4] 方法的调用机制之静态结构
用WinDbg探索CLR世界 [4] 方法的调用机制之动态分析 - 上
用WinDbg探索CLR世界 [4] 方法的调用机制之动态分析 - 下
为增加直观印象,我们先通过 SOS 调试扩展看看运行时的 TP 是什么样子的,如首先可以看看 RP 实现类 TestProxy 的细节:
以下为引用:
.load sos
!Name2EE ProxyDemo.exe ProxyDemo.TestProxy
--------------------------------------
MethodTable: 009b5334
EEClass: 02f536e0
Name: ProxyDemo.TestProxy
!DumpClass 02f536e0
Class Name : ProxyDemo.TestProxy
mdToken : 02000004 (F:\Study\DotNet\ProxyDemo\bin\Debug\ProxyDemo.exe)
Parent Class : 02cad798
ClassLoader : 0017bda8
Method Table : 009b5334
Vtable Slots : e
Total Method Slots : f
Class Attributes : 100001 :
Flags : 3000023
NumInstanceFields: 5
NumStaticFields: 0
ThreadStaticOffset: 0
ThreadStaticsSize: 0
ContextStaticOffset: 0
ContextStaticsSize: 0
FieldDesc*: 009b52e0
MT Field Offset Type Attr Value Name
009d9810 400131b 4 CLASS instance _tp
009d9810 400131c 8 CLASS instance _identity
...
注意这里 TestProxy 从 RealProxy 继承而来的 _tp 字段,保存的就是在构造函数中同步建立的 TP 实例。我们可以进一步通过 DumpStackObjects 和 DumpObj 命令查看 RP/TP 实例的内容,如
以下为引用:
!DumpStackObjects
ESP/REG Object Name
esi 12f66c00ae5660 System.Runtime.Remoting.Proxies.__TransparentProxy
edi 12f66c00ae4750 ProxyDemo.TestProxy
0012f670 00ae195c ProxyDemo.ProxyTester
0012f678 00ae4750 ProxyDemo.TestProxy
...
!DumpObj 00ae4750
Name: ProxyDemo.TestProxy
MethodTable 0x009b5334
EEClass 0x02f536e0
Size 28(0x1c) bytes
mdToken: 02000004 (F:\Study\DotNet\ProxyDemo\bin\Debug\ProxyDemo.exe)
FieldDesc*: 009b52e0
MT Field Offset Type Attr Value Name
009d9810 400131b 4 CLASS instance 00ae5660 _tp
009d9810 400131c 8 CLASS instance 00000000 _identity
...
!DumpObj 00ae5660
Name: System.Runtime.Remoting.Proxies.__TransparentProxy
MethodTable 0x7ff5000c
EEClass 0x02cad734
Size 28(0x1c) bytes
mdToken: 020004db (e:\windows\microsoft.net\framework\v1.1.4322\mscorlib.dll)
FieldDesc*: 009d9330
MT Field Offset Type Attr Value Name
009d939c 4001488 4 CLASS instance 00ae4750 _rp
009d939c 4001489 8 CLASS instance 00ae2470 _stubData
009d939c 400148a c System.Int32 instance 009d643c _pMT
009d939c 400148b 10 System.Int32 instance 009b51f0 _pInterfaceMT
009d939c 400148c 14 System.Int32 instance 793427c7 _stub
可以看到 TP/RP 实例分别通过 _tp/_rp 字段互相引用,保持双向绑定的有效性。而 TP 实例则是一个 __TransparentProxy 类型的实例,在 _pMT 和 _pInterfaceMT 中分别保存了被代理类型的 MT 及其接口 MT。
以下为引用:
!DumpMT 0x9d643c
EEClass : 02cacd24
Module : 0015ec40
Name: System.MarshalByRefObject
mdToken: 0200002c (e:\windows\microsoft.net\framework\v1.1.4322\mscorlib.dll)
MethodTable Flags : 20c0000
Number of IFaces in IFaceMap : 0
Interface Map : 009d64b4
Slots in VTable : 19
!DumpMT 0x9b51f0
EEClass : 02f53618
Module : 0017c110
Name: ProxyDemo.ICalculator
mdToken: 02000002 (F:\Study\DotNet\ProxyDemo\bin\Debug\ProxyDemo.exe)
MethodTable Flags : 80000
Number of IFaces in IFaceMap : 0
Interface Map : 009b5224
Slots in VTable : 2
值得注意的是 TP 实例 _pMT 指向的方法表是 MBRO 而非 RP,只是在其 _pInterfaceMT 中缓存了 RP 实际代理类型的接口。
如果说这个 __TransparentProxy 类型对象与其他普通对象有什么区别的话,那就是他的 MT 并非是从静态 Metadata 加载,而是由前面所述的 RemotingServices.CreateTransparentProxy 方法动态构造的,因此并没有普通 MT 的 MD 名字信息,如
以下为引用:
!DumpMT -MD 0x7ff5000c
EEClass : 02cad734
Module : 0015ec40
Name: System.Runtime.Remoting.Proxies.__TransparentProxy
mdToken: 020004db (e:\windows\microsoft.net\framework\v1.1.4322\mscorlib.dll)
MethodTable Flags : 30c0000
Number of IFaces in IFaceMap : 0
Interface Map : 009d93dc
Slots in VTable : 5
--------------------------------------
MethodDesc Table
Entry MethodDesc JIT Name
7ff40010 7ff40015 None
7ff4001a 7ff4001f None
7ff40024 7ff40029 None
7ff4002e 7ff40033 None
7ff40038 7ff4003d None
而其对象字段也并非从父类继承而来。DumpObj 命令返回的字段信息第一个域 MT 表示此字段从哪个类型继承而来,考察前面 TestProxy 和 __TransparentProxy 实例的对象信息,如
以下为引用:
!DumpObj 00ae4750
Name: ProxyDemo.TestProxy
MethodTable 0x009b5334
EEClass 0x02f536e0
Size 28(0x1c) bytes
mdToken: 02000004 (F:\Study\DotNet\ProxyDemo\bin\Debug\ProxyDemo.exe)
FieldDesc*: 009b52e0
MT Field Offset Type Attr Value Name
009d9810 400131b 4 CLASS instance 00ae5660 _tp
...
!DumpMT 009d9810
EEClass : 02cad798
Module : 0015ec40
Name: System.Runtime.Remoting.Proxies.RealProxy
mdToken: 02000479 (e:\windows\microsoft.net\framework\v1.1.4322\mscorlib.dll)
MethodTable Flags : 2040000
Number of IFaces in IFaceMap : 0
Interface Map : 009d98f0
Slots in VTable : 42
!DumpObj 00ae5660
Name: System.Runtime.Remoting.Proxies.__TransparentProxy
MethodTable 0x7ff5000c
EEClass 0x02cad734
Size 28(0x1c) bytes
mdToken: 020004db (e:\windows\microsoft.net\framework\v1.1.4322\mscorlib.dll)
FieldDesc*: 009d9330
MT Field Offset Type Attr Value Name
009d939c 4001488 4 CLASS instance 00ae4750 _rp
...
!DumpMT 009d939c
009d939c is not a MethodTable
后面解说 TP 实现原理时会详细这些区别,相应的 __TransparentProxy 对象的 MT 中的 MD 值也并非普通的 MD,如
以下为引用:
!DumpMT -MD 0x7ff5000c
EEClass : 02cad734
Module : 0015ec40
Name: System.Runtime.Remoting.Proxies.__TransparentProxy
mdToken: 020004db (e:\windows\microsoft.net\framework\v1.1.4322\mscorlib.dll)
MethodTable Flags : 30c0000
Number of IFaces in IFaceMap : 0
Interface Map : 009d93dc
Slots in VTable : 5
--------------------------------------
MethodDesc Table
Entry MethodDesc JIT Name
7ff40010 7ff40015 None
7ff4001a 7ff4001f None
7ff40024 7ff40029 None
7ff4002e 7ff40033 None
7ff40038 7ff4003d None
!DumpMD 7ff40015
7ff40015 is not a MethodDesc
至此,我们已经对 TP/RP 的静态和动态实现有了一个大致的影响。接下来将从静态和动态两个层面,分析 TP 的实现原理,以及 CLR 如何使用 TP。