.NET组件和COM组件之间的相互操作

来源:互联网 发布:波尔代热斯现象 知乎 编辑:程序博客网 时间:2024/05/17 09:34


.NET技术是微软大力推广的下一代平台技术,自从.NET技术架构Beta2版本的正式发布,此项技术也逐渐走向成熟和稳定。按照微软的平台系统占有率,我们不难想象得到,在未来的一两年内.NET技术必定会势如破竹一般的登上主流的技术平台,而一个新的技术平台得以快速发展的最重要的前提是:他不会彻底的摒弃以前的技术,这一点对于.NET技术来说指的就是COM/COM+技术了。   

一般来说,在IT技术界以及硬件产业,技术的更新换代速度非常得惊人,而惯例是所有的新技术都会遵循向下兼容的原则,但是.NET技术不仅仅做到了这一点,.NET甚至实现了相互之间的各自调用,这一点是非常难能可贵的。也就是说,不但我们可以在.NET组件中调用COM组件,同时也可以在COM组件中正常的调用.NET组件。这点带来的好处是显而易见的,一方面我们可以保持现有的技术资源,另一方面,在现有资源中可以利用.NET所带来的各种新技术。   

在开始之前我们先来看一下.NET组件和我们目前常使用的COM组件之间有何不同之处吧。   

.NET组件可以分为两大类:共享的.NET组件和私有的.NET组件。   

共享的.NET组件需要通过标准公开关键字密码技术来保证自身的唯一性,在这一点上面比较类似于COM的全球唯一ID号码GUID。但是,我们在可能的情况下应当尽量避免使用共享的.NET组件,因为这样将会有可能再一次的陷入目前在视窗系统下长期困扰开发人员的“DLL地狱”。   

私有的.NET组件是我们将会经常使用的.NET组件方式,在这种方式之下,我们发布.NET组件需要做的只是简单的进行拷贝操作就可以了,仿佛回到了远古的DOS时代了,在也不必关心纷繁复杂的系统注册表了,也不必担心DLL的版本被覆盖等等的问题了。   

而COM组件是微软曾经力推了很多年的一种代码复用的技术框架,在这些年里也得到了极大的发展和应用,但它的弊端却也日益明显,我们不得不面对众多的COM组件之间的版本控制和令人恐怖的DLL地狱,还有注册表、GUID等等。在我们安装一个软件的同时,也带来了大量的我们所未知的版本繁多的COM组件到我们的操作系统中。   

但是,COM组件的技术优势也是明显的,在很大的程度上实现了Windows平台下的代码复用,所以我们才会提出这样的一个话题,如何才能在.NET技术日臻成熟的情况下,保护和利用已经存在的大量的采用COM技术的软件和产品呢?   

下面我们就探讨一下,如何实现.NET组件和COM组件之间的相会调用和操作。   

首先,我们来看看在现有的COM组件中如何调用.NET编写的组件吧:   
在这里我们使用C#编写一个最简单的组件,仅仅实现返回一个字符串,详细的代码如下:   

在上面的.NET组件当中我们实现了一个类成员:say。他的用途是简单的返回一段字符串。   

切换到MS-DOS命令行下面,运行:   
C:\> csc   /t:library   /out   ClassLibrary1.DLLClassLibrary1.cs     

上面的编译器参数/t:library   告诉C#编译器我们现在是在建立一个库应用程序。这样,我们就得到了一个名称为ClassLibrary1.DLL的.NET组件了。   
但是想要在现有的COM组件中使用这个组件,我们还有进行如下步骤:   

切换到MS-DOS命令行下面,运行:   
C:\> regasm   out   ClassLibrary1.DLL   /regfile:ClassLibrary1.reg     

上面的命令行是注册我们的.NET组件,并且产生了一个备用的注册表文件。大家一定会记得在以前我们的win9x/NT/2000下面注册COM组件使用的命令是:   
regsvr32   c:\test.dll     

在.NET下面,注册.NET组件就需要上面的regasm命令了,值得注意的是,这个方法仅仅是为了可供COM组件来调用,.NET本身之间相互调用组件是不需要任何注册的!   

还没有结束呢,接下来我们需要:   

切换到MS-DOS命令行下面,运行:   
C:\> tlbExp   ClassLibrary1.dll   /out:ClassLibrary1.tlb     

上面的命令行表示将会产生一个.NET组件的类型库,目的是为了我们在COM组件中进行提前绑定操作。   

