IDL如何生成接口,以及与各种编程语言的关系

前言

这里讨论的IDL(Interface Definition Language)在COM中用于定义接口和组件,它是一种描述性语言,不依赖于任何编程语言。IDL文件定义了接口、方法、参数和返回值等,然后通过MIDL(Microsoft IDL编译器)编译生成一些代码和文件,这些文件可以被不同编程语言使用。

IDL如何生成接口?

  1. 编写IDL文件:开发者编写一个.idl文件,其中包含接口定义、CoClass(组件类)定义、类型库定义等。

  2. MIDL编译:使用MIDL编译器编译.idl文件,会生成以下文件:

    • 头文件(.h):包含C/C++语言中接口定义的头文件,其中包含了接口的GUID定义、接口声明(抽象基类)等。

    • 类型库文件(.tlb):二进制文件,包含了类型信息,可以被其他语言(如VB、C#、Delphi等)使用来了解接口和组件的定义。

    • 代理/存根代码(_p.c, _i.c):用于跨进程/跨机器通信的代理和存根代码,这些代码需要被编译成DLL并注册,以便在跨边界调用时进行列集(marshaling)和散集(unmarshaling)。

  3. 实现接口:开发者根据生成的C++头文件,实现接口的具体类。这个类必须继承自接口(即IDL中定义的接口,在C++中表现为纯虚基类)并实现所有方法。

  4. 注册组件:将组件的CLSID和路径等信息写入注册表,以便客户端可以通过CoCreateInstance创建组件。

与各种编程语言的关系

IDL生成的输出(特别是类型库.tlb)可以被多种编程语言使用,从而实现了跨语言互操作。

  • C++:直接使用MIDL生成的头文件,包含接口定义和GUID。开发者可以基于这些头文件实现接口和组件。

  • Visual Basic:VB可以通过引用类型库(.tlb)来使用COM组件。VB的早期绑定就是通过类型库实现的,这样在编码时就可以有IntelliSense,并且编译时检查类型。

  • C#:在.NET中,可以通过“添加引用”来引用COM组件,IDE会自动从类型库生成一个互操作程序集(Interop Assembly),其中包含了对应接口和组件的.NET定义(通常作为托管接口和类)。这称为COM Interop。

  • 脚本语言(如VBScript, JavaScript):脚本语言可以通过COM的IDispatch接口(后期绑定)来调用组件,或者如果组件支持双接口(同时继承自IDispatch和自定义接口),则可以通过类型库提供类型信息,实现更高效的调用。

工作原理和机制

  1. 跨语言兼容性:IDL提供了一种中立的方式来描述接口,然后通过编译生成不同语言需要的绑定代码。这样,不同语言都可以按照同一种二进制标准(COM)来创建和调用对象。

  2. 类型库的作用:类型库是二进制文件,它描述了接口、方法、参数、返回值、组件类等。它使得那些不直接使用C++头文件的语言(如VB、C#)能够获得类型信息,从而进行早期绑定(在编译时检查类型)和提供智能感知。

  3. 代理/存根机制:当客户端和组件不在同一个进程空间时,COM需要跨进程通信。MIDL生成的代理/存根代码负责将接口方法的调用及其参数打包(列集)成一种标准格式,通过RPC(远程过程调用)传输到另一个进程,然后由存根代码解包(散集)并调用实际对象的方法,再将结果以同样方式返回。

  4. GUID的重要性:每个接口和组件类都有一个全局唯一标识符(GUID),这确保了不同开发者定义的接口和组件不会发生命名冲突,也使得客户端能够明确地请求特定的接口。

示例

假设我们有一个简单的IDL文件:

import "oaidl.idl";
import "ocidl.idl";

[
    object,
    uuid(12345678-1234-1234-1234-123456789ABC), // IID
    dual,
    pointer_default(unique)
]
interface IMyInterface : IDispatch
{
    [id(1)] HRESULT MyMethod([in] BSTR input, [out, retval] BSTR* output);
};

[
    uuid(12345678-1234-1234-1234-123456789ABD), // CLSID
    version(1.0)
]
coclass MyComponent
{
    [default] interface IMyInterface;
};

使用MIDL编译后,会生成:

  • MyInterface.h:C++头文件,包含IMyInterface的抽象基类定义和GUID。

  • MyComponent.tlb:类型库文件。

在C++中,我们可以包含头文件,然后实现IMyInterface接口:

#include "MyInterface.h"

class CMyComponent : public IMyInterface
{
    // 实现IUnknown和IMyInterface的方法
    // ...
};

在C#中,我们可以通过“添加引用”来引用MyComponent.tlb,然后就可以这样使用:

MyComponentClass obj = new MyComponentClass();
string result = obj.MyMethod("input");

IDL是COM技术的核心之一,它通过中立的语言定义接口,然后通过编译生成多种语言所需的代码和类型信息,从而实现了跨语言、跨进程的组件模型。其背后的机制包括类型库、代理/存根、GUID和COM运行时等。

IDL是COM技术栈中实现跨语言互操作性的基石,下面来具体深入探讨一下

一.  什么是IDL

"接口定义语言"是一种用于精确定义软件组件接口的"描述性语言"。它本身不是编程语言,不包含任何实现逻辑。它的核心作用是生成一份所有相关方都能认可的、无歧义的"合同”。

在COM中,使用的是微软的IDL,通常以 `.idl` 为文件扩展名。

IDL如何生成接口?

IDL文件本身并不生成接口的实现,而是生成定义接口所需的代码框架和类型信息。这个过程由一个叫做<MIDL>的编译器来完成。

整个过程可以分为以下几个步骤:

1.1  编写IDL文件

开发者首先编写一个 `.idl` 文件,用它来描述接口。一个典型的IDL文件包含以下内容:
// 引入基础定义
import "oaidl.idl";
import "ocidl.idl";

// 定义接口的IID(全局唯一标识符)
[
    object,
    uuid(12345678-1234-1234-1234-123456789ABC), // 这是一个GUID
    dual, // 这是一个双接口,既支持早期绑定也支持后期绑定
    pointer_default(unique)
]
interface IMyCalculator : IDispatch
{
    // 定义接口的方法
    HRESULT Add([in] LONG num1, [in] LONG num2, [out, retval] LONG* result);
    HRESULT Subtract([in] LONG num1, [in] LONG num2, [out, retval] LONG* result);
};

// 定义实现该接口的组件的CLSID
[
    uuid(12345678-1234-1234-1234-123456789ABD)
]
coclass MyCalculator
{
    [default] interface IMyCalculator;
};

1.2  使用MIDL编译器编译

使用命令行工具 `midl.exe` 来编译这个IDL文件:
midl MyCalculator.idl

1.3  MIDL的输出(生成物)

MIDL编译器会生成一系列关键文件,这些文件是连接IDL定义和具体编程语言的桥梁:

1)头文件(.h): 用于 C/C++。
    *   包含了接口的C++定义,本质上是一个**纯虚类**。
    *   包含了接口ID和类ID的GUID声明。
    *   作用: C++的服务器端(实现者)可以继承这个纯虚类来实现接口。

                     C++的客户端(调用者)可以包含这个头文件来知晓接口的结构。

2)类型库文件(.tlb): 用于非C+语言(如VB, C#, Delphi, 脚本语言)。
    *   这是一个二进制的文件,但它包含了IDL文件中所有的类型信息、接口定义、方法签名等。
    *   作用: 它是高级语言认识COM组件的“说明书”。VB、C#等语言通过“引用”这个.tlb文件,就能在编译时知道接口的存在和方法签名,从而实现早期绑定。

3)代理/存根代码(_p.c, _i.c, dlldata.c): 用于跨进程通信。
    *   当COM客户端和COM对象不在同一个进程空间时(例如,客户端是.exe,对象在.dll中,或者对象在另一台机器上),COM需要一种机制来“打包”和“解包”方法调用的参数。这个过程叫做列集。
    *   MIDL生成的代理/存根代码就是专门负责这个工作的。它们需要被编译成一个单独的DLL(ps.dll)并注册到系统中。

