<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Dimos Maginas</title>
    <description>The latest articles on DEV Community by Dimos Maginas (@dmaginas).</description>
    <link>https://dev.to/dmaginas</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3982227%2F67a1dbd3-922a-4082-aadb-8ee357ac31f8.jpg</url>
      <title>DEV Community: Dimos Maginas</title>
      <link>https://dev.to/dmaginas</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dmaginas"/>
    <language>en</language>
    <item>
      <title>In-process vs out-of-process plugins: the design fork that shaped my Windows app</title>
      <dc:creator>Dimos Maginas</dc:creator>
      <pubDate>Sat, 13 Jun 2026 06:02:35 +0000</pubDate>
      <link>https://dev.to/dmaginas/in-process-vs-out-of-process-plugins-the-design-fork-that-shaped-my-windows-app-40kh</link>
      <guid>https://dev.to/dmaginas/in-process-vs-out-of-process-plugins-the-design-fork-that-shaped-my-windows-app-40kh</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The setup&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Model 1 — In-process plugins (C++ shared libs)&lt;/p&gt;

&lt;p&gt;These are C++ shared libraries the core loads directly into its own address space, behind a plain extern "C" boundary:&lt;/p&gt;

&lt;p&gt;// Stable C ABI — no name mangling, no STL across the boundary&lt;br&gt;
extern "C" {&lt;br&gt;
    __declspec(dllexport) int chauffeur_plugin_init(const ChauffeurHostApi* host);&lt;br&gt;
    __declspec(dllexport) void chauffeur_plugin_shutdown();&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;Pros: fast, share memory with the core, zero IPC overhead. Perfect for trusted, first-party, performance-sensitive work.&lt;/p&gt;

&lt;p&gt;Cons, and they're real:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A misbehaving plugin can take the whole core down — a segfault or a hang isn't catchable with try/catch.&lt;/li&gt;
&lt;li&gt;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.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Model 2 — Out-of-process add-ins (separate processes)&lt;/p&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;p&gt;POST /register      { "id": "excel", "type": "push", ... }&lt;br&gt;
POST /plugin/alive/excel    // heartbeat&lt;/p&gt;

&lt;p&gt;If one crashes, hangs, or gets killed, the core just sees a dead socket and moves on. The blast radius is one process.&lt;/p&gt;

&lt;p&gt;Pros: crash isolation for free, no shared address space, no ABI headaches — and the language becomes irrelevant. C#, JS, Python, anything that speaks HTTP.&lt;/p&gt;

&lt;p&gt;Cons: IPC latency, serialization, and you're now running N processes.&lt;/p&gt;

&lt;p&gt;The lesson&lt;/p&gt;

&lt;p&gt;The thing that surprised me: the in-process/out-of-process choice is the language choice in disguise.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Need low-latency, in-memory calls and you trust the code? In-process, accept the ABI tax.&lt;/li&gt;
&lt;li&gt;Everything else — third-party, scripted, crash-prone, multi-language? Out-of-process, let the process boundary do the safety work.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;On exception safety&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  Chauffeur is a paid Windows app with a free 30-day trial. Local-only workspace save &amp;amp; restore; your context never leaves your PC. Check it out (&lt;a href="https://www.emaginasion.com" rel="noopener noreferrer"&gt;https://www.emaginasion.com&lt;/a&gt;). Happy to answer architecture questions in the comments.
&lt;/h2&gt;

</description>
      <category>architecture</category>
      <category>cpp</category>
      <category>showdev</category>
      <category>softwareengineering</category>
    </item>
  </channel>
</rss>
