凝视着COM的深渊

2020-06-24 14:48:42

如果你知道软件是如何在Windows上开发的,你很有可能最终会遇到COM。虽然Windows公开了一些简单的CAPI(这些API比同时代的工具箱、X或直觉要好得多),但是几乎任何比USER32更复杂的东西都是通过COM接口公开的,从Internet Explorer到DirectX。COM也用于扩展应用程序:Office、Visual Studio,甚至Windows shell都为应用程序提供了COM接口以供挂钩。微软喜欢在Windows中的任何东西上都使用COM,即使第三方不太喜欢它(通常是出于可移植性/复杂性的原因)。最大限度地使用COM可以使您的应用程序更闪亮34%,但是要正确使用这些接口需要做大量的工作(而且有很多这样的接口!)。

不幸的是,能够利用COM的人不多,更不用说在更模糊的情况下(如扩展shell)。正如斯波尔斯基指出的那样,能够做到这一点的人相当罕见。自然,我被一位朋友抨击要编写一个shell命名空间扩展,这是COM扩展中比较模糊的类别之一(文档很少,做过的很少,也很少知道它们的存在)。shell名称空间扩展向shell添加一个名称空间(基本上是虚拟文件夹),其中包含更多的对象。它们的常见用例包括电话MTP、内联ZIP文件查看等。

我的扩展非常简单,我认为我可以实现它(我做到了……。很困难,正如你会看到的)我自己。它将枚举所有打开的Windows资源管理器窗口,并将指向它们的链接放入一个名称空间中。关键是这可以从常用系统文件对话框中访问,如果您打开了大量资源管理器窗口,但想要快速保存到其中之一,这将非常有用。这是受到OS/2 Workplace Shell特性(OS/2的一个好特性!)的启发。挑战是从对COM的最低限度的了解转变为只知道足够危险的知识,能够创建有效的shell命名空间扩展。

对于具有近乎传奇的复杂性名声的东西,事实证明COM实际上是基于一些非常简单的原语。COM基于实现vtable(一种功能丰富的结构)的接口(以C++方式)。这些接口具有固定的形状,因此可以从C使用它们。每个COM类都实现接口IUnnow,该接口实现三个功能:

AddRef,它递增引用计数。每个COM对象都是引用计数的,所以当您复制挂起的引用时会用到这一点。

Release,这会递减引用计数。当计数达到零时,该对象释放自身。

QueryInterface,它为您提供另一个接口的vtable(如果该对象实现它的话)(强制转换)。接口由GUID标识。

这还不算太糟。当然,对象可以实现很多接口,并且因为接口具有固定的形状,所以以后要扩展接口,您必须创建另一个接口。微软最终会给它们编号,所以你会遇到有IShellFolder2的情况。而且,随着微软实现更多需要更多接口的功能,如果您不小心,类可能会变得笨拙。然后,您必须假设接口有很好的文档记录!就可扩展性而言,调试并不比printf宏更重要(据我所知)。

COM类是注册的(这就是REGSVR32的用途),其他应用程序在那里知道它们。类型库提供元数据,通常由IDL文件生成。

虽然您可以在C中使用COM,但它可能会有点笨拙,因为COM受益于RAII和作用域析构函数的环境。ATL为用于C++的COM提供了基于模板的包装,这是Microsoft推荐用于COM开发的。Visual Studio在为COM类类别生成样板方面提供了很大的帮助。

Windows的美妙之处在于它的可扩展性。Windows的丑陋之处在于没有人能正确地扩展它。尝试从头开始编写shell命名空间扩展是一项西西弗式的努力。我认为没有人做过这件事。取而代之的是,你必须像2002年普通的Windows程序员一样去做--阅读比你聪明得多的人关于CodeProject的文章,人们在Stack溢出之前就是从CodeProject复制粘贴的。他的例子很有帮助,但它们有一个严重的缺陷-它们自己实现列表视图,而不是委托给接口来处理股票One及其默认行为的使用。(例如,该示例将在XP和更新版本上崩溃,因为它不处理新的ListView视图。)。很有洞察力,但又回到了绘图板上。

