.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:
作者:张羿
转载请注明出处
.NET 4中发布了最新版本的公共语言运行时,简称CLR (Common Language Runtime) 。这个版本是CLR 2.0之后又一个新的版本,包含着CLR小组几年以来的辛勤工作。
CLR上海团队计划在未来的几个月内陆续介绍其中的一些特性,本文作为一个概览,先作蜻蜓点水,抛砖引玉。也欢迎大家回复本文,告诉我们你所感兴趣的话题,我们会进一步作深入的介绍。
CLR 简介
CLR作为.NET框架中最为底层的部件,扮演着运行托管代码虚拟机的角色,承担着诸如即时编译(Just In Time Compile),垃圾回收(Garbage Collect)等任务。打一个比方,如果把操作系统看做是运行二进制程序的宿主,那么CLR就是托管世界的操作系统。
图一 CLR 在.NET框架中所处的位置
CLR作为.NET框架中的一部分,总是跟着.NET发行,但是近年来.NET的发行版本从2.0一直到3.5, 但是CLR却还一直保留在2.0,如下表所示:
|
.NET框架版本 |
时间 |
CLR |
|
1.0 |
2002.2 |
1.0 |
|
1.1 |
2003.4 |
1.1 |
|
2.0 (Generics) |
2006.1 |
2.0 |
|
3.0 (WPF/WCF/WF) |
2006.11 |
2.0 |
|
3.5 (LINQ) |
2007.11 |
2.0 |
|
4.0 Beta |
2009.5 |
4.0 |
图二 CLR 版本
大家可以看到,2.0的发行已经是三年之前的事情了,在这几年中,CLR小组的工作最后都汇集在了这次发行之中,可谓是众星云集,下面我们一一叙来。
托管与本地代码的互操作
托管代码与本地代码之间的互操作(interop)担负着.NET世界对外联系的责任。比如调用一个本地dll或者COM组件。在CLR 4中,我们作了以下工作,来提高互操作的易用性。
网络广播:http://msevents.microsoft.com/CUI/EventDetail.aspx?EventID=1032417433&Culture=zh-CN
1. 自定义QI(Custom QI)
当托管代码被COM调用的时候,它扮演着COM组件的角色。对于COM组件来说,IUnknown::QueryInerface(QI)是类型转化的关键。CLR4之前,为每个托管COM组件提供了一个QI实现; CLR4 允许用户自定义QI,大家可以从mscorlib中新增的interface,System.Runtime.InteropServices.ICustomQueryInterface着手了解这一新功能。
2. TlbImp源代码以及自定义工具
在托管代码中调用COM组件,需要这个COM组件用托管语言申明自己的接口,也就是Interop Assembly(IA)。在一般情况下,用户不需要自己动手撰写这些assembly,而可以使用TlbImp这个工具,根据TLB生成IA。在CLR 4的开发中,我们用托管代码把TlbImp重写了,并且把源代码公布在了codeplex上面。
发布TlbImp的源代码的好处之一,是方便使用者根据自己的需求,通过修改源代码来自拓展TlbImp的功能。我们也收集了很多客户需要自定义TlbImp的要求,并且提取了一些呼声最高的自定义请求,制作了TlbImp自定义工具,也在codeplex发行。详见http://blogs.msdn.com/silverlightshanghai/archive/2009/03/13/codeplex-tlbimp.aspx
3. 等价类型
前面提到,COM组件要为.NET所用,需要Interop Assembly。不同版本的COM组件,带来了部署上的问题。在CLR 4.0之中,我们通过等价类型的引入,就部署IA的问题,给出了更好的解决方案。
4. StubMethodReditection
自定义Stub来处理Interop中的Marshalling和目标函数调用;
5. 其他
Interop其他方面的改动,使用COM取代了原先的远程对象访问;让用户自己决定清理RCW的时机等等,会有更为详细的博文作具体介绍。
垃圾回收
垃圾回收一直是CLR中的核心模块,对托管程序运行的性能至关重要。在这个版本中,CLR引入了background GC,和原来的Concurrent GC相比,在GC进行的过程中,会更少的阻断其他进程,从而提高整个CLR的运行效率。同时,此前在sp2中引入的GC::RegisterForFullGCNotification可以让 CLR4.0可以通知用户第二代GC发生,从而使服务器有机会处理负载平衡,使得整个服务器端的处理能力不至于因为GC的发生受到太大的影响。
代码约定
在CLR4.0中,引入了代码约定,更方便用户规范代码的行为,大家可以从System.Diagnostics.Contracts这一命名空间着手,进一步了解其内容。
Corrupted state exception
CLR 4.0中,对异常处理的哲学有了一个改进:在默认情况下,try/catch语句将不能捕获诸如AccessViolationException等异常。因为这些异常的损毁(Corrupt)了机器的状态(state),即使用户捕获了它们,也无法继续执行代码,或者说,继续执行代码也会变得非常危险。
新的安全模型
用过CLR v2的安全模型的朋友们可能还会记得诸如Evidence,Policy以及Permission等概念,这些复杂的对象一起构筑了v2的安全模型的框架,CLR4.0中,安全模型被大大简化,SecurityCritical,SecurSafeCritical等一些安全级别构筑了新的安全模型的基础。
同一个进程,多个CLR
CLR4.0的出现,又添加了一个CLR的版本,尽管我们尽量保证各个不同版本之间的兼容性,但是还是可能出现一些已经开发的组件,需要特定的版本才能运行。为了确保用户过去编写的组件不会因为新的CLR版本而不能运行,CLR4.0中允许用户在一个进程中,运行不同的CLR版本,这样不同的组建就可以各取所需,运行在适合他们的CLR中了。
基本类库
基本类库,也就是mscorlib.dll,包括了诸如System.Object这样在整个类型系统中最为核心的类库。CLR4.0也包含了很多新功能:比如用于支持动态语言的System.Tuple,新的集合类型System.Collections.Generic.SortedSet,用于提高文件系统浏览性能的API,操作注册表的API,以及对内存映射文件的支持等等。
总的来说,CLR4.0相较于CLR2.0,在保证了很高的兼容性的同时,做了大量的改进工作,在之后的一系列博客中,我们团队的成员会进一步作更为具体的介绍,敬请大家期待。
最近在论坛上经常看到一些基本的interop的问题,给我动力写完之前的.net interop入门系列,给刚刚涉足.NET interop的朋友们一个大体上的概念。
每每谈及.NET interop,我的脑中总是出现下面一幅图:
该图代表了.net interop的四个典型场景。之前我的同事和我讨论了.NET和COM互操作的应用:
今天我主要讲一下P/Invoke和Reverse P/Invoke,和COM interop相比,P/Invoke无需注册组件,使用上更轻量,更绿色。
1. P/Invoke
P/Invoke(platform invoke)是.NET调用本地代码(native code)的一种比较轻便的方式。只需要将本地代码编写成动态链接库,然后在c#代码中,声明一个外部静态函数,并且用DllImport属性指明动态连接库的入口。举例如下:
using System;
using System.Runtime.InteropServices;
class PInvoke
{
[DllImportAttribute("user32.dll", EntryPoint = "MessageBoxW")]
public static extern int MessageBoxW(
[In]System.IntPtr hWnd,
[In][MarshalAs(UnmanagedType.LPWStr)] string lpText,
[In][MarshalAs(UnmanagedType.LPWStr)] string lpCaption,
uint uType);
public static void Main()
{
MessageBoxW(IntPtr.Zero, "Hello", "Interop", 0);
}
}
稍加解释这个代码。类PInvoke中,有个MessageBoxW的函数声明,它的实现在user32.dll(系统自带)中,入口是MessageBoxW,参数的构成是根据windows API的声明而定的,我们在Codeplex上有一个工具,专门帮助大家声称一个本地代码(c++)编写的函数在托过代码(c#)中的函数声明,之前我们团队的成员也撰文介绍了这个工具的使用。
有了这个声明以后,在Main中调用MessageBox,就和调用其他托管代码一样轻松自如了。
2. Reverse P/Invoke
接着,我们来看看在本地代码中调用.NET方法。本地代码需要拿到一个.NET委托(delegate),然后把这个delegate当作一个函数指针使用,示例如下:
using System;
using System.Windows.Forms;
using System.Runtime.InteropServices;
public class Program
{
internal delegate void DelegateMessageBox([MarshalAs(UnmanagedType.LPWStr)]string msg);
[DllImport("Native.dll", CallingConvention = CallingConvention.Cdecl)]
static extern void NativeMethod(DelegateMessageBox d);
public static void ShowMessageBox(string msg)
{
MessageBox.Show(msg);
}
public static void Main()
{
NativeMethod(new DelegateMessageBox(ShowMessageBox));
}
}
这个例子中,我们希望本地代码能够调用托管函数ShowMessageBox来显示一个对话框。为了让本地代码可以调用这个函数,我们根据它的声明,定了了一个delegate,并且通过P/Invoke把这个委托传给了本地代码。本地代码可以如下调用托管代码:
#include <stdio.h>
#include <wtypes.h>
extern "C" {
__declspec(dllexport) void NativeMethod(void (__stdcall *pShowMsgBox)(WCHAR *wChar))
{
(*pShowMsgBox)(L"hello reverse interop");
}
}
注意到托管代码中的委托到了本地代码中,就是一个函数指针,本地代码可以像一个普通的函数指针一般调用托管代码。
大家可能注意到dll的声明用了extern “C”,它指明了调用规范是cdecl,在之前的托过代码的DllImport中,也相应的注明了调用约定,关于调用约定的详细介绍,可以参见我的另一篇博客。
今天的介绍就到这里,大家可以把这些示例代码当作一个template,根据实际需求作相应的具体改动。
在刚刚结束的Mix09大会上(Mix是微软面向web开发者和设计者的会议),Silverlight团队的程序经理Joe Stegman介绍了silverlight3的许多让人兴奋的新功能,摘录如下:
1. 支持更多的媒体编码格式
在Silverlight3中新增加的多媒体编码格式包括H.264,AAC,MP4。Silverlight这项技术,从出生以来,就一直把对多媒体,尤其是视频的支持,放在首要位置。这次对更多的编码格式提供支持,方便网站建设者更容易的发布、部署视频资料,只需要如下一行XML:
<MediaElement X:Name="m" source="bbb.mp4">
2. 利用GPU加速
随着显卡计算能力的加强,应用程序把越来越多的图形计算任务从CPU中拿出来交给GPU完成。然而,要利用到GPU,对程序员而言通常意味着更多的编码任务,在Silverlight3中,这个任务的复杂程度被大大简化,只需要在XAML中多加入几行XML代码,就可以轻松享受GPU的超强计算能力,贴一张GPU渲染的效果图:
3. 透视化3D
透视化3D,简单的说,就是把2D对象放到3D空间中去。和传统的3D把一个3D空间的对象投影到2D空间中不同,透视化3D意味着更高的性能,更友好的编程接口,同时能完成我们80%的3D任务。好了,说了那么多,来看一个demo,一行XAML带来的变化:
<border.projection>
<planeprojection rotationY="-30"/>
</border.projection>
以上这个例子,稍加扩展,就可以在silverlight3中,在3D空间中旋转一个控件。
4. 自定义特效
在silverlight3中,引入了shader的概念,它是一个像素粒度的操作——每当silverlight3要显示一个像素的时候,它对shader说:“我要显示这个像素了,你是否要做些处理,实现某些特效?”。我们来看看shader的强大之处:下面一个示例中,左边的图像是背景图像,右边的图像是前景图像,在silverlight3之前,我们尽管可以同时显示这两个图像,但是背景图像会被前景遮住。(当然可以设 置前景的透明度,但是这样整个前景图像的清晰度就下降了)我们在显示前景图像的时候可以应用一个shader:把所有的黑色过滤掉。这样,我们在前景上过滤掉所有黑色背景的同时,还拥有了一个清晰的火焰。
5. 更多的控件支持
每一个新的silverlight版本的发布,都伴随着很多新的控件的问世。这个版本也不例外,新推出的控件有:DockPanel,Expander,Label,TreeView,ViewBox等等。在这里举一个“Save as...”(另存为)控件的例子。可能有的朋友会问,“另存为”作为一个耳熟能详的控件,为什么要等到silverlight3才提供?答案是出于安全性的考虑:silverlight为了保护用户的安全,对本地文件的读写有很大的限制。(否则的话,如果服务器端可以随意读写用户的本地文件,那么就很容易做出一个钓鱼网站了)。silverlight3中,对另存为控件的支持也有着安全性的考虑。开发人员可以创建一个SaveFileDialog实例,但是当用户选定本地文件的时候,他只能得到这个文件的stream,而不是这个文件的路径。这样的设计避免了提供一个打开任意路径的本地文件的功能,从而使silverlight运行在一个更为安全的环境中。
6. 本地消息传递(local messaging)
Silverlight是浏览器的一个插件,在同一时间可能会有多个实例。比如多个浏览器同时访问包含silverlight的网页,就会有多个silverlight的实例同时运行,本地消息传递允许这些不同的siliverlight控件实例之间互相通信。
7. 在浏览器外运行silverlight
silverlight3支持把一个silverlight页面安装到本地,用户可以像一个桌面程序一样离线使用这个程序,并且可以右键卸载这个程序。
比如下面一个浏览器中运行的国际象棋程序,用户可以在右键菜单中,把它存储为一个本地程序。
然后,就可以像一个普通的桌面程序一样,运行这个程序了。同时,也能在这个桌面程序的右键菜单中卸载这个程序。
更多关于silverlight3的信息,可以在下面这个网站中获得
http://silverlight.net/getstarted/silverlight3/default.asp
其中包括工具的开发,第一手学习的资料(博客以及书籍),以及示例程序。
大家好。距离上次我们发布在CodePlex上的新版本TlbImp已经过了快半年了。在这半年的时间内,除了主要进行.NET 4.0相关的新功能开发之外,我们上海CLR小组也没有忘记进行TlbImp相关功能的继续开发,于今年3月9日再次发布了TlbImp的一个新版本:
http://www.codeplex.com/clrinterop/Release/ProjectReleases.aspx?ReleaseId=17579
这次版本中我们引入了两个重要功能:
1. 通过规则自定义互操作程序集以及规则自定义编辑器
2. 回归测试工具
基于规则的自定义功能
我们先来看一下自定义功能。这个新版本的TlbImp允许用户通过自定义的一系列的规则来指定TlbImp如何生成最终的互操作程序集。之前有不少用户向我们提到在使用TlbImp的时候,经常需要对TlbImp生成的结果做一些修改,而且必须是自动化的修改。他们通常使用的方法是先使用ILDASM反汇编,使用Perl脚本修改反汇编代码,然后再使用ILASM重新生成互操作程序集。为了解决这个问题,我们引入了一个新功能,允许用户以非常自由的方式来定义他们最终想要看到的结果。
让我们先来看一个简单的例子:假设我们希望改变互操作程序集中的某个类型的名称。先双击打开TlbImpConfigFileEditor.exe启动自定义文件的编辑器,然后打开我们需要自定义的Type Library,这里我们选择发布版本中Samples\ChangeManagedName\ChangeManagedNameSample.tlb文件,如下:

左边显示的是我们需要自定义的Type Library,而右边,则是我们需要自定义的规则,这些规则可以告诉TlbImp如何修改最终生成的互操作程序集。首先,将我们需要修改的IComparable接口结点从左边拖到右边,松开鼠标,出现如下的对话框:

上面这个对话框是用来创建一个新的规则,规则指定TlbImp对于哪些对象应用何种动作。在这个对话框中我们需要定义这个规则所对应的动作(Action),因此需要在Action下拉框中选择ChangeManagedName,然后点击OK即可。之后编辑器状态如下(需要自己展开结点):

大家可以看到右边已经出现了一个新的规则叫做Change interface name,对应的Category是Type,也就是说这个规则是针对互操作程序集中的类型设置的。Condition指定了规则所需要满足的条件,选中Condition节点(或者其子节点)可以在下面的Condition Expression中看到对应的规则表达式,也就是NativeName Equal ‘ICompareable’,意思是该规则是针对任何名字叫做IComparable的类型。注意因为我们是从IComparable节点直接拖到右边,因此这些条件是编辑器自动生成的。大家如果需要也可以自己通过点击Native Equal IComparable条件来修改,或者点击<Empty>来增加新的条件。现在我们可以点击<Empty>节点,在下拉框中选择TypeKind,第二个下拉框选择Equal,第三个下拉框选择Interface,最终的结果如下:

注意表达式节点的组织方式是类似语法树的样子,也就是说And节点下面的互相之间是And关系,最终的结果总是可以在Condition Expression一栏看到:
( NativeName Equal 'IComparable' ) And ( TypeKind Equal 'Interface')
当编辑好了规则的时候,我们需要指定对应的具体动作的参数。因为我们需要修改对象类型的名称,双击Action下面的NewName子结点会弹出如下对话框:

输入我们想修改成的名字,然后点击OK。
这样一个规则就完成了:

修改完毕之后存盘为ChangeInterfaceName.xml,然后在命令行下面调用TlbImp,使用/config参数引用之前存盘的Config文件(黄色加亮部分:

之后通过ILDASM打开我们生成的结果:

可以看到IComparable已经被改名成了IMyInterface。
TlbImp总共支持下面几种动作(Action):
1. ChangeManagedName:修改类型、函数的名称
2. ResolveTo:将一个类型替换为另外一个类型(可以是另外一个程序集的类型)。现在已经有用户在CodePlex上面提出这个功能需求了:http://clrinterop.codeplex.com/WorkItem/View.aspx?WorkItemId=2565
3. AddAttribute:为任意类型添加任意Attribute
4. PreserveSig:为单个函数或者类型中的所有函数添加PreserveSigAttribute并相应修改函数的原型
5. ConvertTo:修改函数中的参数类型
每种对应的动作在Samples目录下面都有对应的例子,有兴趣的朋友可以参照文档自行实验。
回归测试工具
为了帮助用户在修改TlbImp代码的时候可以更容易保证自己的修改不会引起其他问题,我们引入了一个简单的回归测试工具,大家可以到这里下载:
http://clrinterop.codeplex.com/Release/ProjectReleases.aspx?ReleaseId=17579
下载完毕之后打开Bin目录下面的TlbImpRegressionTestTool.exe,然后通过File菜单打开Testcase目录下面的testcases.xml文件:

可以看到所有的Testcase都被列出来了。下一步在Run菜单里面选择Settings,输入TlbImp2.exe和WinDiff.exe所在位置:

完毕之后,选择Run下面的Run All Testcases或者Run Selected Testcases,该工具便会调用TlbImp2.exe依次运行Testcase来检查TlbImp2的相应功能是否正确:

绿色为测试成功,红色为失败。如果有失败的情况,双击该行可以打开WinDiff比较TlbImp2当前生成的结果和应该生成的结果之间的区别。
最后,希望大家能够积极试用TlbImp的新功能。如果有希望看到的TlbImp的新功能,或者对目前的TlbImp有哪些觉得做的不够好的地方,都可以到下面的地址提出你的宝贵意见: http://clrinterop.codeplex.com/WorkItem/List.aspx