65.9K
CodeProject 正在变化。 阅读更多。
Home

介绍 Comet

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (49投票s)

2004年1月1日

17分钟阅读

viewsIcon

108624

Comet 如何帮助您在“真正的”C++中创建和使用 COM 服务器,扩展或替换 ATL。

引言

Comet 是一个开源的模板库,由 Sofus Mortensen 编写,Paul Hollingsworth, Mikael Lindgren 和我本人协助。它提供了一套替代的、更完整的 STL 风格的 COM 资源封装类,以及一个用于实现和使用 COM 库的接口封装生成器。

Comet 之前的现状

COM 内存资源管理

我的工作涉及大量的 COMActiveXOLE 控件。其中一些是用 MFC 编写的,一些是用 ATL 编写的,大多数使用 MIDL 生成的头文件,一些使用 #import,还有一些使用 Comet。除了正确理解控件的语义之外,我遇到的最大问题之一是处理 COM 资源;确保字符串、safearrays 和 variants 被正确分配和释放,并确保引用计数得到妥善处理。

如果您了解所有规则,那么在留意的情况下,COM 中的引用和内存管理并非难事。然而,似乎许多 COM 程序员要么没有留意,要么忙于弄清楚程序实际应该做什么(那些天真的傻瓜!)。因此,在没有乱搞内存管理的情况下,编写和维护 COM 代码似乎相当困难。

Microsoft COM 封装器

Microsoft 提供的两个(是的,两个)用于便利内存处理的封装类集,都有严重的缺陷(其中一些缺陷是相同的),它们带来的陷阱和修复的缺陷一样多。我很有把握知道它们所有的缺陷,至少我以前是这么认为的,直到有一天我发现了另一个涉及 [out][in,out] 参数与 Microsoft CComVariant 类之间的不匹配问题。

这些类行为糟糕的两个绝佳例子。第一个是重载的 operator &,它是恶魔的作品,应该被清除。它用于返回内部原始 COM 类型的指针,并使某些函数调用看起来非常整洁。

    CComPtr<IDoSomething> pDoSomething;
    HRESULT hr = pDoSomething.CoCreateInstance( CLSID_CoDoSomething );
    if( FAILED(hr) ) return hr;
    CComBSTR mystring;
    hr = pDoSomething->DoIt(&mystring);
    if( FAILED(hr) ) return hr;

不幸的是,这意味着

  1. 您无法(轻松地)将对象的指针传递给函数调用
  2. 您必须使用糟糕的 CAdapt 类才能将其包含在模板化集合中
  3. ampersand 运算符实际上只能覆盖返回指针的两种可能用途中的一种。

无法传递指针是一件很麻烦的事,您可以使用引用,但它们并不总是合适的。

不得不使用 CAdapt 类似乎很烦人,但这是又一个需要知道的东西,而新手用户不会去查找那本不够充分的文档。这通常会让人想在容器中使用原始 COM 指针(在我看来,这是 C++ 的一个基本罪过)。

Microsoft COM 封装类上的 ampersand 运算符一半是为了提供 [out] 指针支持。调试模式下的断言确保您不传递非空指针,这排除了通过引用传递(VB 中的默认行为),但如果您在没有调试的情况下编译,您很容易忽略这样一个事实:通过不释放包含的资源并将其传递给仅 [out] 参数,可能会导致内存泄漏!

考虑到 COM 封装器的原始性质,一些用于常见方法/函数的类型安全方法,如 QueryInterfaceCreateInstance 是必不可少的,并且应该始终优先于它们的原始对应项。然而,它们需要使用 Microsoft 编译器宏 __uuidof(),该宏允许访问 MIDL 编译器输出的 __declspec(uuid()) 定义,但它们的好处远远超过了我可能对使用它们持有的任何顾虑。

