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的类型之间的交互。


作者
: 张羿              
转载请注明出处