前言
这里讨论的IDL(Interface Definition Language)在COM中用于定义接口和组件,它是一种描述性语言,不依赖于任何编程语言。IDL文件定义了接口、方法、参数和返回值等,然后通过MIDL(Microsoft IDL编译器)编译生成一些代码和文件,这些文件可以被不同编程语言使用。
IDL如何生成接口?
-
编写IDL文件:开发者编写一个.idl文件,其中包含接口定义、CoClass(组件类)定义、类型库定义等。
-
MIDL编译:使用MIDL编译器编译.idl文件,会生成以下文件:
-
头文件(.h):包含C/C++语言中接口定义的头文件,其中包含了接口的GUID定义、接口声明(抽象基类)等。
-
类型库文件(.tlb):二进制文件,包含了类型信息,可以被其他语言(如VB、C#、Delphi等)使用来了解接口和组件的定义。
-
代理/存根代码(_p.c, _i.c):用于跨进程/跨机器通信的代理和存根代码,这些代码需要被编译成DLL并注册,以便在跨边界调用时进行列集(marshaling)和散集(unmarshaling)。
-
-
实现接口:开发者根据生成的C++头文件,实现接口的具体类。这个类必须继承自接口(即IDL中定义的接口,在C++中表现为纯虚基类)并实现所有方法。
-
注册组件:将组件的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和自定义接口),则可以通过类型库提供类型信息,实现更高效的调用。
工作原理和机制
-
跨语言兼容性:IDL提供了一种中立的方式来描述接口,然后通过编译生成不同语言需要的绑定代码。这样,不同语言都可以按照同一种二进制标准(COM)来创建和调用对象。
-
类型库的作用:类型库是二进制文件,它描述了接口、方法、参数、返回值、组件类等。它使得那些不直接使用C++头文件的语言(如VB、C#)能够获得类型信息,从而进行早期绑定(在编译时检查类型)和提供智能感知。
-
代理/存根机制:当客户端和组件不在同一个进程空间时,COM需要跨进程通信。MIDL生成的代理/存根代码负责将接口方法的调用及其参数打包(列集)成一种标准格式,通过RPC(远程过程调用)传输到另一个进程,然后由存根代码解包(散集)并调用实际对象的方法,再将结果以同样方式返回。
-
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世界中“通用语言”的角色,打通了不同编程语言之间的壁垒。

962

被折叠的 条评论
为什么被折叠?