不幸的是,对于 _com_ptr 的实现,Microsoft 团队更进一步,决定使用 VB 指针模型,即两个不同的 _com_ptr 类型之间的赋值总是并且在没有警告的情况下会导致 QueryInterface。即使指针是赋值兼容的!这并不总是我们想要的,因为程序员倾向于在他们的 ATL 接口映射中遗漏基类。

C++ 编程方向

行为良好的类和 STL

STL(标准模板库)为 C++ 类使用提供了一些优秀的模型。在 STL Concepts(接口的模板等价物)的框架内仔细定义容器和算法,可以实现高效且通用的代码生成。虽然 STL 目前可能没有定义一套完整的算法和容器类型,但它提供了一种实现高效模板代码的方法,这反映在 Boost 等代码库的出色工作中,这些库将容器扩展到包含大字符串(ropes),引入了图的概念,并显著增强了 C++ 编程的许多其他方面。

异常安全性、一致的接口以及易用性,使得用户难以误用资源,这些都是行为良好的类/类库的一部分。

可维护性

使用封装类来管理资源是现代 C++ 编程的重要组成部分。一些更具侵入性的编程习惯可以通过设计良好的资源封装器来缓解。这应该能提高代码的可读性,更重要的是,提高代码的可维护性,记住后面的人可能不像您一样注重资源!

一个好的现代编译器应该能够优化掉大多数精心设计的资源封装器,而且异常处理的开销,在我看来,完全值得用于降低维护成本。

异常用于错误管理

在如何处理错误方面,确实存在一个选择。可以选择通过适合每个可能返回错误的函数的枚举和字符串来处理错误,或者将错误类作为返回值,或者使用异常。

为处理错误而设的临时枚举很快就会变得非常乏味,需要编写大量的代码来翻译不同错误之间的关系。错误类在不使用异常的情况下工作得很好,但这意味着任何其他返回值都必须作为参数传递,并且意味着您必须在代码中始终分配和检查错误,如果您忘记了,这并不明显,会导致“这个错误是故意被忽略还是意外被忽略?”的问题。

异常的缺点是您需要在编译器中启用异常处理,这会带来创建大量额外堆栈帧的开销,并且所有可能抛出异常的代码都必须以异常安全的方式编写。然而,优点是

  1. 您可以捕获各种类型的错误(您不受限于单一错误类),
  2. 不同级别的代码可以捕获不同的异常,
  3. 除非您想忽略或处理某种特定情况,否则您不必担心检查错误,
  4. 代码通常更具可读性(“返回”值被回收,并且减少了检查错误状态的需要),
  5. 异常安全的代码更容易被更多人维护(也可随时返回),并且
  6. 在使用 API 函数以异常安全的方式进行时,许多可能需要的工作可以被优秀的程序员隐藏在优秀的类封装器中供所有人使用。

在 Comet 出现之前(BC)的几年前,我已经决定 STL 是正确的方向,并且异常是最佳选择,尽管有开销,即使我曾在一个拥有非常完善的错误返回类的项目中工作过。所以,当我看到 Comet 处于其早期阶段并将其与 #import 进行比较时,我很快就折服了。从那时起,我看到了(并帮助了)Comet 做出了越来越多惊人的事情。

Comet 解决方案

Comet 封装器

Comet 分为两部分,第一部分是 Comet 模板库,我将从这里开始。这些封装器确实使用异常作为处理错误(异常情况)的方式,其优点我稍后会论证。很容易忘记 Microsoft 的第二套 COM 封装器也广泛使用异常,只要方法不返回 HRESULT。Comet 有自己的错误类,它很好地继承了 std::exception,以提供与 STL 兼容的异常。

Comet 的基本规则是,任何操作都不应该是隐式的(例如 QueryInterface 和类型转换),而显式操作应该尽可能不显眼且易于理解。例如

    com_ptr<IViewObject> viewobj = com_cast( obj ); // <-- non-throwing 
                                                   //query-interface
    if (viewobj.is_null())
    {
     com_ptr<IViewObject2> viewobj2 = try_cast( obj ); // <-- throwing 
                                                      // query-interface
     viewobj = viewobj2; // <-- Assignment between 
                        // assignment compatible types.
    }

