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.
I did something like this to run a specific 64-bit COM component from a 32-bit process — a large program that I couldn’t convert to 64 bits because it depended on obsolete 32-bit ActiveX controls that were no longer available. I obviously couldn’t load it in-process, so I used the surrogate. In the end, it worked perfectly.
The main issue I ran into was the surrogate always starts in SYSTEM32 and does not inherit the working directory or environment from the invoking process. I had to add a custom interface to my surrogate host object to change the working directory and...
The unloading problem can be solved by not using a surrogate, but a local server. The local server can implement IClassFactory and whatever surrogate set-up interface (passing working directories, environment variables, etc.) and object creation interface in a singleton. It then registers the singleton as a single-use class factory for, say, CLSID_My32bitFriend. The 64-bit client can access a new local server by CoGetClassObject or CoCreateInstance (they're effectively the same thing) --- those two APIs will never let you access the same local server process twice. Calling methods (QI-ing, initializing, creating objects) will be routed to the (same) server process (from...
You probably want to do this for testing your own code anyway, since the COM object is something you bought.
Build another dummy object using the same interface and you can then return error codes at will.
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...