CLR 4.0 Beta1新功能:Stub Method Redirection

刺骨的言语ヽ痛彻心扉 2022-08-06 11:06 72阅读 0赞

.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:

clip\_image002

当然了,这里的Stub的内容只是一个简单的抽象,实际的内容会比这个复杂一些。在实际情况下,CLR在第一次执行MessageBox的时候,会动态生成MessageBox对应的IL STUB,使用内部的类似于ReflectionEmit的机制直接输出IL代码的Byte Code,然后交给JIT来编译之,比如MessageBox对应的IL Stub是这样子的:

  1. 1: .maxstack 6
  2. 2: .locals (native int,int32,native int,int32,native int,native int,int32,native int,int32,int32,int32,int32)
  3. 3: // Initialize {
  4. 4: /*( 0)*/ call native int [mscorlib] System.StubHelpers.StubHelpers::GetStubContext()
  5. 5: /*( 1)*/ call void [mscorlib] System.StubHelpers.StubHelpers::DemandPermission(native int)
  6. 6: // } Initialize
  7. 7: // Marshal {
  8. 8: /*( 0)*/ ldc.i4.0
  9. 9: /*( 1)*/ stloc.0
  10. 10: IL_000c: /*( 0)*/ nop // argument {
  11. 11: /*( 0)*/ ldarg.0
  12. 12: /*( 1)*/ stloc.1
  13. 13: /*( 0)*/ ldc.i4.1
  14. 14: /*( 1)*/ stloc.0
  15. 15: /*( 0)*/ nop // } argument
  16. 16: /*( 0)*/ nop // argument {
  17. 17: /*( 0)*/ ldc.i4.0
  18. 18: /*( 1)*/ stloc.s 0x4
  19. 19: /*( 0)*/ ldarg.1
  20. 20: /*( 1)*/ brfalse IL_0037
  21. 21: /*( 0)*/ ldarg.1
  22. 22: /*( 1)*/ call instance int32 [mscorlib] System.String::get_Length()
  23. 23: /*( 1)*/ ldc.i4.2
  24. 24: /*( 2)*/ add
  25. 25: /*( 1)*/ stloc.3
  26. 26: /*( 0)*/ ldc.i4 0x105
  27. 27: /*( 1)*/ ldloc.3
  28. 28: /*( 2)*/ clt
  29. 29: /*( 1)*/ brtrue IL_0037
  30. 30: /*( 0)*/ ldloc.3
  31. 31: /*( 1)*/ localloc
  32. 32: /*( 1)*/ stloc.s 0x4
  33. 33: IL_0037: /*( 0)*/ ldc.i4.1
  34. 34: /*( 1)*/ ldarg.1
  35. 35: /*( 2)*/ ldloc.s 0x4
  36. 36: /*( 3)*/ call native int [mscorlib] System.StubHelpers.CSTRMarshaler::ConvertToNative(int32,string,native int)
  37. 37: /*( 1)*/ stloc.2
  38. 38: /*( 0)*/ ldc.i4.2
  39. 39: /*( 1)*/ stloc.0
  40. 40: /*( 0)*/ nop // } argument
  41. 41: /*( 0)*/ nop // argument {
  42. 42: /*( 0)*/ ldc.i4.0
  43. 43: /*( 1)*/ stloc.s 0x7
  44. 44: /*( 0)*/ ldarg.2
  45. 45: /*( 1)*/ brfalse IL_006c
  46. 46: /*( 0)*/ ldarg.2
  47. 47: /*( 1)*/ call instance int32 [mscorlib] System.String::get_Length()
  48. 48: /*( 1)*/ ldc.i4.2
  49. 49: /*( 2)*/ add
  50. 50: /*( 1)*/ stloc.s 0x6
  51. 51: /*( 0)*/ ldc.i4 0x105
  52. 52: /*( 1)*/ ldloc.s 0x6
  53. 53: /*( 2)*/ clt
  54. 54: /*( 1)*/ brtrue IL_006c
  55. 55: /*( 0)*/ ldloc.s 0x6
  56. 56: /*( 1)*/ localloc
  57. 57: /*( 1)*/ stloc.s 0x7
  58. 58: IL_006c: /*( 0)*/ ldc.i4.1
  59. 59: /*( 1)*/ ldarg.2
  60. 60: /*( 2)*/ ldloc.s 0x7
  61. 61: /*( 3)*/ call native int [mscorlib] System.StubHelpers.CSTRMarshaler::ConvertToNative(int32,string,native int)
  62. 62: /*( 1)*/ stloc.s 0x5
  63. 63: /*( 0)*/ ldc.i4.3
  64. 64: /*( 1)*/ stloc.0
  65. 65: /*( 0)*/ nop // } argument
  66. 66: /*( 0)*/ nop // argument {
  67. 67: /*( 0)*/ ldarg.3
  68. 68: /*( 1)*/ stloc.s 0x8
  69. 69: /*( 0)*/ ldc.i4.4
  70. 70: /*( 1)*/ stloc.0
  71. 71: /*( 0)*/ nop // } argument
  72. 72: /*( 0)*/ nop // return {
  73. 73: /*( 0)*/ nop // } return
  74. 74: // } Marshal
  75. 75: // CallMethod {
  76. 76: /*( 0)*/ ldloc.1
  77. 77: /*( 1)*/ ldloc.2
  78. 78: /*( 2)*/ ldloc.s 0x5
  79. 79: /*( 3)*/ ldloc.s 0x8
  80. 80: /*( 4)*/ call native int [mscorlib] System.StubHelpers.StubHelpers::GetStubContext()
  81. 81: /*( 5)*/ ldc.i4.s 0x30
  82. 82: /*( 6)*/ add
  83. 83: /*( 5)*/ ldind.i
  84. 84: /*( 5)*/ ldind.i
  85. 85: /*( 5)*/ calli unmanaged stdcall int32(int32,native int,native int,int32)
  86. 86: // } CallMethod
  87. 87: // UnmarshalReturn {
  88. 88: /*( 1)*/ nop // return {
  89. 89: /*( 1)*/ stloc.s 0xa
  90. 90: /*( 0)*/ ldc.i4.5
  91. 91: /*( 1)*/ stloc.0
  92. 92: /*( 0)*/ ldloc.s 0xa
  93. 93: /*( 1)*/ stloc.s 0x9
  94. 94: /*( 0)*/ ldloc.s 0x9
  95. 95: /*( 1)*/ nop // } return
  96. 96: /*( 1)*/ stloc.s 0xb
  97. 97: // } UnmarshalReturn
  98. 98: // Unmarshal {
  99. 99: /*( 0)*/ nop // argument {
  100. 100: /*( 0)*/ nop // } argument
  101. 101: /*( 0)*/ nop // argument {
  102. 102: /*( 0)*/ nop // } argument
  103. 103: /*( 0)*/ nop // argument {
  104. 104: /*( 0)*/ nop // } argument
  105. 105: /*( 0)*/ nop // argument {
  106. 106: /*( 0)*/ nop // } argument
  107. 107: /*( 0)*/ leave IL_00b3
  108. 108: IL_00b3: /*( 0)*/ ldloc.s 0xb
  109. 109: /*( 1)*/ ret
  110. 110: // } Unmarshal
  111. 111: // Cleanup {
  112. 112: IL_00b6: /*( 0)*/ ldloc.0
  113. 113: /*( 1)*/ ldc.i4.1
  114. 114: /*( 2)*/ ble IL_00ca
  115. 115: /*( 0)*/ ldloc.s 0x4
  116. 116: /*( 1)*/ brtrue IL_00ca
  117. 117: /*( 0)*/ ldloc.2
  118. 118: /*( 1)*/ call void [mscorlib] System.StubHelpers.CSTRMarshaler::ClearNative(native int)
  119. 119: IL_00ca: /*( 0)*/ ldloc.0
  120. 120: /*( 1)*/ ldc.i4.2
  121. 121: /*( 2)*/ ble IL_00df
  122. 122: /*( 0)*/ ldloc.s 0x7
  123. 123: /*( 1)*/ brtrue IL_00df
  124. 124: /*( 0)*/ ldloc.s 0x5
  125. 125: /*( 1)*/ call void [mscorlib] System.StubHelpers.CSTRMarshaler::ClearNative(native int)
  126. 126: IL_00df: /*( 0)*/ endfinally
  127. 127: // } Cleanup
  128. 128: .try IL_000c to IL_00b3 finally handler IL_00b6 to IL_00e0
  129. 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)

  1. 不够灵活