二.  与各种编程语言的关系

IDL/MIDL生成的不同输出,正是为了适应不同编程语言的工作方式。

2.1  与 C++ 的关系

*   工作方式: 直接使用头文件(.h)。
*   工作机制:
   a)   服务器端(实现者): 包含 `.h` 文件,定义一个C++类,公开继承自 `IMyCalculator`,并实           现所有的纯虚函数(包括 `IUnknown` 的方法)。
    b)   客户端(调用者): 包含 `.h` 文件,在代码中就可以直接使用 `CoCreateInstance` 和                   `QueryInterface` 来获取接口指针并调用方法。
*   工作特点: 性能最高,因为调用是直接的虚函数表指针调用。

2.2  与 Visual Basic / VBScript 的关系

*   工作方式: 引用类型库(.tlb)。
*   工作机制:
    a)  在VB IDE中,通过“Project” -> “References”添加对 `.tlb` 文件或包含TLB的DLL的引用。
    b)   之后,VB就能像使用原生对象一样使用COM对象:
        Dim calc As MyCalculator
        Set calc = New MyCalculator
        Dim result As Long
        result = calc.Add(5, 10) ‘ 看起来就像调用本地方法       
    c)  VB编译器在背后通过类型库信息,自动处理了 `CoCreateInstance`、`QueryInterface` 和列          集等复杂操作。
