COM Tcl

1、简介

COM(Component Object Model)即组件对象模型,是Windows中较为普遍的一种技术。它是软件组件之间的二进制接口的规范和体系结构,它:

  • 定义用来在独立开发的组件之间共享数据和方法的接口;
  • 允许通信组件驻留在单个进程或系统上的不同进程中,甚至不同系统上
  • 与具体的编程语言无关

许多App中的技术,包括Windows本身都是基于COM的。编写库的软件开发人员更喜欢使用COM,因为它允许他们的产品可以使用多种编程语言。例如Microsoft Excel应用,使用COM作为与其他应用程序集成的方法,也作为软件组件提供它们的功能。Windows操作系统本身也使用COM作为提供服务的基础,从桌面(如Windows Shell)到系统管理服务(如WMI和ADSI)都支持COM。

2、COM相关概念

2.1、 Interfaces

一个COM接口是一系列相关方法(operations)和属性(data)的集合,通常称为成员,一同定义了一组服务或方法。接口定义没有说明这些是如何实现的。举例来说,一个远程文件访问的接口定义,定义了能够对远程文件进行的操作,但实际上可能会有多个遵循该接口定义但使用不同协议(FTP,HTTP等)的实现。

2.1.1、IIDs

接口定义由接口标识符唯一标识,IID(Interface identifier),是一个由128-bit位组成的GUID。IID有一个关联的名称,但是严格上来说这只是为人类可读性而设计的,COM基础设施内部并会不使用。

如果你想要实现自己的COM interface,你可以通过Microsoft的guuidgen.exe工具,或者通过Tcl下TWAPI包的twapi::new_guid命令

2.2、 Coclasses

一个coclass实现一个或者多个COM接口,一般使用更通用的术语class来指代coclass。

2.2.1、CLSIDs

与interfaces定义一样,calsses使用一个称为CLSID的GUID来作为唯一标志。但是,和interfaces不同的是,只有当应用程序需要显式地创建CLSID的实例时,类才需要关联CLSID。当实例作为其他操作的一部分隐式创建时,不需要CLSID。

2.2.2、PROGIDs

classes同样拥有一个对应的称为PROGID(program identifier)的人类可读的名称。不同于CLSID,PROGID不能保证唯一性。冲突的可能性相对较小,大多数应用程序,包括我们的示例,都是为了方便而使用它们。使用TWAPI,你可以在CLSIDs和PROGIDs之间随意转换

% progid_to_clsid InternetExplorer.Application
→ {0002DF01-0000-0000-C000-000000000046}
% clsid_to_progid 
→ InternetExplorer.Application.1

2.2.3、Versions

注意到上面实例中clsid_to_progid后,PROGIDs尾部带了一个整数。这是因为一个组件通常有一个与之关联的版本号,这个整数就是版本号。当新版本的class release后,版本后就会增加。但是,请注意,如果公共接口的CLSID保持不变,类实现者就没有更改方法或属性的自由。

2.3、Objects

Objects是COM class的一个实例。继续用远程文件举例,一个COM object代表一个特定的远程文件。它里面封装的数据,例如文件大小,与该文件的属性对应,对其调用的任何方法都将对该文件进行操作。

2.4、COM applications

很多时候,COM组件实现了相关的功能。这些组件需要共享远程激活、安全等配置的公共设置。为了简化管理员分别配置每个组件的任务,COM提供了一种将组件分组为“应用程序”的机制。

2.4.1、AppIDs

每个COM组件都通过它的CLSID和一个COM应用的AppID与该应用相关联。所有的公共配置信息都保存在这个AppID中并共享个它的所有组件。

2.5、Components, servers and clients

一个COM组件通常是一个二进制,要么是一个DLL或者是一个可执行文件,它实现了一个或者多个classes。服务器承载COM组件,而使用它们的服务的应用程序是COM客户端。COM服务器根据它们相对于客户机驻留的位置被分为三类。

  • 进程内服务加载在动态连接库中实现的组件,与客户端运行在同一个进程中;
  • 本地服务器则是与客户端运行在同一个系统的不同进程;
  • 远程服务器则是与客户端运行在不同系统下。