a. IL Stub是CLR根据内置规则生成(也就是MarshalAs那一套),开发者无法加入新的规则

b. 开发者无法使用自己的Stub来替换ILStub

  1. 组件化和维护性:CLR有大量生成IL Stub的代码,这些代码非常复杂,规则繁多,大大增加了CLR的复杂度,而且本身是由C++写成,较难维护

我们的**Vision**

既然IL Stub本身有这么多问题,那么我们应该如何解决这些问题呢?在开发Stub Method Redirection新功能之前,我们Team内部有一些讨论,达成的共识如下:

  1. CLR只支持最简单的calli调用本地代码

  2. IL Stub由编译时刻工具生成:ILStubGen.exe

a. 工具内置数据转换规则

b. 用户可通过插件自定义

  1. 生成的IL Stub通过calli调用本地代码

  2. Interop类型和Stub直接嵌入在目标程序中:NO PIA是朝这个方向的正确一步

  3. 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. 提供自定义的类型转换

  1. 编写第三方工具自己生成Stub(不过一般来讲这个会是由CLR和.NET Framework提供)

任何编写的Stub Method必须满足下面这些要求:

  1. 必须是静态

  2. 第一个参数是接口类型

  3. 其他参数和对应接口方法完全一致

  4. 必须和对应接口位于同一个Assembly,这既是简化,也符合我们的Vision

  5. 必须满足访问性要求:从接口的方法必须可以访问到Stub,这个和逻辑上的调用顺序是一致的

  6. 不可以是generic