这显示了三种不同的赋值方式。第一个使用 com_cast,它导致赋值进行 QueryInterface,但不抛出错误。第二个使用 try_cast,它同样导致赋值进行 QueryInterface,但会导致抛出错误。第三个赋值仅用于赋值兼容的对象,除非(在本例中)IViewObject2 继承自 IViewObject,否则会导致编译器错误。

Comet 还将简单但明确的规则应用于访问原始 COM 指针,对此它非常谨慎。没有重载的 operator&,取而代之的是一系列在类之间一致的方法(有效地提供了一种 OLE COM 类型封装器的概念)。它们是

  • in():返回适合 [in] 参数的指针,
  • out():返回适合 [out] 参数的指针,首先释放已分配的内存,
  • inout():返回适合 [in,out] 参数的指针,以及
  • get():与 in() 相同,但使用 STL 风格的指针访问器。

最好的部分是,在完整的 Comet 项目中,即使这些也仅在与标准 OLE 接口交互时才需要。

利用类型库

Comet 的另一部分是从类型库生成接口和实现封装器的能力。这些封装器完全封装了 TypeLibrary 定义的接口和 coclasses,并为它们提供了真正的 C++ 式接口,其中包含所有标准 OLE 类型的封装器,以及对结构和其他更高级概念的一些支持。

接口封装器为接口提供客户端封装器,使用 comet 类型封装器,并返回 [out,retval] 属性,将失败的 HRESULT 作为异常抛出(同时支持 IErrorInfo)。

实现封装器还提供了一个高效的转换层,以便可以使用更 C++ 的风格来实现库、coclasses 和接口,而无需程序员处理任何糟糕的原始 COM 类型(除非他们真的想),并将异常转换为 HRESULT 和关联的 ErrorInfo。(标准 coclass 实现默认支持 ISupportErrorInfo)。

Comet 尽可能多地利用类型库中的信息,从而消除了对 ATL 和 MFC 所必需的、用于指定库实现的 coclass(在类型库中提到)以及可能在 coclass 上预期的接口(同样在类型库中提到)的糟糕宏映射的需求。当然,可以覆盖默认设置,但同样,不需要预处理器宏。

其效果是,一个完整的库实现包括一个 IDL 文件、一个 RC 和项目文件,然后是一个实现文件,看起来像这样

#include "MyTypelibLib.h"

using namespace comet;
typedef com_server<MyTypelibLib::type_library > SERVER;
// Declare the server wrapper
COMET_DECLARE_DLL_FUNCTIONS(SERVER)
using namespace MyTypelibLib;

template<>
class coclass_implementation<MyCoclass> : public coclass<MyCoclass>
{
public:
    // IMyInterface implementation
    bstr_t GetName()
    {
        return m_name;
    }
    void PutName( const bstr_t &newName)
    {
        m_name = newName;
    }
};

这不是夸张,这是完整的文件。最好的部分是,使用 coclass 和接口与使用过程一样简单

#include "MyTypelibLib.h"
using namespace comet;
using namespace MyTypelibLib;
// ...

std::wstring someFunction()
{
    com_ptr<IMyInterface> obj = MyCoclass::create();
    obj->PutName(L"Test");

    return obj->GetName();
}

我会添加一个 ATL 实现的比较,但在添加了实现一个 coclass 所需的所有必要映射和附加内容后,它会变得太大了。(这还不包括 RGS 文件)。

默认和可选参数

对于大多数类型,都支持具有默认可选参数的方法,并且不像 #import 那样仅限于 VARIANT。Comet 的 variant_t 类型也明确支持缺失的参数。

Dispatch 接口支持

IDispatch 仅接口(dispinterface)和源接口的调用和实现与标准的 v-table 接口无法区分。

