`
cuijiemin
  • 浏览: 265360 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

连接 COM 与 .NET 的桥梁

阅读更多
预备知识

作者:caeser2

本文代码使用ISO C++和.net v1.1框架(VS 2003)编写,但其原理适合所有支持.net框架的语言

一、前言
  由于传统的COM技术使用静态的非托管编程,而.net使用动态的托管编程,所以这个题目本质上讨论的是托管与非托管代码之间的互操作中的一个特例。目前的 .net v2.0 提供以下 三种互操作:

  • 模块级别的P/Invoke方法
    这种操作方法适合调用在动态链接库 (DLL)(如 Win32 API 中的 DLL)中实现的非托管函数。将在下一节讨论;
  • 组件级别的COM Interop方法
    这是本文的重头戏,它分两个部分:
      1、导出类型库的方法:本文的主角;
      2、使用封装类的方法 :该方法为CLI C++所独有,由框架自动生成包装类(在COM客户端中为非托管头文件代码),原理同第三种互操作相同;
  • 代码级别的C++ Interop方法
    这种操作方法是 CLI C++ 所独有的,即只能用在VS 2005及以上版本中。其实现非常简单,对外部 DLL 的包装全部都由向导生成,不用写一行代码,所以本文不再描述。(另一个原因是我暂时没有条件使用 VS 2005 ^_^!)
  • 二、类型转换与封送处理
      既然是两个平台、两个世界之间的互操作,它们之间的信息传递就必须转换为对方能看懂的类型,这种转换叫做“Marshal”,或者叫做“封送”。数据的封送处理是个异常复杂的过程,感兴趣的读者可以在 MSDN 中搜索描述其原理的文档“Marshaling Details”。下图是 COM Interop 的封送原理。



      好在 .net 平台的 Interop 程序集机制可以帮助我们进行一些简单的数据封装工作,把复杂的 Marshal 细节给隐藏了。这样使得我们在 .net 客户端中调用 COM 服务器时传递一些简单的数据会非常方便,但如果传送的数据是自定义的,则仍需手动 Marshal。该机制的原理描述在 MSDN 中的“COM 包装”。
      下图摘自 MSDN 2003 版,描述了.net v1.1 将把我们的数据从一个平台的类型转换为另一个平台的何种类型,表中没有没有显式标识的任何类型都将被转换为 Int32 系统类型。



      嗯...看上去还真不少,不过别高兴的太早了,在实际的使用中只有如int等较为简单的类型才能保证各种灵活的传递方式而不出问题...@_*。需要注意的是,COM一方的VARIANT类型(这也是编写COM时的推荐类型)将按照其vt的值来转换为具体的类型并封装,所以COM一方设置vt时一定要设对了,别乱设!另外对于表中没有给出的具体类型,最好不要使用或作为自定义类型进行封装。
      有关数据类型转换的详细介绍,请参见MSDN中的"COM 数据类型",自定义类型的描述文档是"自定义标准包装"。如果想让.net组件也像COM组件那样实现回调接口或者连接点,则需要托管接口与该接口的非托管实现之间的通信,这时需要自定义Marshal封送处理,实现这个功能的文档是"自定义封送处理"。

    三、错误处理
      .net客户端没有 HRESULT 类型,.net 服务器中定义方法函数时也可以没有返回值,那么如何处理错误呢?其实这个工作也由 .net 平台的封送机制帮我们做了。
      1、COM 服务器->.net客户端
      客户端直接用try()...catch()方式即可得到翻译过的HRESULT信息,系统定义的错误信息在 .net 类库中一般都有定义好的现成类,处理起来很方便。
      2、.net服务器->COM客户端
      客户端可以像通常那样 HRESULT hr=...; 的方式得到错误信息。
    当服务器端不支持丰富错误信息接口 IErrorInfo 时,错误信息将按照下表所示进行双向的翻译。



      当服务器端实现了丰富错误信息接口 IErrorInfo 时,错误信息将按照下表所示进行双向的翻译,由于表的内容太多,这里只列出其中的一部分。



      这部分内容和上表在 MSDN 中的描述文档是“HRESULT” 和异常,有关.net的异常处理,参见 MSDN 文档“处理和引发异常”。

    四、总结
      第一篇主要介绍了一些概念,给大家一个思考问题的思路,具体细节没有进行详细讨论,而是给出MSDN中的位置,大家需要的可以去参考参考,建议使用至少MSDN2003以上版本的中文版(翻译的还不错^_^)。这些知识在实践中大多都被编译器隐藏起来了,所以作用并不大,了解一下即可。该篇没有示例代码,下一篇我们就要开始进行实战啦^_^

    COM 服务器的 P/Invoke 方式

    下载源代码
     

    一、COM 服务器 --> COM 客户端
      这是传统的 COM 知识,如果对这部分内容不清楚,可以去看 杨老师 的个人专栏,那里有非常棒的教程,我就不在这里废话了^_^
      我不细说可并不代表这部分不重要,恰恰相反,如果读者对这部分很熟悉,就会发现后面所有的内容在形式上几乎都是模仿传统的COM调用。

    二、COM 服务器 --> .net 客户端
      嗯,这才是重点。下图是这部分的原理。每个COM对象都会有且只有一个运行库可调用包装(RCW)代理,而不管它有多少个引用。


     

      在没有公开接口(或者根本就没有)的情况下



     

    这种情况用到的操作就是P/Invoke。我们至少要知道如下二个内容:

    1. DLL文件的名称
    2. 将要调用的函数的名称或者序号;
      然后需要做如下二个步骤:
      • 在.net程序中标识它,必须是静态的、外部的
      • C++得这样:extern "C";像调用普通函数那样调用它;
      对于参数要注意:
      • 如果是结构或类,注意内部成员必须定义为public,才能公开
      • 可以应用一些属性来实现“个性化”,详见下面的“个性化”属性代码;
      如果要调用的函数有很多,或者想将这个函数成为托管类的成员,可以使用包装类:
      • 直接在现有类内声明 DLL 函数;
      • 使函数相互隔离,易于查找,可以分别为每个 DLL 函数创建一个类;
      • 为了形成逻辑分组并减少系统开销,可以将一组相关的 DLL 函数写进一个类;

    我们一起来看示例代码,首先来个简单的调用步骤演示,调用Win32API提供的 MessageBox():

    //1.先写好要用到的命名空间
    using namespace System::Runtime::InteropServices;
    typedef IntPtr HWND; //就是非托管类型 void * 啦,win32平台上是4个字节,所以也可以写成int
    
    //2.利用DllImport属性(即DllImportAttribute属性类)"#import"导入DLL文件,并标识调用函数
    [DllImport("user32", EntryPoint="MessageBoxA")]
    
    //3.创建原型,请读者注意数据类型的变化
    extern "C" int MsgBox(HWND hWnd,String*  pText,String*  pCaption,unsigned int uType);
    
    //4.调用
    MsgBox(this->Handle,"hello","hi",0);
    
    //5.包装类这样写,很简单,就不写进提供下载的示例代码了^_^
    public __gc class SDKMsgBox: {
    public:
    [DllImport("user32", EntryPoint="MessageBoxA")]
    extern "C" int MsgBox(HWND hWnd,String*  pText,String*  pCaption,unsigned int uType);
    .......
    }
    

    如果传递的值是数组、结构或者类,就没这么简单了,需要自定义封装(即Marshal,进行自定义类型转换)

    //对于数组,只需定义一下封送方法即可,就不写入供下载的示例代码了
    extern "C" void SendArray(
    	[MarshalAs(UnmanagedType::LPArray)]Array<int> list,
    	int length 
    	);
    /*
    对于结构,比如,User32.dll中的这个函数
    BOOL PtInRect(const RECT *lprc, POINT pt);
    RECT和Point是两个结构
    
    注意,下面Point声明为__value而非__gc,因为.net v1.1的封送处理会出现问题(可能是笔者失误,
    也可能是.net v1.1的Bug),不能自动将托管指针所指向的内容复制到非托管堆中(不是指__box打包
    功能哦),所以实际使用中没有像上例的参数(String *)那样使用托管指针。
    */
    //StructLayout即StructLayoutAttribute属性类,是用来定义对象的内存布局的
    //Sequential表示对象的成员按照定义的顺序进行内存布局
    [StructLayout(LayoutKind::Sequential)]
    public __value struct Point {
        public:
    	int x;
    	int y;
    }; 
    //Explicit表示对象的成员按照FieldOffset(即FieldOffsetAttribute属性类)指定的位置进行内存布局
    [StructLayout(LayoutKind::Explicit)]
    public __gc struct Rect {
    	public:
        [FieldOffset(0)] int left;		//FieldOffset()中的数字是内存布局,
        [FieldOffset(4)] int top;		//一定不能写了,这里都是int,所以每次都+4
        [FieldOffset(8)] int right;
        [FieldOffset(12)] int bottom;
    };
    [DllImport("User32.dll")]
    extern "C" bool PtInRect(const Rect& r, Point p);//第1个参数定义成托管则规定1级间接寻址要使用引用
    /*
    如果COM服务器方的参数里有char *text,最好如下定义属性
    [StructLayout(LayoutKind::Sequential,CharSet=CharSet::Ansi)]
    ... ( ... , String *text);
    其它的类型依此类推
    */
    }
    

      类的传递方法没什么好说的,自然是和结构的传递方法相同。但有一点要注意,传递类时通常要有至少1级间接寻址,即指针(和上例中的Rect一样)。

    /*
    比如,Kernel32.dll中的这个函数
    void GetSystemTime(SYSTEMTIME* SystemTime);
    把SYSTEMTIME看成类,结构和类本来就是“同根生”嘛^_^
    */
    [StructLayout(LayoutKind::Sequential)]
    public __gc class MySystemTime {
        public:
    	unsigned short wYear; 
        	unsigned short wMonth;
        	unsigned short wDayOfWeek; 
    	unsigned short wDay; 
        	unsigned short wHour; 
        	unsigned short wMinute; 
        	unsigned short wSecond; 
        	unsigned short wMilliseconds; 
    };
    [DllImport("Kernel32.dll")]
    extern "C" void GetSystemTime(MySystemTime& st);
    

    如果想使用回调函数,那就更麻烦了,需要用到委托/事件机制来接收消息。

    using namespace System::Runtime::InteropServices;
    //定义一个委托
    __delegate bool CallBack(int hwnd, int lParam);
    
    [DllImport("user32")] 
    extern "C" int EnumWindows(CallBack* x, int y); 	//参数CallBack从函数指针变成了委托,其实它们大同小异
    
    //回调函数,在调试窗口显式窗口句柄
    bool Report(int hwnd, int lParam) {
        System::Diagnostics::Trace::WriteLine(hwnd.ToString(),"Window handle is:");
        return true;
      };
    
    //使用
    //实例化一个委托myCallBack
    CallBack* myCallBack = new CallBack(this, &EnumReport::Report);
    EnumWindows(myCallBack, 0); 	//将函数指针(实例化的委托)传给COM服务器,COM服务器会自动调用它返回结果
    

    如果想使用回调接口或连接点,看清本节的标题啦,根本就没有接口,怎么做啊?呵呵。

    好啦,P/Invoke 差不多能干的就这些啦,下面我罗列了一些有用的表格。几个常用的 Win32 API DLL


    可用的属性,通常使用 DllImportAttribute( [DllImport(...)] ) 来设置值


     

      本节大多内容可以在MSDN2003以上版本的“使用非托管 DLL 函数”中找到。“个性”化封送处理(仅COM服务器-->.net客户端)请参见“用平台调用封送数据”,平台调用即P/Invoke。

    分享到:
    评论

    相关推荐

      连接COM与.NET的桥梁(二)——COM服务器的P/Invoke方式

      在“连接COM与.NET的桥梁(二)——COM服务器的P/Invoke方式”中,作者深入讲解了如何利用P/Invoke来调用COM服务器的方法。首先,你需要了解COM服务器,它是一种COM对象,可以在不依赖特定运行时环境的情况下被不同...

      连接COM与.NET的桥梁(三)——COM服务器的COM Interop方式

      这部分的示例代码叫做ComP6srcDNet中的ComP5工程,呵呵,有点眼熟吧,其实我的目的只是想介绍.net部分,所以COM和MFC部分引自杨老师的“COM 组件设计与应用(七)——编译、注册、调用” ,只有Use_Net的代码是我写...

      实例71. 如何在VB.NET建立数据库连接_vs.NET_VB.net_数据开发_数据库_vb数据库_

      4. **DataAdapter对象**:它作为桥梁,将数据库连接、Command对象和数据集(DataSet)连接起来。它可以填充DataSet,或者通过Command对象更新数据库。 在VB.NET中,还可以使用Dataset和DataTable来处理离线数据。...

      SAP .Net Connector -- ASP.NET 连接 SAP 系统

      ### 三、ASP.NET与SAP系统连接的应用场景 ASP.NET是Microsoft推出的用于构建动态Web应用的框架,结合SAP .Net Connector,可以实现在Web应用中集成SAP业务逻辑的目标。具体应用场景包括: 1. **数据同步**:自动或...

      .NET的odbc连接技术

      ODBC驱动程序是实现这种交互的关键,它们充当了.NET应用与不同数据库之间的桥梁。本篇文章将深入探讨.NET中ODBC连接技术的使用方法、重要概念以及实际操作代码示例。 首先,我们需要理解ODBC的工作原理。ODBC建立了...

      flex与.net通讯中间件

      中间件在Flex与.NET之间的作用主要是作为数据传输的桥梁,负责处理数据的编码、解码以及传输安全等任务。它使得Flex客户端能够调用.NET服务端的方法,获取和更新服务器的数据,同时使.NET服务器能够向Flex客户端推送...

      ADO.net数据库连接示例程序

      1. **Connection对象**:它是ADO.NET中与数据库建立连接的核心组件。通过创建SqlConnection对象(对于SQL Server)或OracleConnection对象(对于Oracle)并设置正确的连接字符串,我们可以打开和关闭到数据库的连接...

      ASP.NET连接MySQL的驱动

      为了实现ASP.NET与MySQL的连接,我们需要一个特定的驱动程序,这就是所谓的“ASP.NET连接MySQL的驱动”。 驱动程序,或称为数据提供者(Data Provider),在.NET环境中通常是ADO.NET的一部分,它负责在应用程序和...

      连接 COM 与 .NET 的桥梁(三)——COM 服务器的 COM Interop 方式VC源代码

      这部分的示例代码叫做ComP6srcDNet中的ComP5工程,呵呵,有点眼熟吧,部分,所以COM和MFC部分引自杨老师的“COM 组件设计与应用(七)——编译、注册、调用” ,只有Use_Net的代码是我写的,我在这里多谢杨老师啦,...

      lua与.net结合的教程

      #### 二、LuaInterface:连接Lua与.NET CLR的桥梁 - **定义与作用**:LuaInterface是一个库,用于在.NET CLR环境中实现Lua脚本语言的功能。它使得Lua能够消费.NET平台上的对象和服务,并且允许.NET应用程序嵌入Lua...

      精通C#与.NET 4.0数据库开发PPT

      《精通C#与.NET 4.0数据库开发》是一份深度探讨C#编程语言与.NET Framework 4.0平台下数据库应用开发的教程。这份PPT涵盖了从基础到高级的数据库开发技术,旨在帮助开发者全面掌握C#在数据库领域的应用。 1. **C#...

      vc知识电子杂志47期

      Backoff,Polynomials和中译英一则 Cell插件在J2EE系统中的应用 MANAGED SPY:用我们的新工具继续Spy++对Windows Forms的神话 Python 的数据库操作 ...连接 COM 与 .NET 的桥梁(三)——COM 服务器的 COM Interop 方式

      ASP.NET连接SQL Server数据库的实现.pdf

      ***连接SQL Server数据库的实现主要涉及到***数据访问技术,以及.NET Framework数据提供程序的使用。以下是对本文档提供的知识点的详细阐述: ### ***组件分析 ***是Microsoft公司为.NET框架开发的应用程序提供的...

      连接 COM 与.NET 的桥梁(二)——COM 服务器的 P/Invoke 方式VC源代码

      由于传统的 COM 技术使用静态的非托管编程,使用动态的托管编程,所以这个题目本质上讨论的是托管与非托管代码之间的互操作中的一个特例。目前的 .net v2.0 提供以下三种互操作...... 关键字:com,.net,p/invoke,...

      Asp.net core2.2利用Entity Framework Core连接Mysql数据库

      DbContext是EF Core中的主要工作类,它是应用程序与数据库之间的桥梁。创建一个新的类继承自`Microsoft.EntityFrameworkCore.DbContext`,然后在这个类中声明与数据库表对应的实体类的DbSet属性。 ```csharp public...

      vb.net连接数据库方法

      ### VB.NET连接数据库方法详解 #### 一、ADO.NET与ODBC.NET简介 **ADO.NET** 是 Microsoft ActiveX Data Objects (ADO) 的升级版本,它提供了更为强大的数据处理能力,支持离线数据处理和高性能的数据读取。ADO...

      VB.NET与数据库开发

      5. 数据适配器与数据集:DataAdapter是桥梁,将数据库连接、SQL命令和DataSet连接起来。它负责填充DataSet,并将DataSet中的更改同步回数据库。DataSet则作为一个离线数据存储,可以在应用程序内存中存储和操作数据...

    Global site tag (gtag.js) - Google Analytics