好了,接下来我们就可以在自己的基于目前COM技术的代码中方便的使用上面我们使用C#编写的.NET组件了。在这里,我们使用VB6.0编写一个小小的测试代码,在开始之前我们需要在VB的集成环境中使用菜单中的“引用”选项,选择我们刚才产生的类型库文件ClassLibrary1.tlb。   

VB测试的代码如下:   
Private   Sub   Form_Load()   
Dim   test   As   New   ClassLibrary1.hello   
Dim   str   As   String   

str   =   test.say( "dddd ")   
MsgBox   str   
End   Sub     

接下来,我们就来看看,如何在.NET组件中使用目前存在的COM组件吧。   

对于.NET来讲,使用COM组件要想对简单一些。.NET在设计之初就考虑到了如何方便的利用现有的各种技术资源,这也是微软的一贯作风,.NET的强大之处也可见一斑。.NET提供了大量的类库来方便的实现同COM的相互操作,其中很重要的一个名称空间就是:System.Runtime.InteropServices。通过这个名称空间的名字我们也可以从字面上看出,“互操作服务”。System.Runtime.InteropServices这个名称空间提供了一系列的类来对COM对象进行操作。   

下面的例子中,我们来调用一下系统自带的Win32函数MessageBoxA,这个函数位于系统的COM组件user32.dll当中,我们调用的代码如下:   
using   System;   
using   System.Runtime.InteropServices;   

class   Test   
{   
[DllImport( "user32.dll ")]   
public   static   extern   int   MessageBoxA(inthWnd,string   strMsg,string   strCaption,intnType);   

public   static   void   Main()   
{   
int   myMsg;   
myMsg=MessageBoxA(0, "Hello! ", "test ",0);   
}   
}     

切换到MS-DOS命令行下面,运行:   
C:\> csc   Test.cs     

编译完毕我们的C#应用程序之后,直接运行就可以看到对话框了!   

需要注意的是,在调用COM组件之前,我们需要在.NET程序中引用名称空间:System.Runtime.InteropServices。因为我们需要使用这个名称空间所提供的一个方法:DllImport。   

怎么样,在.NET中使用COM组件是非常的方便的吧。上面是我们调用的系统本身自带的DLL组件,同样的,也可以使用这种方法来调用我们自己使用VB/VC编写的COM组件。   

好了,我们已经探讨完了如何在.NET和COM之间相互操作了,我们也很显然的看到了.NET技术的强大,现在越早掌握.NET技术就会在未来越容易的占领领先的位置,让我们共同努力掌握强大的.NET平台技术吧。




pi函数是构筑Windws应用程序的基石,每一种Windows应用程序开发工具,它提供的底层函数都间接或直接地调用了Windows   API函数,同时为了实现功能扩展,一般也都提供了调用WindowsAPI函数的接口,   也就是说具备调用动态连接库的能力。Visual   C#和其它开发工具一样也能够调用动态链接库的API函数。.NET框架本身提供了这样一种服务,允许受管辖的代码调用动态链接库中实现的非受管辖函数,包括操作系统提供的Windows   API函数。它能够定位和调用输出函数,根据需要,组织其各个参数(整型、字符串类型、数组、和结构等等)跨越互操作边界。   

下面以C#为例简单介绍调用API的基本过程:   
动态链接库函数的声明   
 动态链接库函数使用前必须声明,相对于VB,C#函数声明显得更加罗嗦,前者通过   Api   Viewer粘贴以后,可以直接使用,而后者则需要对参数作些额外的变化工作。   

 动态链接库函数声明部分一般由下列两部分组成,一是函数名或索引号,二是动态链接库的文件名。   
    譬如,你想调用User32.DLL中的MessageBox函数,我们必须指明函数的名字MessageBoxA或MessageBoxW,以及库名字User32.dll,我们知道Win32   API对每一个涉及字符串和字符的函数一般都存在两个版本,单字节字符的ANSI版本和双字节字符的UNICODE版本。   

 下面是一个调用API函数的例子:   