Comet 使用模板的强大功能来生成类型安全的 C++ 代码,而不是预处理器。虽然 ATL 也使用模板(因此得名),但它只使用几种技术,并将大部分工作交给用户维护的预处理器映射。

连接点

标准的 Microsoft ATL 向导处理连接点的方式很差,不支持按引用传递的 IDispatch 参数,并且 v-table 实现中存在未初始化的变量。

Comet 生成器为类型库中提到的任何连接点提供实现,并支持以用户定义的方式处理错误。当 VB 应用程序使用 v-table 连接点时,检查 NULL v-table 条目也很有用。(VB 在 v-table 中用 NULL 替换未实现的“事件”,非常感谢 Microsoft)。

触发连接点非常简单。如果只提供一个连接点,代码看起来像这样

connection_point.Fire_RaiseEvent(L"Argument");

如果有多个连接点,作用域限定符可以方便地访问事件,如下所示

connection_point_for<IMyEvent>::connection_point.Fire_RaiseEvent(L"Argument");

结构体封装器支持

Comet 甚至包含结构体的智能封装器!一些巧妙的技巧允许结构体对实现者和使用者来说看起来像是具有所有正确的 comet 类型封装器。

Safearrays

Microsoft 留下的又一个巨大的漏洞,尤其是在基于异常的库(第二套 COM 封装器)的背景下,就是缺乏任何 SAFEARRAY 封装器。Comet 通过为 SAFEARRAY 向量(非多维数组)提供适当的支持,在很大程度上填补了这一空白,这些支持符合 STL 的随机访问容器概念。

Comet 的 safearray_t<> 支持大多数类型,包括枚举类型和自定义接口,并在附加 SAFEARRAY 时进行运行时检查,以确保它们是正确的类型。您甚至可以在接口中使用 SAFEARRAY( type) 在 IDL 中指定 safearrays,Comet 会为您封装它们。

这是一个创建和初始化 safearray 向量的示例

    safearray_t<long> longarray(10,0); // 10 items, 0-based vector of longs
    long initvar=100;
    for (safearray_t<long>::iterator it= longarray.begin(); 
                   it != longarray.end(); ++it, ++initvar)
        (*it) = initvar;

摘要

在这些繁多的功能面前,Microsoft 特有的 #import 显得力不从心。事实上,它无法正确处理 IPictureDisp 接口,这让我质疑 Microsoft 团队使用了它多少!我还知道它会生成无法编译的代码,枚举类型在其被接口使用后才定义(尽管它们在类型库和 IDL 文件中更早出现)。

好吧,这有点刻薄。我相信 Comet 也会有一些类似的怪异之处,但这些会被修复,而不是添加到功能列表中。

ATL 兼容性

ATL 拥有构建 ActiveX/OLE 控件和容器所需的大部分支持,Comet 尚未准备好尝试复制这项工作,但 Comet 非常乐意与 ATL 并存。

可以选择的接口(包括 dispatch 接口)可以继承自 Comet 生成的接口封装器,并以通常的 Comet 风格实现方法。

ATL 对连接点的可疑实现也可以轻松地被更健壮的 Comet 版本取代。

还支持将 ATL 风格的 coclass 定义显式添加到 Comet 项目中,因此您可以选择任何一种方式工作!

符合现代 C++ 范式

Comet 的主要目标之一是让程序员能够创建 COM 对象,而不会失去现代 C++ 编程风格的健壮性。

这样,程序员就可以专注于设计,而不是管理 COM 资源这种繁琐的任务,而 COM 资源管理需要使用其中一个或两个 COM 资源封装器集,这些封装器需要一些知识和/或枯燥地查阅勉强够用的文档才能正确操作。

Comet 让使用 COM 的 C++ 程序员重新获得了异常安全的编程环境,并使用了 STL 和 STL 风格的范式。

类型安全的元编程:让编译器为您工作

精心实现的模板库应该能够消除创建高效编译代码的许多困难工作。

