上次我们谈到了.NET 4.0中为什么要引入Type Equivalency这样一个新功能,这次我们来看一个比较简单的一个程序:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Office.Interop.Excel;
namespace NOPIAExcelDemo
{
class Program
{
static void Main(string[] args)
{
Application excelApp = new Application();
excelApp.Workbooks.Add();
excelApp.Visible = true;
}
}
}
这个程序调用Excel,创建一个新的Workbook,并把Excel主程序设置为可见。
首先,我们在VS 2010中添加一个新的C#控制台项目,然后在Solution Explorer中选择Add Reference,选择Excel 12的Interop Assembly:
选择点击OK之后,在Reference下面会多出一项Microsoft.Office.Interop.Excel的引用,在其上右键点击选择Properties:
里面有一项Embed Interop Types,修改为True。
完成之后运行程序,没发现区别是不是?呵呵,这就对了。我们回头来看看生成的代码是什么样子的。
用ILDASM打开生成的EXE,双击Manifest,结果如下:
// Metadata version: v4.0.11001
.assembly extern mscorlib
{
.publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) // .z\V.4..
.ver 4:0:0:0
}
.assembly extern System.Core
{
.publickeytoken = (B7 7A 5C 56 19 34 E0 89 ) // .z\V.4..
.ver 4:0:0:0
}
.assembly NOPIAExcelDemo
{
// ...
}
上面的结果说明什么呢?虽然我们之前通过Add Reference让这个项目引用了Excel的PIA,但是,这个EXE并没有对Excel的PIA的引用,也就是说,这个EXE可以独立于PIA运行!这也正是这个Feature的最直接的作用:消除PIA,直接将Interop相关的托管类型直接嵌入(Embed)到EXE中。现在我们再看看这个EXE中又有那些类型:
非常清楚,除了NOPIAExcelDemo本身的类型之外,这个EXE把Excel的PIA中部分类型,如_Application, _Workbook, WorkBooks…等等,都包括进来了。注意这些类型都是刚才的程序所引用到的,而没有用到是不会出现在这个EXE中的。再进一步看看Workbooks这个接口。注意我们调用到了了Workbooks.Add方法,而在这个EXE中的Workbooks类型也只有Add方法!那么其他方法都去了那里呢?让我们再回头看看PIA中的Workbooks类型是什么样子的:
可以看到其中的方法要多上不少。而且EXE中的Workbooks类型还有两个奇怪的_VtblGap1_3,_VtblGap2_15。这两个函数的作用是什么呢?CLR的Interop有一个不太为人知的特性:因为COM中的接口是基于虚函数表的,如果只调用虚函数表中的某个函数,而不用到其他函数,那么其他函数的入口点是可以不需要的,只需要用到的那个函数在正确的虚表位置,指向正确的函数地址即可。因此,CLR提供了一个功能,凡是接口中以_VtblGap_<N>_<M>形式命名的函数,都视为虚函数表中的M个空白项(注意N值忽略)。以Workbooks函数为例,第一个_VtblGap1_3函数表明这里有三个不用到的虚函数表项,对应着Workbooks里面的get_Application, get_Creator, get_Parent函数。Get_Parent之后正好是Add函数。在Add函数之后,又有一个_VtblGap2_15,对应着从Close到OpenXML这15个函数。注意.NET属性并非函数(而是由编译器翻译成对应的函数),因此不算在内。这种做法可以有效的节约空间占用,减少用到的接口的复杂性。这个将Interop Assembly中的类型“拖入”到用户的EXE中的操作,我们内部称之为Pull-in,是由C#编译器实现的。
谈了这么多,总结一下:C#编译器允许将Interop Assembly中的类型直接嵌入在最终生成的EXE中,从而断绝和Interop Assembly(包括PIA)之间的引用关系。需要Interop Assembly从此只限于在编译时,而非运行时。
不过,这个只解决了问题的一部分,那就是如何避免最终的EXE和Interop Assembly之间存在引用关系。然而,最重要的是,如果在另外的Assembly中,也引用到了另外版本的Interop Assembly中的某个类型,比如Office 11中的Workbooks,在这种情况下,如何解决托管类型冲突的问题呢?答案在TypeIdentifierAttribute中。如果我们查看EXE中的Workbooks的接口定义,我们会发现下面的内容:
.class interface public abstract auto ansi import Microsoft.Office.Interop.Excel.Workbooks
implements [mscorlib]System.Collections.IEnumerable
{
.custom instance void [mscorlib]System.Runtime.InteropServices.TypeIdentifierAttribute::.ctor(string,
string) = ( 01 00 00 00 00 00 )
.custom instance void [mscorlib]System.Runtime.InteropServices.GuidAttribute::.ctor(string) = ( 01 00 24 30 30 30 32 30 38 44 42 2D 30 30 30 30 // ..$000208DB-0000
2D 30 30 30 30 2D 43 30 30 30 2D 30 30 30 30 30 // -0000-C000-00000
30 30 30 30 30 34 36 00 00 ) // 0000046..
} // end of class Microsoft.Office.Interop.Excel.Workbooks
其中的TypeIdentifierAttribute是Type Equivalency,也就是NOPIA这个Feature的核心内容。在下一篇文章中,我将讲解这个Attribute的作用,以及C#编译器如何处理具有这个Attribute的类型之间的交互。
作者: 张羿
转载请注明出处