A customer had an app with a plugin model based on vendor-supplied COM objects that are loaded into the process via CoÂCreateÂInstance
.
Main process |
Engine ↓ Plugin |
These COM objects run in-process, but the customer realized that these plugins were a potential source of instability, and they saw that you can instruct COM to load the plugin into a sacrificial process so that if the plugin crashes, the main program is unaffected.
What they want is something like this:
Main process | Surrogate process | |
Engine | → | Plugin |
But how do you opt a third-party component into the COM Surrogate? The third party component comes with its own registration, and it would be rude to alter that registration so that it runs in a COM Surrogate. Besides, a COM Surrogate requires an AppId, and the plugin might not have one.
The answer is simple: Create your own object that is registered to run in the COM Surrogate. Define an interface for that custom object, say, ISurrogateHost
and give that interface a method like LoadPlugin(REFCLSID pluginClsid, REFIID iid, void** result)
which calls CoÂCreateÂInstance
on the plugin CLSID and requests the specified interface pointer from it. (If you want to support aggregation, you can add a punkOuter
parameter.)
Main process | Surrogate process | |
Engine   |
→   |
Host ↓ Plugin |
The LoadÂPlugin
method runs inside the surrogate, so when the plugin loads in-process, it loads into the surrogate process.
The host can return a reference to the plugin directly to the main app engine, so it steps out of the way once the two sides are connected. The purpose of the host is to set up a new process.
In fact, you don’t even need to invent that special surrogate interface. There is already a standard COM interface that does this already: ICreateObject
. It has a single method, uncreatively named CreateObject
that takes exactly the parameters we want, including the punkOuter
.
Your surrogate host object would go like this, using (rolls dice) the WRL template library.
struct SurrogateHost : Microsoft::WRL::RuntimeClass< Microsoft::WRL::RuntimeClassFlags< Microsoft::WRL::RuntimeClassType::ClassicCom | Microsoft::WRL::RuntimeClassType::InhibitWeakReference>, ICreateObject, Microsoft::WRL::FtmBase> { STDMETHOD(CreateObject)( REFCLSID clsid, IUnknown* outer, REFIID iid, void** result) { return CoCreateInstance(clsid, outer, CLSCTX_INPROC_SERVER, riid, result); } };
In the engine, where you would normally do
hr = CoCreateInstance(pluginClsid, outer, CLSCTX_INPROC_SERVER, riid, result);
you instead create a surrogate host:
Microsoft::WRL::ComPtr<ICreateObject> host; hr = CoCreateInstance(CLSID_SurrogateHost, nullptr, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&host));
and then each time you need an object, you ask the surrogate host to do it:
hr = host->CreateObject(pluginClsid, outer, riid, result);
You can even get fancy and decide that some plugins are sus and should run in a surrogate, whereas others are trustworthy and may run inside the main process.
if (is_trustworthy(pluginClsid)) { // Let this one load into the main process hr = CoCreateInstance(pluginClsid, outer, CLSCTX_INPROC_SERVER, riid, result); } else { // Boot this one to the surrogate process hr = host->CreateObject(pluginClsid, outer, riid, result); }
Reusing the host
object means that a single surrogate process is used for all plugins. If you want each plugin running in a separate surrogate, then create a separate host for each one.
Speaking of aggregation... The documentation of CoCreateInstanceEx says that pUnkOuter must be NULL if the object is created out-of-process.
Does aggregation actually work across apartments (but not across processes)? How does reference count caching (i.e., the proxy in each apartment maintains the number of references inside the current apartment --- that's my understanding of how things work) interact with weak QueryInterface (2022-02-10 and 11)?
Now, technically, the pUnkOuter sent to CoCreateObjectEx for out-of-process activation could be a proxy to an object residing in the activated process (e.g., multi-use local server) so the call should in principle not fail for CLASS_E_NOAGGREGATION, but it's...