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.
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.