Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
.NET Framework v4.0和VisualStudio 2010 Beta1已经出来有阵子了,估计有些喜欢尝鲜的朋友已经下载试用了。这一次发布包含了大量的新功能。我们上海CLR开发团队会编写一系列的文章介绍Interop的相关新功能。我来给大家简单介绍一下Stub Method Redirection功能。这个功能是CLR上海开发团队设计、开发并测试的新功能之一,这一次我们上海CLR小组共开发了下面几个功能
1. Managed TlbImp (Rewrite)
2. Stub Method Redirection
3. IL Stub ETW Diagnostics
4. Custom QueryInterface
而在CodePlex上面:
1. 发布了TlbImp的最新版本,包括基于规则的Customization(具体可以参考:这一篇)
2. 即将发布IL Stub Diagnostics Tool,可以方便大家直接观看IL Stub,内部使用IL Stub ETW Diagnostics新功能实现
除此之外,还有一些功能是由美国团队开发的:
1. NO PIA
2. IL Stub Everywhere
3. Limit Pumping
4. PreferComThanRemoting
除了NOPIA在我之前的文章已经介绍过之外,其他功能我们会陆续写文章介绍。这次我们先介绍Stub Method Redirection。在介绍这个功能之前,有必要先介绍一下相关的背景知识:
什么是IL Stub
大家都知道,在进行Interop调用的时候,CLR会对参数进行转换(也就是所谓的Marshalling),然后再调用到目标函数。这样一个参数转换和Marshalling实际上是一小段Stub(桩代码)来负责的,比如在调用MessageBox的时候,MessageBox_IL_STUB就是负责Marshalling和参数调用的Stub:
当然了,这里的Stub的内容只是一个简单的抽象,实际的内容会比这个复杂一些。在实际情况下,CLR在第一次执行MessageBox的时候,会动态生成MessageBox对应的IL STUB,使用内部的类似于ReflectionEmit的机制直接输出IL代码的Byte Code,然后交给JIT来编译之,比如MessageBox对应的IL Stub是这样子的:
1: .maxstack 6
2: .locals (native int,int32,native int,int32,native int,native int,int32,native int,int32,int32,int32,int32)
3: // Initialize {
4: /*( 0)*/ call native int [mscorlib] System.StubHelpers.StubHelpers::GetStubContext()
5: /*( 1)*/ call void [mscorlib] System.StubHelpers.StubHelpers::DemandPermission(native int)
6: // } Initialize
7: // Marshal {
8: /*( 0)*/ ldc.i4.0
9: /*( 1)*/ stloc.0
10: IL_000c: /*( 0)*/ nop // argument {
11: /*( 0)*/ ldarg.0
12: /*( 1)*/ stloc.1
13: /*( 0)*/ ldc.i4.1
14: /*( 1)*/ stloc.0
15: /*( 0)*/ nop // } argument
16: /*( 0)*/ nop // argument {
17: /*( 0)*/ ldc.i4.0
18: /*( 1)*/ stloc.s 0x4
19: /*( 0)*/ ldarg.1
20: /*( 1)*/ brfalse IL_0037
21: /*( 0)*/ ldarg.1
22: /*( 1)*/ call instance int32 [mscorlib] System.String::get_Length()
23: /*( 1)*/ ldc.i4.2
24: /*( 2)*/ add
25: /*( 1)*/ stloc.3
26: /*( 0)*/ ldc.i4 0x105
27: /*( 1)*/ ldloc.3
28: /*( 2)*/ clt
29: /*( 1)*/ brtrue IL_0037
30: /*( 0)*/ ldloc.3
31: /*( 1)*/ localloc
32: /*( 1)*/ stloc.s 0x4
33: IL_0037: /*( 0)*/ ldc.i4.1
34: /*( 1)*/ ldarg.1
35: /*( 2)*/ ldloc.s 0x4
36: /*( 3)*/ call native int [mscorlib] System.StubHelpers.CSTRMarshaler::ConvertToNative(int32,string,native int)
37: /*( 1)*/ stloc.2
38: /*( 0)*/ ldc.i4.2
39: /*( 1)*/ stloc.0
40: /*( 0)*/ nop // } argument
41: /*( 0)*/ nop // argument {
42: /*( 0)*/ ldc.i4.0
43: /*( 1)*/ stloc.s 0x7
44: /*( 0)*/ ldarg.2
45: /*( 1)*/ brfalse IL_006c
46: /*( 0)*/ ldarg.2
47: /*( 1)*/ call instance int32 [mscorlib] System.String::get_Length()
48: /*( 1)*/ ldc.i4.2
49: /*( 2)*/ add
50: /*( 1)*/ stloc.s 0x6
51: /*( 0)*/ ldc.i4 0x105
52: /*( 1)*/ ldloc.s 0x6
53: /*( 2)*/ clt
54: /*( 1)*/ brtrue IL_006c
55: /*( 0)*/ ldloc.s 0x6
56: /*( 1)*/ localloc
57: /*( 1)*/ stloc.s 0x7
58: IL_006c: /*( 0)*/ ldc.i4.1
59: /*( 1)*/ ldarg.2
60: /*( 2)*/ ldloc.s 0x7
61: /*( 3)*/ call native int [mscorlib] System.StubHelpers.CSTRMarshaler::ConvertToNative(int32,string,native int)
62: /*( 1)*/ stloc.s 0x5
63: /*( 0)*/ ldc.i4.3
64: /*( 1)*/ stloc.0
65: /*( 0)*/ nop // } argument
66: /*( 0)*/ nop // argument {
67: /*( 0)*/ ldarg.3
68: /*( 1)*/ stloc.s 0x8
69: /*( 0)*/ ldc.i4.4
70: /*( 1)*/ stloc.0
71: /*( 0)*/ nop // } argument
72: /*( 0)*/ nop // return {
73: /*( 0)*/ nop // } return
74: // } Marshal
75: // CallMethod {
76: /*( 0)*/ ldloc.1
77: /*( 1)*/ ldloc.2
78: /*( 2)*/ ldloc.s 0x5
79: /*( 3)*/ ldloc.s 0x8
80: /*( 4)*/ call native int [mscorlib] System.StubHelpers.StubHelpers::GetStubContext()
81: /*( 5)*/ ldc.i4.s 0x30
82: /*( 6)*/ add
83: /*( 5)*/ ldind.i
84: /*( 5)*/ ldind.i
85: /*( 5)*/ calli unmanaged stdcall int32(int32,native int,native int,int32)
86: // } CallMethod
87: // UnmarshalReturn {
88: /*( 1)*/ nop // return {
89: /*( 1)*/ stloc.s 0xa
90: /*( 0)*/ ldc.i4.5
91: /*( 1)*/ stloc.0
92: /*( 0)*/ ldloc.s 0xa
93: /*( 1)*/ stloc.s 0x9
94: /*( 0)*/ ldloc.s 0x9
95: /*( 1)*/ nop // } return
96: /*( 1)*/ stloc.s 0xb
97: // } UnmarshalReturn
98: // Unmarshal {
99: /*( 0)*/ nop // argument {
100: /*( 0)*/ nop // } argument
101: /*( 0)*/ nop // argument {
102: /*( 0)*/ nop // } argument
103: /*( 0)*/ nop // argument {
104: /*( 0)*/ nop // } argument
105: /*( 0)*/ nop // argument {
106: /*( 0)*/ nop // } argument
107: /*( 0)*/ leave IL_00b3
108: IL_00b3: /*( 0)*/ ldloc.s 0xb
109: /*( 1)*/ ret
110: // } Unmarshal
111: // Cleanup {
112: IL_00b6: /*( 0)*/ ldloc.0
113: /*( 1)*/ ldc.i4.1
114: /*( 2)*/ ble IL_00ca
115: /*( 0)*/ ldloc.s 0x4
116: /*( 1)*/ brtrue IL_00ca
117: /*( 0)*/ ldloc.2
118: /*( 1)*/ call void [mscorlib] System.StubHelpers.CSTRMarshaler::ClearNative(native int)
119: IL_00ca: /*( 0)*/ ldloc.0
120: /*( 1)*/ ldc.i4.2
121: /*( 2)*/ ble IL_00df
122: /*( 0)*/ ldloc.s 0x7
123: /*( 1)*/ brtrue IL_00df
124: /*( 0)*/ ldloc.s 0x5
125: /*( 1)*/ call void [mscorlib] System.StubHelpers.CSTRMarshaler::ClearNative(native int)
126: IL_00df: /*( 0)*/ endfinally
127: // } Cleanup
128: .try IL_000c to IL_00b3 finally handler IL_00b6 to IL_00e0
129:
可以看到IL代码非常多,这些都是CLR内部自动生成的。因为看到这些代码有助于开发者理解内部工作原理和找到错误(一般来说是开发者本身的问题,比如MarshalAs写错了),我们将发布一个工具可以让你看到IL Stub具体内容,底层是通过调用另外一个CLR V4 Interop的新功能:IL Stub ETW Diagnostics实现的,以后有机会我会写另外一篇文章介绍。至于IL代码本身的相关内容可以参考Experts IL Assembler和Common Language Infrastructure Annotated Standard.
总的来说,一般的IL Stub总要负责下面几件事情:
1. 安全检查
2. 参数转换,包括返回值
3. 调用目标函数,检查返回值,可能会抛出异常
4. 清理临时内存
其实还有一些其他细节问题如切换GC模式等,建立Frame等等,但是这些属于CLR内部细节问题,这里不再赘述。
IL Stub的问题
IL Stub目前为止都工作的很好。其实,CLR内部本来不是所有情况下都是用IL Stub,2.0以前还存在所谓的ML Stub (Marshalling Language),专门工作在x86下,IL则是工作在x64和IA-64上,后来美国团队将之整合,现在就只有IL Stub了。看起来现在的IL Stub就足够了,不过事实上我们认为ILStub仍然存在一些问题:
1. 无法调试
a. 目前VS暂时不支持调试IL代码
b. 即使可以调试,绝大多数开发者根本不熟悉IL代码
c. IL代码是动态生成,增大了调试支持实现的难度
d. 较难通过工具直接看到(我们即将发布新工具支持看到IL Stub)
2. 不够灵活
a. IL Stub是CLR根据内置规则生成(也就是MarshalAs那一套),开发者无法加入新的规则
b. 开发者无法使用自己的Stub来替换ILStub
3. 组件化和维护性:CLR有大量生成IL Stub的代码,这些代码非常复杂,规则繁多,大大增加了CLR的复杂度,而且本身是由C++写成,较难维护
我们的Vision
既然IL Stub本身有这么多问题,那么我们应该如何解决这些问题呢?在开发Stub Method Redirection新功能之前,我们Team内部有一些讨论,达成的共识如下:
1. CLR只支持最简单的calli调用本地代码
2. IL Stub由编译时刻工具生成:ILStubGen.exe
a. 工具内置数据转换规则
b. 用户可通过插件自定义
3. 生成的IL Stub通过calli调用本地代码
4. Interop类型和Stub直接嵌入在目标程序中:NO PIA是朝这个方向的正确一步
5. CLR运行时刻加载IL Stub:Stub Method Redirection支持该功能
可以看到,按照如上的方法,CLR可以完全从生成IL stub的任务中解放出来,IL Stub的生成也从动态(运行时)转为静态(编译时),并且可以用C#编写,解决了调试、性能、组件化,维护性的众多问题。为了实现这个美好的Vision,有很多工作要做,而且这些工作显然没法在一个Release之内完成,因此我们采取的方法是迭代渐进式的。也就是说,每个Release都会添加一些功能,和这个Vision更加接近。这个Release,我们做的就是NO PIA,以及Stub Method redirection(的一部分)。
Stub Method Redirection
所谓Stub Method,也就是用户编写的编译时刻决定的Stub,可以用任意语言编写,CLR在运行时刻不会动态生成IL Stub,而是会使用用户自定义的Stub,而实现这个的秘诀就是:
ManagedToNativeComInteropStubMethodAttribute
这个Attribute有两个参数:
1. Type:Stub Method所位于的类
2. Name:Stub Method的名称。虽然我们也想实现所谓的methodof功能(类似typeof),但是让C#在4.0中替我们加上这个功能不是太现实,因此我们就先使用名字来查找,速度稍慢,但是因为相关查找只用进行一次,而且可以通过NGEN来避免查找(NGEN来负责查找然后把查找结果直接写入本地代码中),因此速度上不存在问题。
一旦在接口(非接口不可以)的某个方法上面添加上这个Attribute,CLR就知道根据这个Attribute来找Stub,而非自己生成。
用户可以通过这个功能做下面的事情:
1. 编写自己的Stub
a. 加以优化(比如内存池之类的)
b. 提供自定义的类型转换
2. 编写第三方工具自己生成Stub(不过一般来讲这个会是由CLR和.NET Framework提供)
任何编写的Stub Method必须满足下面这些要求:
1. 必须是静态
2. 第一个参数是接口类型
3. 其他参数和对应接口方法完全一致
4. 必须和对应接口位于同一个Assembly,这既是简化,也符合我们的Vision
5. 必须满足访问性要求:从接口的方法必须可以访问到Stub,这个和逻辑上的调用顺序是一致的
6. 不可以是generic
一旦不满足要求,CLR在执行方法的时候会抛出异常,比如:
这个信息是我和PM MM讨论数次之后决定的,目的是让其尽量清晰。
对于一个Stub Method来讲,通常的格式是这样子的:
1: class FooStubClass
2: {
3: internal static void ForwardFooStub(IFoo thisObject, string arg)
4: {
5: try{
6: // Step 1: 托管参数转换到非托管参数(In)
7: // Step 2: 获得调用目标函数的地址
8: // Step 3: 通过Delegate调用目标函数
9: // Step 4: 非托管参数转换到托管参数(Out)
10: // Step 5: 转换返回值
11: }
12: finally
13: {
14: // Step 6: 清理工作
15: }
16: }
17: }
18:
下面分别解释一下:
1. 托管参数转换到非托管参数(In):一般这里调用Marshal的对应函数来进行转换,比如Marshal.StringToBSTR
2. 获得调用目标函数的地址:这个稍微复杂一点,注意因为是COM,所以需要通过虚函数表来获得:
1: //
2: // Get interface pointer
3: //
4: IntPtr pIntf = Marshal.GetComInterfaceForObject(_this, typeof(IFoo));
5:
6: //
7: // Get target
8: //
9: IntPtr pTarget = IntPtr.Zero;
10:
11: unsafe
12: {
13: void** pVtbl = *(void***)pIntf;
14: pTarget = new IntPtr(*(pVtbl + 7)); // IUnknown => 3, IDispatch => 4
15: }
16:
比如上面的代码就获得了_this的IFoo指针,然后获取了虚函数表第八项(跳过IUnknown3个函数,IDispatch 4个函数)作为函数指针
3. 通过Delegate调用目标函数:这一步骤需要首先调用Marshal.GetDelegateForFunctionPointer获得函数指针对应的Delegate,注意Delegate的参数必须得是对应非托管的类型,比如MessageBox对应的delgate是(IntPtr, IntPtr, IntPtr, int),然后再调用delegate,传入参数
4. 非托管参数转换到托管参数(Out):转换的时候既要包括IN也要包括OUT,比如[in, out]char []这种情况,必须两种方向都要照顾到,IN在调用之前转换,而OUT则是在调用之后转换
5. 转换返回值:这个没太多好说的,和OUT比较类似
6. 清理工作:转换不要忘记清理中间生成的临时数据,比如string转换到char *需要调用Marshal.StringToCoTaskMemAnsi转换,之后调用Marshal.FreeCoTaskMem释放,释放则是在Cleanup中作
最后是一个完整的例子:
1: Using System;
2: using System.Collections.Generic;
3: using System.Linq;
4: using System.Text;
5: using System.Runtime.InteropServices;
6: using System.Runtime.CompilerServices;
7:
8: namespace StubMethodDemo
9: {
10: [ComImport]
11: [Guid("0741BD5F-549A-46FD-A857-0E3B23620399")]
12: interface IFoo
13: {
14: [MethodImplAttribute(MethodImplOptions.InternalCall)]
15: [ManagedToNativeComInteropStubAttribute(typeof(FooStubClass), "IFoo_Hello_Stub")]
16: void Hello(string name);
17: }
18:
19: [ComImport]
20: [Guid("68389CF3-212B-449D-83CB-0DD4572FEF03")]
21: class Foo : IFoo
22: {
23: [MethodImplAttribute(MethodImplOptions.InternalCall)]
24: public extern void Hello(string name);
25: }
26:
27: class FooStubClass
28: {
29: public delegate int IFoo_Hello_Delegate(IntPtr _this, IntPtr a);
30:
31: public void IFoo_Hello_Stub(IFoo _this, string name)
32: {
33: IntPtr nativeArg_name = IntPtr.Zero;
34:
35: try
36: {
37: //
38: // Marshal CLR => Native
39: //
40: nativeArg_name = Marshal.StringToBSTR(name);
41:
42: //
43: // Get interface pointer
44: //
45: IntPtr pIntf = Marshal.GetComInterfaceForObject(_this, typeof(IFoo));
46:
47: //
48: // Get target
49: //
50: IntPtr pTarget = IntPtr.Zero;
51:
52: unsafe
53: {
54: void** pVtbl = *(void***)pIntf;
55: pTarget = new IntPtr(*(pVtbl + 7)); // IUnknown => 3, IDispatch => 4
56: }
57:
58: //
59: // Make the call
60: //
61: Delegate dele = Marshal.GetDelegateForFunctionPointer(pTarget, typeof(IFoo_Hello_Delegate));
62: IFoo_Hello_Delegate targetDelegate = (IFoo_Hello_Delegate)dele;
63: int hr = targetDelegate(pIntf, nativeArg_name);
64: if (hr < 0)
65: Marshal.ThrowExceptionForHR(hr);
66:
67: //
68: // Marshal Native => CLR
69: //
70:
71: //
72: // Marshal return
73: //
74: }
75: finally
76: {
77: //
78: // Cleanup
79: //
80: if (nativeArg_name != IntPtr.Zero)
81: Marshal.FreeBSTR(nativeArg_name);
82: nativeArg_name = IntPtr.Zero;
83: }
84: }
85: }
86:
87: class Program
88: {
89: static void Main(string[] args)
90: {
91: Foo myFoo = new Foo();
92: myFoo.Hello("Foo!");
93: }
94: }
95: }
96:
作者:张羿
转载请注明出处