COM的一个特性是以上的场景对于客户端来说是透明的。无论如何,它都以相同的方式使用COM服务器组件。

2.6、Monikers

Monikers(绰号,别名)是一个COM对象,它的唯一目的是用来识别另一个COM对象。要标识的对象可能与特定Excel工作簿中特定工作表中的特定文件或特定单元格范围一样简单。对象本身可以是分层的,也可以是由其他对象组成的。名称必须适用于所有这些情况,因此它们的功能并不像乍一看那么简单。

Monikers通过IMoniker接口提供方法。对于我们来说只需要知道:

  • Monikers具有序列化的字符串表示形式,称为显示名称,它定义了一个独特的COM object。例如,字符串winmgmts:Win32_Service='rpcss'唯一标识了与RPCSS服务对应的WMI对象。
  • 此字符串可用于实例化被命名的对象

2.7、接口定义语言

对于像C和C++这样的编译语言,客户端访问COM对象所需的代码是从接口的定义生成的。这些定义是通过IDL(Interface Definition Language)编写的。midl编译器根据接口定义生成所需的(例如)C代码。对于脚本语言,不需要IDL和midl编译器(因为不需要编译)。

2.8 COM自动化

在过去,使用IDL编译器生成的用于连接COM组件的源代码对于像VBScript和Javascript这样的脚本语言来说是不合适的,理由如下:

  • 就其本质而言,这些语言没有编译/链接步骤,任何代码生成都必须在运行时上完成。除了增加脚本语言的复杂性之外,还存在一个基本问题,即包含接口定义的MIDL文件不是COM组件常用的东西;
  • 通用COM接口是基于C/C++的,它可能会使用到各种数据结构,但在脚本语言却没有合适的等价的结构;
  • 用编译语言编写的程序应该预先“了解”它们使用的任何库或组件。另一方面,脚本为了保证足够灵活,不需要满足这个要求,它甚至可以与将来发布的COM组件一起工作。

为了解决这些问题,Microsoft创建了接口定义IDispatch和COM组件所遵循的允许从脚本语言调用它们的规则。这种技术统称为COM自动化、ActiveX或IDispatch接口。

在大多数情况下,脚本语言,包括Tcl的twapi_com包,只能与支持自动化接口的COM组件一起工作。幸运的是,大多数非应用程序内部的、供第三方使用的COM组件都支持这些接口。这其中包括了微软的Office应用程序以及像WMI这样的操作系统组件。

这是对这个问题的一个非常简化的、不完全准确的总结,但对我们的目的来说已经足够了。例如,脚本可以访问类型库,这些类型库包含IDL文件中包含的信息的二进制形式。

3、自动化客户端

虽然从编译语言(如C)中使用基于IDispatch的自动化接口可能有点麻烦,但由于脚本语言隐藏了大部分的复杂性,可以很容易地访问IDispatch。

我们用Internet Explorer举例来说明COM自动化使其功能可用的方式。

3.1、实例化一个Object

我们通过TWAPI的comobj命令和PROGID来实例化一个COM object。这将返回一个包装实例化COM objec的Tcl对象。这个包装器对象公开底层COM对象的所有方法和属性并且实现了一些自己的补充方法。

为了将它们与底层COM对象的方法和属性区分开来,按照惯例补充方法以-字符开头。

% package require twapi_com
% namespace path twapi
% set ie [comobj InternetExplorer.Application]
::oo::Obj26

这将会创建一个新的IE组件实例,虽然你看不到新的IE窗口,但是你可以通过任务管理器查看到一个新的IE进程。

3.1.1、attach到一个已存在的COM实例中

前面的示例启动了一个新的IE实例,并且创建了一个COM自动化对象。有时候,你可能需要attach到一个已存在的自动化对象。例如,你想控制一个用户已经开启了的Excel应用。此时,你可以通过-active选项,来获取一个正在运行中的Excel实例

set xl [comobj Excel.Application -active]

为此,组件必须已经在运行,并且必须在系统维护的正在运行的对象表中注册。例如,Microsoft Office应用程序就可以做到这一点。相比之下,Internet Explorer则不行。