一旦不满足要求,CLR在执行方法的时候会抛出异常,比如:

clip\_image004

这个信息是我和PM MM讨论数次之后决定的,目的是让其尽量清晰。

对于一个Stub Method来讲,通常的格式是这样子的:

  1. 1: class FooStubClass
  2. 2: {
  3. 3: internal static void ForwardFooStub(IFoo thisObject, string arg)
  4. 4: {
  5. 5: try{
  6. 6: // Step 1: 托管参数转换到非托管参数(In)
  7. 7: // Step 2: 获得调用目标函数的地址
  8. 8: // Step 3: 通过Delegate调用目标函数
  9. 9: // Step 4: 非托管参数转换到托管参数(Out)
  10. 10: // Step 5: 转换返回值
  11. 11: }
  12. 12: finally
  13. 13: {
  14. 14: // Step 6: 清理工作
  15. 15: }
  16. 16: }
  17. 17: }
  18. 18:

下面分别解释一下:

  1. 托管参数转换到非托管参数(In):一般这里调用Marshal的对应函数来进行转换,比如Marshal.StringToBSTR

  2. 获得调用目标函数的地址:这个稍微复杂一点,注意因为是COM,所以需要通过虚函数表来获得:

    1: //

    1. 2: // Get interface pointer
    2. 3: //
    3. 4: IntPtr pIntf = Marshal.GetComInterfaceForObject(_this, typeof(IFoo));
    4. 5:
    5. 6: //
    6. 7: // Get target
    7. 8: //
    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个函数)作为函数指针

  1. 通过Delegate调用目标函数:这一步骤需要首先调用Marshal.GetDelegateForFunctionPointer获得函数指针对应的Delegate,注意Delegate的参数必须得是对应非托管的类型,比如MessageBox对应的delgate是(IntPtr, IntPtr, IntPtr, int),然后再调用delegate,传入参数

  2. 非托管参数转换到托管参数(Out):转换的时候既要包括IN也要包括OUT,比如[in, out]char []这种情况,必须两种方向都要照顾到,IN在调用之前转换,而OUT则是在调用之后转换

  3. 转换返回值:这个没太多好说的,和OUT比较类似

  4. 清理工作:转换不要忘记清理中间生成的临时数据,比如string转换到char *需要调用Marshal.StringToCoTaskMemAnsi转换,之后调用Marshal.FreeCoTaskMem释放,释放则是在Cleanup中作

