I'm building Chauffeur, a local Windows tool that snapshots your whole working state across apps — open files, editor tabs, browser tabs, projects — and restores it on demand. "Save game" for your desktop. 100% local: no cloud, no account, no telemetry.
But this post isn't a pitch. It's about the one architecture decision that shaped everything: how do other apps plug into the core? I ended up running two completely different plugin models on purpose, and the reasoning is worth sharing.
The setup
A small background service (C++/Qt 6) exposes a local REST API. Everything integrates through that single point. The question was never really "what language" — it was in-process or out-of-process. Those are the two real choices, and the language question mostly answers itself afterward.
Model 1 — In-process plugins (C++ shared libs)
These are C++ shared libraries the core loads directly into its own address space, behind a plain extern "C" boundary:
// Stable C ABI — no name mangling, no STL across the boundary
extern "C" {
__declspec(dllexport) int chauffeur_plugin_init(const ChauffeurHostApi* host);
__declspec(dllexport) void chauffeur_plugin_shutdown();
}
Pros: fast, share memory with the core, zero IPC overhead. Perfect for trusted, first-party, performance-sensitive work.
Cons, and they're real:
- A misbehaving plugin can take the whole core down — a segfault or a hang isn't catchable with try/catch.
- The ABI must stay stable forever. Every signature change is a new extern "C" entry point you carry around. No passing std::string across the boundary; C types only.
Model 2 — Out-of-process add-ins (separate processes)
The Excel/Word add-ins (C# VSTO), the browser extensions (JS), the VS Code extension — none of them share memory with the core. They register over the local REST API and send a heartbeat:
POST /register { "id": "excel", "type": "push", ... }
POST /plugin/alive/excel // heartbeat
If one crashes, hangs, or gets killed, the core just sees a dead socket and moves on. The blast radius is one process.
Pros: crash isolation for free, no shared address space, no ABI headaches — and the language becomes irrelevant. C#, JS, Python, anything that speaks HTTP.
Cons: IPC latency, serialization, and you're now running N processes.
The lesson
The thing that surprised me: the in-process/out-of-process choice is the language choice in disguise.
- Need low-latency, in-memory calls and you trust the code? In-process, accept the ABI tax.
- Everything else — third-party, scripted, crash-prone, multi-language? Out-of-process, let the process boundary do the safety work.
Picking per integration instead of forcing one model everywhere is the decision I'd make again without hesitation. The REST boundary doubles as the version boundary too — you negotiate a protocol version on /register instead of versioning a memory layout.
On exception safety
If you go in-process, try/catch around every interface call helps with well-behaved exceptions — but it does nothing against segfaults, stack smashes, or infinite loops. Those take the host with them. If you need real isolation, a process boundary is the only thing that actually gives it to you. Versioned interfaces (IPluginV1/IPluginV2) solve ABI compatibility, not crash safety. Don't conflate the two.
Top comments (0)