3.1.2、通过monikers获取自动化对象

假设我们想要一个可以用来操作特定Excel文件的自动化对象,一种方法是使用comobj Excel.Application创建一个新的自动化对象,然后使用它的方法加载感兴趣的Excel文件。这将返回一个新的自动化对象,该对象的方法可以用于操作文件。

有一个更简单的方法就是通过comobj_object命令和一个用来指定文件的moniker参数,对于文件,moniker字符串就是文件的完整路径。

% set xlfile [comobj_object [file join [pwd] scripts sample.xlsx]]
→ ::oo::Obj99
% $xlfile Name
→ sample.xlsx
% $xlfile Path
→ C:\src\tcl-on-windows\book\scripts
% $xlfile -destroy

通常,Windows会计算出用于根据文件扩展名打开文件的应用程序(在本例中是COM组件)。

WMI一章包含了许多用来演示名称的例子。

3.2、使用属性

COM对象的属性包含与该对象关联的可公开访问的数据。IE拥有一个Visible属性,指示了visibility状态。我们可以对包装好的COMOBJ使用-get方法

% $ie -get Visible
→ 0

这就解释了为什么看不到IE窗口,非只读的属性可以使用-set方法来修改它的值

% $ie -set Visible 1

在实践中,可以忽略-get和-set,因此下面也可以使用

% $ie Visible
→ 1
% $ie Visible 1

惟一需要-get-set的时候是在不确定引用名称(本例中是Visible)是属性还是方法的时候,COM自动化为方法提供了单独的名称空间,属性检索和属性设置函数。因此,该对象也可以有一个称为Visible的方法,-get-set消除属性操作和使用-call表示的方法调用之间的歧义。

然而,这种情况是罕见的,因此在绝大多数情况下,没有必要消除歧义。

使用显式调用(如-get)可以大大加快速度,因为不必搜索方法名称空间。但是,由于方法和属性查找是缓存的,所以在重复调用的情况下这种差异会得到缓解。

3.2.1、默认属性

在像Visual Basic这样的语言中,对象可以自己调用。例如,假如没有指定任何方法或属性,那么就会转换为被返回对象标记为其默认属性的属性。在Tcl中,通过-default方法可以获取。

在IE中,默认属性就是Name属性:

% $ie Name
→ Windows Internet Explorer
% $ie -default
→ Windows Internet Explorer

3.3、使用方法

方法的调用方式基本上与访问属性的方式相同。

% $ie -call Navigate http://www.microsoft.com

和属性一样,我们通常可以省略-call同时自动化方法对大小写不敏感,因此:

% $ie navigate http://www.microsoft.com

同样可以工作。

虽然对COM对象来说方法和属性都是大小写不敏感的,但是对于COMOBJ封装来说不是,因此-destroy方法不能用-Destroy代替。

3.4、调用链

当你使用COM时,你可能经常需要访问一些深深嵌套在其他对象里的对象。你需要检索到中间对象,再导航到你想要的对象,最后丢弃中间对象。

举例来说,下面的代码填充了一个Excel表格

% set xl [comobj Excel.Application]
→ ::oo::Obj113
% $xl Visible true
% set workbooks [$xl Workbooks]
→ ::oo::Obj119
% set workbook [$workbooks Add]
→ ::oo::Obj124
% set sheets [$workbook Sheets]
→ ::oo::Obj129
% set sheet [$sheets Item 1]
→ ::oo::Obj134
% set cells [$sheet range a1 c3]
→ ::oo::Obj139
% $cells Value2 12345
% $xl DisplayAlerts 0 1
% $xl Quit
% comobj_destroy $cells $sheet $sheets $workbook $workbooks $xl

在上面这个例子里,为了能够修改Excel里的一块区域,我们要导航一堆Objects。开启App,打开工作簿集,打开工作簿集中的一个工作簿,打开表单集,打开表单集里的表单,最终获取到表单里的一块区域。完成数据设置后,要将所有的Objects删除。

作为代替方案,COMBOJ自动化包装器提供了-with方法,用于简化该过程。