*   工作特点: 极大地简化了COM的使用,生产力高,实现了早期绑定(编译时检查类型)。

2.3  与 C# / .NET 的关系

*   工作方式: 通过“互操作程序集”。
*   工作机制
    a)   在Visual Studio中“添加引用”到一个COM组件时,IDE会自动运行 **TlbImp.exe** 工具,将            `.tlb` 文件转换成一个 **.NET 程序集**。
    b)   这个程序集被称为“主互操作程序集”,它包含了对应COM接口和CoClass的 .NET 定义(如            `interface IMyCalculator` 和 `class MyCalculatorClass`)。
    c)   .NET 客户端代码通过 **RCW** 与COM对象交互。RCW负责将 .NET 调用转换为COM调              用,并管理COM对象的生命周期(将 .NET 的垃圾回收转换为COM的引用计数)。
        ```csharp
        // 在C#中,使用起来和普通.NET对象几乎没有区别
        MyCalculatorClass calc = new MyCalculatorClass();
        int result = calc.Add(5, 10);
        ```
*   工作特点: 实现了 .NET 和 COM 世界之间的无缝桥接。

2.4  与 脚本语言(如JScript)的关系

*   工作方式: 通过IDispatch接口(后期绑定)。
*   工作机制:
    a)   脚本引擎(如ASP中的VBScript/JScript)通常不支持类型库。它们通过COM的 `IDispatch`            接口来动态地调用方法。
    b)   脚本引擎调用 `IDispatch::GetIDsOfNames` 将方法名“Add”转换成一个数字DISPID。
    c)   然后调用 `IDispatch::Invoke`,传入这个DISPID和参数数组,来执行方法。
    d)   如果COM对象是一个**双接口**(像我们IDL例子中的 `dual`),它既支持通过vTable直接              调用(C++/VB),也支持通过 `IDispatch` 调用(脚本)。
*   工作特点: 灵活性高,但性能低于直接vTable调用,且没有编译时类型检查。

三.  工作原理和机制总结

1).  契约第一: IDL是这一切的起点,它定义了一份与语言无关的、精确的二进制契约。

2).  编译时绑定 vs. 运行时绑定:
    *   C++/VB(通过TLB) 属于编译时绑定。它们在编译期就知道了接口的确切内存布局,生成             高效的调用代码。
    *   脚本语言属于运行时绑定。它们在运行时动态地查找和调用方法,更灵活但性能较低。

3).  MIDL是翻译官: MIDL编译器将一份IDL契约“翻译”成不同语言能理解的格式:
    *   给C++翻译成 `.h` 头文件。
    *   给其他语言翻译成 `.tlb` 类型库。

4).  类型库是通用护照: `.tlb` 文件是一个二进制元数据仓库,它让VB、C#等高级语言能够理解          COM组件的类型信息,从而提供IntelliSense、编译时检查等现代化开发体验。

5).  实现与接口分离: 无论用什么语言实现COM服务器(C++、Delphi等),只要它按照IDL生成        的二进制布局(vTable)来实现接口,任何语言的客户端(C++、VB、C#、脚本)都能正确          地调用它。这就是COM跨语言互操作性的精髓。

图示流程
[ 开发者编写 MyCalculator.idl ]
          |
          v (MIDL编译)
          |
+-----------------------+
| .h 文件 | .tlb 文件 | 代理/存根代码 |
+-----------------------+
    |          |               |
    |          |               |
    v          v               v
  C++使用    VB/C#引用     系统用于
 (包含头文件) (添加引用)    跨进程通信

通过这一整套机制,IDL成功地扮演了COM世界中“通用语言”的角色,打通了不同编程语言之间的壁垒。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

千江明月

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值