通过利用模板引擎作为在普通“类型”上操作的评估引擎,编译器可以基于从模板参数类型中获得的 C++ 来进行编译时实现决策。

虽然这可能导致一些非常晦涩的模板代码,但它可以将一些更深奥的领域知识决策转移到库中,从而解放程序员。它还允许透明地做出效率决策,而无需程序员主动选择使用哪个模板类。

异常安全编程?

异常处理是有代价的,真正的问题是:异常处理包里有什么?

异常处理的整个思想是,它为我们提供了一种应对异常情况的机制。它使程序员不必为每个函数调用编写关键错误检查,它让程序员在可能出现异常情况的领域重新获得 C++ 运算符重载的能力,并且它还让我们能够阅读我们编写的代码!

编写良好、可维护的代码无疑需要纪律,这些纪律通常被编码到编码标准中。不同的编程语言需要不同的标准,包括 C 和 C++。

将此扩展到 C++ 中的不同环境也值得。原始 COM 编程需要一套编码标准或纪律,就像在非异常处理环境中编程一样。MFC COM 也有其自身的纪律,STL 也是如此。使用异常编程自然也有其自身的纪律。

使用异常编程的纪律通常称为“异常安全编程”,我认为它比编写所有错误都以类或错误码形式返回的 C 风格纪律要负担轻得多。

异常的真正成本当然是额外的堆栈帧和异常状态检查。我相信,这在降低实现和维护成本方面是值得的。

关于 Microsoft 异常实现的两个注意事项

  • 首先,Microsoft 违背了标准,允许系统异常被 catch(...) 捕获。这总是会引起问题,因为编译器需要假设每个操作都可能引起异常!其结果是异常可能会绕过异常树中某些析构函数。这可能意味着一些内存泄漏……问题不大……或者可能导致资源锁不被释放,导致多线程环境陷入停顿。
  • 其次,MSVC6 中潜藏着一个仅限于捕获重新抛出的异常的糟糕 bug。其结果是,作为异常抛出的对象被删除两次!幸运的是,这个 bug 似乎已被 VC++.NET 修复。最简短的出现问题的代码看起来如下
    try { throw std::runtime_error("Error"); }
        catch (...){
            try { throw; }
            catch( std::exception ) {}
        }

要避免此类问题的最简单的规则是:如果您 catch 了某些东西但不知道它是什么……请确保将其 throw 回去。

我们知道的未实现的内容

Comet 中仍有一些我们希望增强/实现的功能,但正在等待必要的支持和用户基础。

  • 更详尽的文档
  • COM 类别
  • 扩展窗口和控件支持。
  • 支持合并代理-存根
  • NewEnum 进行更严格的线程支持

我们对类别有一些初步的想法,包括一些预处理,以弥补 Microsoft IDL 文件中对它们的缺乏支持,但这还有待进一步的开发资源和该领域的特定知识。

总结

反对在项目中*使用 Comet* 的最大论点是“未知”因素。公司可以雇佣懂得*ATL* 或 *MFC* 的人,但很难找到懂得*Comet* 的人。我的回应总是“问题是,即使那些声称了解这些环境的人仍然会犯错,而且,找到懂得*STL* 的人很容易。”。

要完全正确地管理 COM 资源,确实非常困难。对于不警惕、疲倦、懒惰或在压力下交付的人来说,存在许多陷阱。声称过去一个月里从未落入这四类中的至少两类的人,很可能在撒谎,或者在自欺欺人。

ATL、MFC 和 #import 使一些事情更容易,但仍然太容易出错地进行资源管理。Comet 为所有 OLE 自动化数据类型、结构体等提供资源管理,它不仅使资源管理易于正确进行,而且使其难以错误地进行。它还比 #import 更进一步,允许以与调用相同的方式实现服务器,从而使异常的默认处理变得微不足道。

总而言之,Comet 使 COM 编程变得容易且易于维护。绝对值得一试。

当前的 Comet 文档和源代码可以在 Comet 网站上找到。

© . All rights reserved.