[DllImport( "KERNEL32.DLL ",   EntryPoint= "MoveFileW ",   SetLastError=true,   
CharSet=CharSet.Unicode,   ExactSpelling=true,   
CallingConvention=CallingConvention.StdCall)]   
public   static   extern   bool   MoveFile(String   src,   String   dst);   

 其中入口点EntryPoint标识函数在动态链接库的入口位置,在一个受管辖的工程中,目标函数的原始名字和序号入口点不仅标识一个跨越互操作界限的函数。而且,你还可以把这个入口点映射为一个不同的名字,也就是对函数进行重命名。重命名可以给调用函数带来种种便利,通过重命名,一方面我们不用为函数的大小写伤透脑筋,同时它也可以保证与已有的命名规则保持一致,允许带有不同参数类型的函数共存,更重要的是它简化了对ANSI和Unicode版本的调用。CharSet用于标识函数调用所采用的是Unicode或是ANSI版本,ExactSpelling=false将告诉编译器,让编译器决定使用Unicode或者是Ansi版本。其它的参数请参考MSDN在线帮助.   

 在C#中,你可以在EntryPoint域通过名字和序号声明一个动态链接库函数,如果在方法定义中使用的函数名与DLL入口点相同,你不需要在EntryPoint域显示声明函数。否则,你必须使用下列属性格式指示一个名字和序号。   