最后是一个完整的例子:

  1. 1: Using System;
  2. 2: using System.Collections.Generic;
  3. 3: using System.Linq;
  4. 4: using System.Text;
  5. 5: using System.Runtime.InteropServices;
  6. 6: using System.Runtime.CompilerServices;
  7. 7:
  8. 8: namespace StubMethodDemo
  9. 9: {
  10. 10: [ComImport]
  11. 11: [Guid("0741BD5F-549A-46FD-A857-0E3B23620399")]
  12. 12: interface IFoo
  13. 13: {
  14. 14: [MethodImplAttribute(MethodImplOptions.InternalCall)]
  15. 15: [ManagedToNativeComInteropStubAttribute(typeof(FooStubClass), "IFoo_Hello_Stub")]
  16. 16: void Hello(string name);
  17. 17: }
  18. 18:
  19. 19: [ComImport]
  20. 20: [Guid("68389CF3-212B-449D-83CB-0DD4572FEF03")]
  21. 21: class Foo : IFoo
  22. 22: {
  23. 23: [MethodImplAttribute(MethodImplOptions.InternalCall)]
  24. 24: public extern void Hello(string name);
  25. 25: }
  26. 26:
  27. 27: class FooStubClass
  28. 28: {
  29. 29: public delegate int IFoo_Hello_Delegate(IntPtr _this, IntPtr a);
  30. 30:
  31. 31: public void IFoo_Hello_Stub(IFoo _this, string name)
  32. 32: {
  33. 33: IntPtr nativeArg_name = IntPtr.Zero;
  34. 34:
  35. 35: try
  36. 36: {
  37. 37: //
  38. 38: // Marshal CLR => Native
  39. 39: //
  40. 40: nativeArg_name = Marshal.StringToBSTR(name);
  41. 41:
  42. 42: //
  43. 43: // Get interface pointer
  44. 44: //
  45. 45: IntPtr pIntf = Marshal.GetComInterfaceForObject(_this, typeof(IFoo));
  46. 46:
  47. 47: //
  48. 48: // Get target
  49. 49: //
  50. 50: IntPtr pTarget = IntPtr.Zero;
  51. 51:
  52. 52: unsafe
  53. 53: {
  54. 54: void** pVtbl = *(void***)pIntf;
  55. 55: pTarget = new IntPtr(*(pVtbl + 7)); // IUnknown => 3, IDispatch => 4
  56. 56: }
  57. 57:
  58. 58: //
  59. 59: // Make the call
  60. 60: //
  61. 61: Delegate dele = Marshal.GetDelegateForFunctionPointer(pTarget, typeof(IFoo_Hello_Delegate));
  62. 62: IFoo_Hello_Delegate targetDelegate = (IFoo_Hello_Delegate)dele;
  63. 63: int hr = targetDelegate(pIntf, nativeArg_name);
  64. 64: if (hr < 0)
  65. 65: Marshal.ThrowExceptionForHR(hr);
  66. 66:
  67. 67: //
  68. 68: // Marshal Native => CLR
  69. 69: //
  70. 70:
  71. 71: //
  72. 72: // Marshal return
  73. 73: //
  74. 74: }
  75. 75: finally
  76. 76: {
  77. 77: //
  78. 78: // Cleanup
  79. 79: //
  80. 80: if (nativeArg_name != IntPtr.Zero)
  81. 81: Marshal.FreeBSTR(nativeArg_name);
  82. 82: nativeArg_name = IntPtr.Zero;
  83. 83: }
  84. 84: }
  85. 85: }
  86. 86:
  87. 87: class Program
  88. 88: {
  89. 89: static void Main(string[] args)
  90. 90: {
  91. 91: Foo myFoo = new Foo();
  92. 92: myFoo.Hello("Foo!");
  93. 93: }
  94. 94: }
  95. 95: }
  96. 96:

作者:张羿

转载请注明出处

发表评论

表情:
评论列表 (有 0 条评论,72人围观)

还没有评论,来说两句吧...

相关阅读