为什么在 IUserNotificationCallback COM object 上查询 IMarshall 接口?

Why is IMarshall Interface queried on IUserNotificationCallback COM object?

使用 Microsoft 的 IUserNotification2

我正在使用 IUserNotification2 向软件用户显示通知。

我使用 Microsoft 的现有实现,请参见此处。 (注意,我删除了标准 headers 并简化了一点)。

#include <Shobjidl.h> //IUserNotification2 interface header

void NotifyUser(const std::wstring &title,
    const std::wstring &text){
if (!SUCCEEDED(CoInitializeEx(nullptr, COINIT_MULTITHREADED)))
    throw std::exception("could not init COM");

IUserNotification2 * handleNotification = nullptr;
auto result = CoCreateInstance(CLSID_UserNotification, 0, CLSCTX_ALL, IID_IUserNotification2, (void**)&handleNotification);
if (!SUCCEEDED(result) || !handleNotification) {
    throw std::exception("could not create CLSID_UserNotification");
}
DWORD notif_flags = NIIF_RESPECT_QUIET_TIME|NIIF_WARNING;
result = handleNotification->SetBalloonInfo(title.c_str(), text.c_str(), notif_flags);
if (!SUCCEEDED(result))
    throw std::exception("could not SetBalloonInfo of notification");
if (!SUCCEEDED(handleNotification->Show(nullptr, 5000,nullptr))
        throw std::exception("failed Show of notification");

在没有回调的情况下执行此操作(即显示一条消息,但对点击事件没有任何操作)我没有问题,我的代码有效。

添加点击事件回调

为了添加这样的回调,需要传递IUserNotification2Object的IUserNotificationCallback Object to the Show方法。

看到这里我只更改了对 Show 的调用。

Callback cbk;
if (!SUCCEEDED(handleNotification->Show(nullptr, 5000,& cbk)))
        throw std::exception("failed Show of notification");

Callback class 实现 IUserNotificationCallback。请参阅下面的 class 的实现。

class Callback : public IUserNotificationCallback {
public:
    virtual HRESULT STDMETHODCALLTYPE OnBalloonUserClick(POINT * pt) override {return S_OK;}
    virtual HRESULT STDMETHODCALLTYPE OnContextMenu(POINT * pt) override {}
    virtual HRESULT STDMETHODCALLTYPE OnLeftClick(POINT * pt) override {return S_OK;}

    /// Implementing IUnknown Interface
    virtual ULONG STDMETHODCALLTYPE AddRef()override { return 1; }
    virtual ULONG STDMETHODCALLTYPE Release() override { return 0; }
    virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID inRiid, void ** outAddressOfObjectPointer)override {
        if (outAddressOfObjectPointer) {
            if (inRiid == IID_IUnknown || inRiid == IID_IUserNotificationCallback)
            {
                *outAddressOfObjectPointer = this;
                AddRef();
                return NOERROR;
            }
        }
        *outAddressOfObjectPointer = nullptr;
        return E_NOINTERFACE;
    }
};

现在的问题,

是不是当我调用 Show 方法时,我的 IUserNotificationCallback objects 在 IUnknown 接口的 QueryInterface 上被调用。 classic COM 风格。但是我收到了对 IID_IMarshall 的查询,我通过添加

进行了检查
else if (inRiid == IID_IMarshal) {
    printf("interface queried is IMarshall\n");

在查询接口方法中。

但是为什么我应该收到这个,文档中没有任何地方提到我应该回答这个 IID。 在这种情况下,我让 E_NOINTERFACE 在 void** 参数中返回一个 nullptr。

我这导致显示方法失败。

注意

我看了notifu的代码,看看有没有帮助,但是和我的基本一样

解决方案

显然 Roman R. 提供了一个可行的解决方案。 将对 CoInitializeEx 的调用更改为 if (!SUCCEEDED(CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED))) 使其有效!

再次感谢您

nowhere in the documentation it is mentionned that I should answer to this IID

可能会向任何 COM 对象查询对象未知的接口。这是正常的,更重要的是,COM 对象必须正确响应此,然后任何 COM 对象都必须实现 IUnknown 和此方法。

所以你基本上应该设置 *ppvObject 为 NULL 和 return E_NOINTERFACE 如果你不实现接口,否则 return S_OK 并使用有效指针初始化 *ppvObject,一旦调用者不再需要该指针,就为 IUnknown::Release 调用做好准备。

你的解决方案

Apparently the solution (suggested by a friend) is too always give back the this pointer even when returning E_NOINTERFACE.

因此是不正确的。如果调用者提供了一个由智能指针管理的变量,然后无论如何都试图释放它,那么有时也可能非常危险。这对于标准编组 OS 代码来说不太可能,后者更愿意立即丢弃该值。

如果您这样做,您的实施将是正确的

*outAddressOfObjectPointer = NULL;
if (inRiid == IID_IUnknown)
{
        printf("interface required is IUnknown\n");
        *outAddressOfObjectPointer = this;
        AddRef();
        return NOERROR;
}
// ...

NULLnullptr 不会导致此处失败。但是,您的代码在其他方面很危险:COM 初始化,Release 调用中的 return 为零,以及本地堆栈支持的回调 class 实例被破坏超出范围但可能仍然存在稍后通过暴露的接口指针调用。

现在回到最初的问题,封送处理到底在做什么?您正在初始化 MTA 线程并在那里创建单元线程 COM 对象 CLSID_UserNotification。 COM 在侧 STA 线程中为您创建通知 API,然后它尝试将您的回调传递到那里以匹配线程。它必须询问您的 COM 对象是它自己进行封送处理,还是需要提供这个东西。您的对象必须说它不会自行编组并且对此一无所知。这就是正在发生的事情。做到COINIT_APARTMENTTHREADED,你会看到不一样的画面。

进行涉及接口指针的单元间调用时,COM first queries the object for the IMarshal interface:

COM uses your implementation of IMarshal in the following manner: When it's necessary to create a remote interface pointer to your object (that is, when a pointer to your object is passed as an argument in a remote function call), COM queries your object for the IMarshal interface. If your object implements it, COM uses your IMarshal implementation to create the proxy object. If your object does not implement IMarshal, COM uses its default implementation.

因此,这确实被记录在案。

至于QueryInterface,就像你告知你的对象实现了IUnknown等有用的接口一样,当你没有实现某个接口时也必须告知。这是通过使传出接口指针为空(以避免其编组)并返回 E_NOINTERFACE.

来完成的
  • 注意:QueryInterface 的实现充满了微妙之处。例如,在 C++ 中,当你实现一个接口时,你应该将 this 转换为适当的接口指针类型,如果你打算使用聚合或分离,你应该 AddRef 实际的接口指针。

如果您不实现自定义封送处理,我建议您在请求 IMarshal 时不要做任何特别的事情,只需将其作为未实现的接口处理即可。

COM 然后将在当前激活上下文中查找标准封送拆收器(请参阅 comClass in assembly manifests, and clrClass), then in the registry (see the Interface registry key). Type library marshaling is a specific kind of standard marshaling where the ProxyStubClsid32 refers to one of the type library marshalers ({00020420-0000-0000-C000-000000000046} aka PSDispatch for dispinterfaces, {00020424-0000-0000-C000-000000000046} aka PSOAInterface for [oleautomation] interfaces),它期望引用包含接口定义的类型库的 TypeLib 子项。