August 5th, 2026
like2 reactions

Creating a fake agile wrapper that is technically agile but is not useful outside its home apartment, part 3

Last time, we tried to execute on our plan to use the global interface table to hold hold a reference to an object in another apartment that automatically expires when the apartment runs down. But it broke down because objects that are marked INoMarshal can’t go into the global interface table.

So we will just force the square peg into the round hole: We can put the non-marshalable object inside an object that is marshalable.

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;
};

The force_marshal<Smart> object babysits a non-marshalable smart pointer to a COM object and exposes a marshalable wrapper around it. Since the wrapped object is not agile, the wrapper cannot be either. (If the wrapper were agile, then we’d be back where we started: How do we ensure that the m_p is destructed in the correct apartment?)¹

We can put the unmarshalable object inside the wrapper, and then put the wrapper in 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>;

Okay, so now we have managed to create an agile wrapper around an unmarshalable object. This agile wrapper is agile on paper: You can use it from any thread. However, it is not agile in practice: If you try to use it from the wrong apartment, it throws an exception. But at least the behavior when used from the wrong apartment is well-defined, as opposed to the case of directly using an unmarshalable object from the wrong apartment, which is undefined.

Next time, we’ll do some fine tuning.

Bonus chatter: Of course, now that we have a marshalable wrapper, we could use that wrapper to call the original non-marshalable object.

Original apartment     Other apartment
Wrapper Caller
   
Non-marshalable    

The non-marshalable object does not allow any arrows to come in from other apartments, but the wrapper lives in the same apartment as the non-marshalable object, so its arrow is coming from within the same apartment.

You can think of the wrapper as a VPN into the original apartment, allowing calls to come in from the outside, but to appear to the non-marshalable object as if they came from within the same apartment.

Now, you could do that, but I’m not going to. The original object presumably went out of its way to declare itself non-marshalable for a reason, so we honor that preference and not play funny games to trick it into doing something it said that it didn’t want to do.

¹ Two commenters fell into this trap by suggesting that we wrap the non-marshalable object inside an object that implements IMarshal. If you implement IMarshal, then you are saying, “I’m way cooler than a standard non-agile object. I’m going to do fancy stuff (like being agile).” But we want to be a boring non-agile object, so that COM will do standard marshaling for us.

Topics

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

4 comments

Sort by :
  • GL

    Nit-picking nit-picking: Footnote 1 seems to suggest I suggested implementing IMarshal, which is not true. But I do regret the phrasing “registering H in X’s apartment”, which should be “creating H in X’s apartment and registering it to the GIT”. Luca Bacci suggested implementing IMarshal, but didn’t specify how. Whether the commenter intended to aggregate the FTM (which would make the object effectively agile) is unclear.

    • Luca Bacci 1 day ago

      > […] suggested implementing IMarshal, but didn’t specify how.

      Oh yeah, scratch that. I now realize we’re passing __uuidof(IUnknown) to RegisterInterfaceInGlobal. From the point of view of the GIT, the interface pointer can be marshaled

      • Luca Bacci · Edited

        > It’s an object that is marshaled/unmarshaled, not an interface pointer, and you can only unmarshal *into* an interface pointer.

        What I mean is that marshaling is done per-interface. If an object O implements interfaces A and B, COM searches a proxy / stub for A and a proxy / stub for B. There's no proxy / stub for O. Actually, you may have a proxy /stub for A but not B :)

        > Upon GetInterfaceFromGlobal, if it’s in the same apartment, the GIT simply does a QI. (For an unregistered interface, it can be retrieved in, and only in, the home...

        Read more
      • GL 1 day ago

        I'm confused by your terminology. It's an object that is marshaled/unmarshaled, not an interface pointer, and you can only unmarshal *into* an interface pointer.

        IGit's Register/Get use riid-ppv pair, so it's very likely that you can register an object with any interface (and which, is irrelevant most of the time), then retrieve it for any other interface. To verify this theory, I just tried, and you can register any interface pointer with any IID (valid for that object), then retrieve any other interface. Upon GetInterfaceFromGlobal, if it's in the same apartment, the GIT simply does a QI. (For an unregistered interface,...

        Read more