第二轮。帕斯卡尔·赫尔尼(Pascal Hurni)的例子虽然略显粗糙,但更接近我们想要的。他的示例使用了系统ShellView,它为您提供了默认行为并能够从常用文件对话框中使用它,这正是我们想要的。该示例通过注册表枚举(具体地说,文件管理器Directory Opus的收藏夹),并表示真实的文件系统实体,这与我们想要的很接近-只需换出枚举器即可。

我使用了一些我编写的代码来尝试实际列出资源管理器窗口。首先,我认为我必须枚举所有可见窗口并过滤CabinetWClass,然后找出要向其发送哪些消息才能获得有用的信息,但事实证明,通过COM可以采用一种更简单的方法。您创建一个IShellWindows实例,然后调用get_count和item,这将返回一个表示您的窗口的IDispatch。IDispatch本质上是带有反射的IUNKNOWN,适用于像VBA这样要枚举方法和属性的情况。我们可以将其转换为IWebBrowser(Internet Explorer和Windows Explorer焊接在一起时的残余),并从那里获得位置。把它嫁接到Pascal的例子上(为了清晰起见,去掉了我不需要的代码),我有了一个可以工作的MVP(当然,有bug)。

我崇拜货物(学习的第一步)了解了一些关于贝壳的东西,还有更多的东西我不知道。冰山一角如下:

PIDL基本上是资源管理器用来表示项的ID,因为实体可能没有真正的FS表示。PIDL基本上是一个自由格式的结构,它用它的大小标记您想要的内容,以及您的NSE将用什么来标识项目。它们可以是绝对的,也可以是相对的(只与您有关),就像路径一样。

它喜欢以IDataObject的形式请求包装的PID,IDataObject是剪贴板通常使用的可以包含任何内容的OLE结构之一。

名称空间可以在交叉点注册,这使其可以从其他位置访问,而不是通过快捷方式访问其CLSID(GUID)。注册名称空间也有一些仪式;有一个注册表资源脚本,每当注册DLL时都会调用该脚本,以使其更容易。这里提供了一些关于创建名称空间(但从定制者的角度来看,因此将其指向现有位置)以及在某些地方使其为人所知的信息。

有些软件(咳嗽、Office)需要NSE表示真实位置,然后才会显示。该示例通过将临时目录用作物理表现形式来解决此问题。这可能会导致奇怪的行为,但这是让它发挥作用所要付出的代价。

在XP生命周期的后期,特别是Vista,微软扩展了列方案,使其具有属性键。它们更忠实地表示元数据,可以扩展,并且被积极地用于搜索。当然,名称空间扩展可以实现这些。然而,这并不清楚什么是你实现的最起码的东西,像平铺视图字幕工作。您几乎认为您需要一个表示自定义元数据列的IPropertyStore和XML文件,但是我似乎足够幸运,我只需要使用内置的属性键,并且(我相信)真正的文件系统实体已经填充了它们的元数据。我只需将我正在使用的列ID(大多数;我没有注意到任何不好的地方,因为没有映射所有内容)映射到系统,包括属性键,并处理解析为其他属性键的属性键,以确定要在瓷砖上显示的内容等等。

真正令人难过的是,对于这样的事情,因为文档是如此缺乏/缺失,您可能会遇到甚至连Stack Overflow都不能帮助您的问题。可能需要对古老的Usenet帖子进行试验或盲目信任。或者,也许你可以幸运地认识一个人,他在那里,记得,并且仍然在乎。请记住,当时知道如何有效利用这些的人不多,而且这样做的人数减少到了灭绝的地步,这也是Windows文化失败的另一个原因。

我还发现有人实现了包装器类库和周围的一堆示例,如果我在实际制作之后没有发现它,这可能会很方便。它可能很有用,但对您有很大帮助,所以最好还是理解shell/COM在较低级别上是如何工作的。

我设法实际编写了扩展。它可以在GitHub上找到,我希望它能为shell命名空间扩展提供一个更清晰的例子(因为你可能找不到很多,更不用说很多知道它的人了),并且对资源管理器狂热者有用。我还联系了帕斯卡,询问许可的模棱两可的问题(因为当时人们只是坐在座位边上的Windows圈子里开源代码)--它肯定是麻省理工学院许可的。现在你知道ISause是怎么做的了,它是美味的-只要人们能想出最好的吃法就好了。