% set xl [comobj Excel.Application]
→ ::oo::Obj146
% $xl Visible true
% $xl -with {
    Workbooks
    Add
    Sheets
    {Item 1}
    {Range a1 c3}
} Value2 12345
% $xl DisplayAlerts 0
% $xl Quit
% $xl -destroy

-with的第一个参数时一串方法或者属性,某些属性可能带有参数(如上文中的Item和Range)。列表中的每个方法或属性都对前一个方法返回的对象进行调用,其本身应该返回一个新对象。列表后面的参数(本例中为Value2)是最后一个对象上调用的方法或属性。所有在中间创建的对象都会自动销毁。

3.5、属性

不只是方法,属性也能够接受任意数量的参数。对于属性,它们实际上是索引属性中的索引,但以与方法参数相同的方式传递,在本文中,我们使用后一个术语来引用它们。

向方法传递参数比较简单,我们在这里讨论一些特殊情况。

3.5.1、输入输出参数

略。

3.5.2、可选参数

略。

3.5.3、参数命名

略。

3.5.4、属性类型

略。

3.6、探索方法和属性

当我们使用COM写脚本的时候,我们如何能够知道这个组件提供了那些属性和方法,参数类型默认值?

显而易见的答案是查找组件文档或上网搜索,对于文档记录良好且广泛使用的组件,这通常已经足够了,但有时文档是不可用的或不清楚的。在这种情况下,有两种方法可以获得组件的接口定义,包括组件的方法和属性。

  • Windows SDK附带的oleview程序可以显示关于在系统上注册的每个组件的所有可能的细节,包括它的IDL定义。
  • COMOBJ包装器对象的-print方法将以人类可读的形式显示底层自动化对象的属性和方法。这要求自动化对象实现了提供类型信息的某些接口。
% $ie -print
→ IWebBrowser2
  Functions:
          (vtable 208) hresult Navigate2 1 ([in] variant* URL, [in optional] va...
          (vtable 212) hresult QueryStatusWB 1 ([in] OLECMDID cmdID, [out retva...
          (vtable 216) hresult ExecWB 1 ([in] OLECMDID cmdID, [in] OLECMDEXECOP...
          (vtable 220) hresult ShowBrowserBar 1 ([in] variant* pvaClsid, [in op...
          (vtable 224) hresult ReadyState 2 ([out retval] tagREADYSTATE* plRead...
          (vtable 228) hresult Offline 2 ([out retval] bool* pbOffline)
          (vtable 232) hresult Offline 4 ([in] bool pbOffline)
          (vtable 236) hresult Silent 2 ([out retval] bool* pbSilent)
...Additional lines omitted...

输出在很大程度上应该是不言自明的,但是需要进行一些讨论。

  • 输出将列出该对象支持的所有接口。
  • 对于每个接口,列出了所有方法和属性。
  • 方法和属性都是作为函数调用实现的。函数名后面的整数表示该函数是实现方法(1)、属性检索(2)还是属性集(4)。注意,Offline是一个读写属性,因为有检索和设置条目,而ReadyState是只读属性。
  • vtable NNN表示函数在组件的虚拟分派表中的索引。从我们的角度来看,它没有真正的相关性。
  • 所有函数的返回类型都是hresult,对应于hresult C类型,它表示来自调用的状态代码。在脚本级别,当调用成功时,这是不可见的。在发生错误时,它存储在errorCode全局变量中,并引发一个Tcl异常。
  • 函数的“real”返回值(如果有的话)由参数上的retval属性指示。在Tcl级别上,相应的参数实际上不会作为参数传递给命令。它作为命令的结果返回。
  • 每个参数都用属性标记,这些属性指示它是输入还是输出、可选值、默认值等等。

3.7、删除一个自动化Object

当你完成了对组件的一个操作,你可以使用-destroy方法释放对应的资源。这将破坏COMOBJ包装器对象,并减少对包装的COM自动化对象的引用计数。这并不意味着如果没有对自动化对象的其他引用,则包含该对象的COM服务器将退出。例如,对于Internet Explorer,必须调用Quit方法,否则即使在COM对象被销毁后,进程仍将继续运行。这种行为取决于特定的组件或应用程序。

3.8、Expando objects

略。

4. COM集

略。