[DllImport( "dllname ",   EntryPoint= "Functionname ")]   
[DllImport( "dllname ",   EntryPoint= "#123 ")]   
值得注意的是,你必须在数字序号前加“#”   
下面是一个用MsgBox替换MessageBox名字的例子:   
[C#]   
using   System.Runtime.InteropServices;   

public   class   Win32   {   
[DllImport( "user32.dll ",   EntryPoint= "MessageBox ")]   
public   static   extern   int   MsgBox(int   hWnd,   String   text,   String   caption,   uint   type);   
}   
许多受管辖的动态链接库函数期望你能够传递一个复杂的参数类型给函数,譬如一个用户定义的结构类型成员或者受管辖代码定义的一个类成员,这时你必须提供额外的信息格式化这个类型,以保持参数原有的布局和对齐。   

C#提供了一个StructLayoutAttribute类,通过它你可以定义自己的格式化类型,在受管辖代码中,格式化类型是一个用StructLayoutAttribute说明的结构或类成员,通过它能够保证其内部成员预期的布局信息。布局的选项共有三种:   

布局选项   
描述   
LayoutKind.Automatic   
为了提高效率允许运行态对类型成员重新排序。   
注意:永远不要使用这个选项来调用不受管辖的动态链接库函数。   
LayoutKind.Explicit   
对每个域按照FieldOffset属性对类型成员排序   
LayoutKind.Sequential   
对出现在受管辖类型定义地方的不受管辖内存中的类型成员进行排序。   
传递结构成员   
下面的例子说明如何在受管辖代码中定义一个点和矩形类型,并作为一个参数传递给User32.dll库中的PtInRect函数,   
函数的不受管辖原型声明如下:   
BOOL   PtInRect(const   RECT   *lprc,   POINT   pt);   
注意你必须通过引用传递Rect结构参数,因为函数需要一个Rect的结构指针。   
[C#]   
using   System.Runtime.InteropServices;   

[StructLayout(LayoutKind.Sequential)]   
public   struct   Point   {   
public   int   x;   
public   int   y;   
}   

[StructLayout(LayoutKind.Explicit]   
public   struct   Rect   {   
[FieldOffset(0)]   public   int   left;   
[FieldOffset(4)]   public   int   top;   
[FieldOffset(8)]   public   int   right;   
[FieldOffset(12)]   public   int   bottom;   
}   

class   Win32API   {   
[DllImport( "User32.dll ")]   
public   static   extern   Bool   PtInRect(ref   Rect   r,   Point   p);   
}   
类似你可以调用GetSystemInfo函数获得系统信息:   
?   using   System.Runtime.InteropServices;   
[StructLayout(LayoutKind.Sequential)]   
public   struct   SYSTEM_INFO   {   
public   uint   dwOemId;   
public   uint   dwPageSize;   
public   uint   lpMinimumApplicationAddress;   
public   uint   lpMaximumApplicationAddress;   
public   uint   dwActiveProcessorMask;   
public   uint   dwNumberOfProcessors;   
public   uint   dwProcessorType;   
public   uint   dwAllocationGranularity;   
public   uint   dwProcessorLevel;   
public   uint   dwProcessorRevision;   
}   


[DllImport( "kernel32 ")]   
static   extern   void   GetSystemInfo(ref   SYSTEM_INFO   pSI);   

SYSTEM_INFO   pSI   =   new   SYSTEM_INFO();   
GetSystemInfo(ref   pSI);   

类成员的传递   
同样只要类具有一个固定的类成员布局,你也可以传递一个类成员给一个不受管辖的动态链接库函数,下面的例子主要说明如何传递一个sequential顺序定义的MySystemTime类给User32.dll的GetSystemTime函数,   函数用C/C++调用规范如下:   

void   GetSystemTime(SYSTEMTIME*   SystemTime);   
不像传值类型,类总是通过引用传递参数.   
[C#]   
[StructLayout(LayoutKind.Sequential)]   
public   class   MySystemTime   {   
public   ushort   wYear;   
public   ushort   wMonth;   
public   ushort   wDayOfWeek;   
public   ushort   wDay;   
public   ushort   wHour;   
public   ushort   wMinute;   
public   ushort   wSecond;   
public   ushort   wMilliseconds;   
}   
class   Win32API   {   
[DllImport( "User32.dll ")]   
public   static   extern   void   GetSystemTime(MySystemTime   st);   
}   
回调函数的传递:   
从受管辖的代码中调用大多数动态链接库函数,你只需创建一个受管辖的函数定义,然后调用它即可,这个过程非常直接。   
如果一个动态链接库函数需要一个函数指针作为参数,你还需要做以下几步:   
首先,你必须参考有关这个函数的文档,确定这个函数是否需要一个回调;第二,你必须在受管辖代码中创建一个回调函数;最后,你可以把指向这个函数的指针作为一个参数创递给DLL函数,.   


回调函数及其实现:   
回调函数经常用在任务需要重复执行的场合,譬如用于枚举函数,譬如Win32   API   中的EnumFontFamilies(字体枚举),   EnumPrinters(打印机),   EnumWindows   (窗口枚举)函数.   下面以窗口枚举为例,谈谈如何通过调用EnumWindow   函数遍历系统中存在的所有窗口   

分下面几个步骤:   
1.   在实现调用前先参考函数的声明   
BOOL   EnumWindows(WNDENUMPROC   lpEnumFunc,   LPARMAM   IParam)   
显然这个函数需要一个回调函数地址作为参数.   
2.   创建一个受管辖的回调函数,这个例子声明为代表类型(delegate),也就是我们所说的回调,它带有两个参数hwnd和lparam,第一个参数是一个窗口句柄,第二个参数由应用程序定义,两个参数均为整形。   

  当这个回调函数返回一个非零值时,标示执行成功,零则暗示失败,这个例子总是返回True值,以便持续枚举。   
3.   最后创建以代表对象(delegate),并把它作为一个参数传递给EnumWindows   函数,平台会自动地   把代表转化成函数能够识别的回调格式。   

[C#]   
using   System;   
using   System.Runtime.InteropServices;   

public   delegate   bool   CallBack(int   hwnd,   int   lParam);   

public   class   EnumReportApp   {   

[DllImport( "user32 ")]   
public   static   extern   int   EnumWindows(CallBack   x,   int   y);   

public   static   void   Main()   
{   
CallBack   myCallBack   =   new   CallBack(EnumReportApp.Report);   
EnumWindows(myCallBack,   0);   
}   

public   static   bool   Report(int   hwnd,   int   lParam)   {   
Console.Write( "窗口句柄为 ");   
Console.WriteLine(hwnd);   
return   true;   
}   
}   


指针类型参数传递:   
 在Windows   API函数调用时,大部分函数采用指针传递参数,对一个结构变量指针,我们除了使用上面的类和结构方法传递参数之外,我们有时还可以采用数组传递参数。   

 下面这个函数通过调用GetUserName获得用户名   
BOOL   GetUserName(   
LPTSTR   lpBuffer,   //   用户名缓冲区   
LPDWORD   nSize   //   存放缓冲区大小的地址指针   
);   
    
[DllImport( "Advapi32.dll ",   
EntryPoint= "GetComputerName ",   
ExactSpelling=false,   
SetLastError=true)]   
static   extern   bool   GetComputerName   (   
[MarshalAs(UnmanagedType.LPArray)]   byte[]   lpBuffer,   
    [MarshalAs(UnmanagedType.LPArray)]   Int32[]   nSize   );   
 这个函数接受两个参数,char   *   和int   *,因为你必须分配一个字符串缓冲区以接受字符串指针,你可以使用String类代替这个参数类型,当然你还可以声明一个字节数组传递ANSI字符串,同样你也可以声明一个只有一个元素的长整型数组,使用数组名作为第二个参数。上面的函数可以调用如下:   

byte[]   str=new   byte[20];   
Int32[]   len=new   Int32[1];   
len[0]=20;   
GetComputerName   (str,len);   
MessageBox.Show(System.Text.Encoding.ASCII.GetString(str));   
 最后需要提醒的是,每一种方法使用前必须在文件头加上:   
 using   System.Runtime.InteropServices;