發信人: 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。