跳转至

构建仅在宿主套间内起效的伪敏捷包装器(第三部分)

文章背景与核心概要

本系列文章探讨了 Windows COM 编程中套间(Apartment)与敏捷对象(Agile Object)的技术细节。在第三部分中,作者通过引入可封送包装器 force_marshal 解决了全局接口表(GIT)无法直接存储带有 INoMarshal 标记对象的问题。该机制在成功绕过封送限制的同时严格保留了套间边界限制:当跨套间访问该对象时会明确抛出异常,从而实现了行为明确且符合标准的“伪敏捷(Fake Agile)”引用。


概要

Summary

在系列文章的第三部分中,我们通过引入一个可封送包装器(force_marshal),克服了全局接口表(GIT)无法存储 INoMarshal 对象的限制。该包装器成功绕过了封送限制,同时保留了严格的套间边界,确保在跨套间访问时展示出明确(尽管受限)的行为。

In this third installment of the series, we overcome a limitation preventing the Global Interface Table from storing INoMarshal objects by introducing a marshalable wrapper (force_marshal). This wrapper successfully bypasses marshal restrictions while preserving strict apartment boundaries, ensuring well-defined (though restrictive) behavior when accessed cross-apartment.


INoMarshal 带来的问题

The Problem with INoMarshal

此前,我们尝试使用全局接口表(GIT)来持有另一个套间中对象的引用,该引用在那个套间注销时会自动失效。然而,那种方法失败了,因为带有 INoMarshal 标记的对象不能被放入全局接口表中。

Previously, we attempted to use the global interface table (GIT) to hold a reference to an object in another apartment, which would automatically expire when that apartment ran down. However, that approach failed because objects marked with INoMarshal cannot be placed into the global interface table.

为了解决这个问题,我们可以把“方钉子强行塞进圆孔”:我们将不可封送的对象放置在一个可封送的对象内部。

To work around this, we can force the square peg into the round hole: we will place the non-marshalable object inside an object that is marshalable.


强制封送(Force the Marshal)

Forcing the Marshal

我们定义一个 force_marshal 模板来照顾这个不可封送的智能指针,并暴露一个标准的可封送包装器:

We define a force_marshal template to babysit the non-marshalable smart pointer and expose a standard marshalable wrapper:

template<typename Smart>
struct force_marshal :
    winrt::implements<force_marshal<Smart>, ::IUnknown, winrt::non_agile>
{
    force_marshal(Smart const& p) : m_p(p) {}
    Smart m_p;
};

因为被包装的对象不是敏捷的,所以包装器本身也不能是敏捷的。(如果包装器是敏捷的,我们将回到最初的困境:如何确保 m_p 在正确的套间中被析构?)

Because the wrapped object is not agile, the wrapper cannot be either. (If the wrapper were agile, we would be back to our original dilemma: how do we ensure m_p is destructed in the correct apartment?)

实现伪敏捷引用

Implementing the Fake Agile Reference

准备好包装器后,我们可以将不可封送的对象放入其中,然后将该包装器放入全局接口表中:

With our wrapper ready, we can place the unmarshalable object inside it, and then put that wrapper into the global interface table:

template<typename T>
struct fake_agile_ref
{
     ... 

    fake_agile_ref(Smart const& p) : m_raw(winrt::get_abi(p))
    {
        if (m_raw) {
            m_context = winrt::capture<IContextCallback>(CoGetObjectContext);
            m_token = get_context_token();
            m_git = winrt::create_instance<IGlobalInterfaceTable>(CLSID_StdGlobalInterfaceTable);
            winrt::check_hresult(m_git->RegisterInterfaceInGlobal(
                winrt::make<force_marshal<Smart>>(p).get(),
                __uuidof(IUnknown), &m_cookie));
        }
    }

     ... 
};

template<typename T> fake_agile_ref(winrt::com_ptr<T> const&)
    -> fake_agile_ref<T>;
template<typename T> fake_agile_ref(T const&)
    -> fake_agile_ref<T>;

行为与局限性

Behavior and Limitations

我们现在成功为不可封送的对象创建了一个敏捷包装器。

We have now successfully created an agile wrapper around an unmarshalable object.

  • 名义上(On Paper): 它是敏捷的。你可以从任何线程持有并传递它。
  • 实际上(In Practice): 它并不是真正的敏捷。如果你尝试从错误的套间调用它,它会抛出异常。
  • On Paper: It is agile. You can hold and pass it from any thread.
  • In Practice: It is not truly agile. If you attempt to invoke it from the wrong apartment, it throws an exception.

虽然受到限制,但这种行为是严格定义明确(well-defined)的,避免了跨套间直接调用不可封送对象所导致的未定义行为。

While restrictive, this behavior is strictly well-defined, avoiding the undefined behavior of directly invoking an unmarshalable object across apartments.

在下一部分中,我们将进行一些微调。

In the next part, we will perform some fine-tuning.


补充闲聊:VPN 比喻

Bonus Chatter: The VPN Analogy

既然我们拥有了一个可封送的包装器,理论上我们可以用它来直接穿透调用到原始的不可封送对象中:

Now that we possess a marshalable wrapper, we could theoretically use it to tunnel calls directly into the original non-marshalable object:

原始套间 其它套间
包装器 调用者
不可封送对象
Original apartment Other apartment
Wrapper Caller
Non-marshalable

因为包装器与不可封送对象存在于同一个套间中,所以它的内部指向箭头起源于本地套间内部。你可以将包装器看作是通往原始套间的 VPN:它允许外部调用进入,但让 underlying 对象看起来这些调用就像是从本地发起的一样。

Because the wrapper lives in the same apartment as the non-marshalable object, its incoming arrow originates from within the local apartment. You can view the wrapper as a VPN into the original apartment: it permits external calls to enter, but makes them look to the underlying object as if they originated locally.

但是,我们并不打算这样做。 原始对象显式将其自身声明为不可封送是有其明确考量的。我们选择尊重这一偏好,而不是玩弄小聪明将其强行塞入它所拒绝的状态中。

However, we are not going to do that. The original object explicitly declared itself non-marshalable for a deliberate reason. We choose to honor that preference rather than playing clever tricks to force it into a state it rejected.


脚注 1: 有两位评论者陷入了陷阱,建议我们将不可封送的对象包装在一个实现了 IMarshal 的对象内部。实现 IMarshal 实际上是在声明:“我比标准的非敏捷对象复杂得多,我会自己处理如敏捷性等高级行为。” 但我们的目标是保持为一个平淡无奇的标准非敏捷对象,以便 COM 代表我们处理正常的封送过程。

Footnote 1: Two commenters fell into the trap of suggesting we wrap the non-marshalable object inside an object implementing IMarshal. Implementing IMarshal is essentially declaring, "I am much more sophisticated than a standard non-agile object and will handle fancy behavior like agility myself." But our goal is to remain a boring, standard non-agile object so that COM handles normal marshaling on our behalf.

参考来源:The Old New Thing

Reference: The Old New Thing