browser icon
You are using an insecure version of your web browser. Please update your browser!
Using an outdated browser makes your computer unsafe. For a safer, faster, more enjoyable user experience, please update your browser today or try a newer browser.

谈 QueryInterface 的简化

Posted by on 2014 年 01 月 25 日

你可以任意转载本文,但请在转载后的文章中注明作者和原始链接。
媒体约稿请联系 titilima_AT_163.com(把“_AT_”换成“@”)。

相信所有用过 COM 的 Win32 C++ 程序员都曾遇到过并且曾亲自写下过类似这样的臃肿代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
void STDMETHODCALLTYPE OnDocumentComplete(IDispatch *pDisp, VARIANT *URL)
{
    IWebBrowser2 *pWB2 = NULL;
    pDisp->QueryInterface(IID_IWebBrowser2, reinterpret_cast<void **>(&pWB2));
 
    IDispatch *pDispDoc = NULL;
    pWB2->get_Document(&pDispDoc);
 
    IHTMLDocument2 *pDoc = NULL;
    pDispDoc->QueryInterface(IID_IHTMLDocument2, reinterpret_cast<void **>(&pDoc));
 
    // ...
}

这段代码的目的很简单,就是获取浏览器(Web Browser)的文档(Document)接口。但是,对于 C++ 这种边界不那么广阔的语言来说,程序员必须老老实实来实现每个细节。而对于 JS 或者 VB 这种弱类型且有解释器支持的语言来说,解释器会在底层借由 COM 的自动化(Automation)机制来包揽以上所有的细节,程序员只需要一行代码或简单的一个语言元素就可以完成以上这一大坨代码所描述的所有事情。

于是引出我今天的话题:如何简化使用 COM 时大篇幅的 QueryInterface?——当然,由于 C++ 本身的语言边界所限,我所谓的“简化”并非“省略”,而是将调用者不愿意写的 QueryInterface 代码封装到框架或函数之中。

我想到的第一个方法,是将模板类 CComPtr 为 IWebBrowser2 做一个特化的实现,就像 CComPtr 为 IDispatch 所做的那样——ATL 在 <atlcomcli.h> 之中实现了一个 IDispatch 特化的 CComPtr,其中为自动化接口的属性(Property)和调用(Invoke)增加了一系列辅助函数。但是,实际实现的时候却出现了问题,因为 ATL 在 <atlhost.h> 之中已经抢先实例化了 CComPtr<IWebBrowser2> 的对象,这使得我再另写 CComPtr 的 IWebBrowser2 特化代码时将会遇到 C2766C2908 的编译错误。

那么只好曲线救国,将辅助函数单独提出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class WebBrowserHelper
{
public:
    template <class T>
    static HRESULT GetDocument(IWebBrowser2 *pWB2, T **pp)
    {
        CComPtr<IDispatch> pDispDoc;
        HRESULT hr = pWB2->get_Document(&pDispDoc);
        if (SUCCEEDED(hr)) {
            hr = pDispDoc->QueryInterface(__uuidof(T), reinterpret_cast<void **>(pp));
        }
        return hr;
    }
};

然后调用代码就可以简化不少:

1
2
3
CComQIPtr<IWebBrowser2> wb2(pDisp);
CComPtr<IHTMLDocument2> pDoc;
WebBrowserHelper::GetDocument(wb2, &pDoc);

这就完了吗?当然没有。试想,我如果接下来要拿到 <body> 元素的 IHTMLBodyElement 接口指针,需要怎么做?——要知道 IHTMLDocument2 的 get_body 是这样的:

HRESULT IHTMLDocument2::get_body(IHTMLElement **p);

我是要继续 QueryInterface 出一个 IHTMLBodyElement 呢,还是要扩充我的 WebBrowserHelper?如果是扩充的话,那么 IHTMLDocument3 的 getElementById 是否也需要继续扩充?这就意味着这种薄层的函数封装有可能会让我陷入一个封装的泥沼,因此,我需要一个更优雅的解决方案。

于是,我重新梳理了一下我需要封装的流程:

  1. 接口 A 调用某个方法,得到接口 B 的指针;
  2. 接口 B 调用 QueryInterface 得到接口 C 的指针;
  3. 使用接口 C。

这样一来,事情就明晰起来了——接口 B 是一个用于中转的适配接口,所以我只要把它隐藏起来,这就足够了。那么先列出我的辅助类吧,我名之为 AdapterPtr:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
template <class T1, class T2>
class AdapterPtr : public ATL::CComPtrBase<T1>
{
public:
    AdapterPtr(void) : ATL::CComPtrBase<T1>(), p2(NULL) { /* Nothing */ }
    ~AdapterPtr(void)
    {
        if (NULL != p2) {
            p2->Release();
            p2 = NULL;
        }
    }
    operator T1*() throw()
    {
        return Get();
    }
    T1& operator*()
    {
        return *Get();
    }
    T2** operator&(void) throw()
    {
        ATLASSERT(NULL == p2 && NULL == p);
        return &p2;
    }
    _NoAddRefReleaseOnCComPtr<T1>* operator->() throw()
    {
        return (_NoAddRefReleaseOnCComPtr<T1>*)Get();
    }
    bool operator!() throw()
    {
        return (NULL == Get());
    }
    bool operator!=(_In_opt_ T1* pT)
    {
        return !operator==(pT);
    }
    bool operator==(_In_opt_ T1* pT) throw()
    {
        return Get() == pT;
    }
public:
    T1* Detach(void) throw()
    {
        T1 *pt = Get();
        p = NULL;
        return pt;
    }
protected:
    T1* Get(void)
    {
        if (NULL == p) {
            if (NULL == p2) {
                return NULL;
            }
            p2->QueryInterface(__uuidof(T1), reinterpret_cast<void **>(&p));
            p2->Release();
            p2 = NULL;
        }
        return p;
    }
protected:
    T2 *p2;
};

简单说明一下:

  1. AdapterPtr 借助了 CComPtrBase 的结构,扩充出一个 p2 的成员,用于适配中转接口。
  2. operator& 返回中转接口的二级指针,用于接收 A 接口调用的结果。
  3. Get() 是一个 惰性初始化 的实现,用于在需要使用 C 接口时查询出 C 接口的指针。
  4. 由于原有的 p 成员是被惰性初始化的,所以基类 CComPtrBase 原有的部分方法和 operator 需要覆盖(override),改写成使用 Get 的版本。

最后秀一下使用了 AdapterPtr 的结果:

1
2
3
4
5
6
7
CComQIPtr<IWebBrowser2> pWB2(pDisp);
AdapterPtr<IHTMLDocument2, IDispatch> pDoc;
pWB2->get_Document(&pDoc);
 
AdapterPtr<IHTMLBodyElement, IHTMLElement> pBody;
pDoc->get_body(&pBody);
// Use pBody ...

AdapterPtr 的代码已提交至 GitHub

订阅本站

没有评论

(Required)
(Required, will not be published)