I'm building yyzTools, which bundles 9 third-party engines (OpenSSL, FFmpeg, ImageMagick, pdfcpu, Aria2, 7-Zip, RapidOCR, Everything...).
I chose to spawn them as subprocesses rather than static-link their SDKs.
Here's why—and the cost.
The conventional approach When your app needs OpenSSL crypto, FFmpeg video processing, ImageMagick image ops—you reach for the SDK.
Link libssl, link libav*, link libMagick.
One binary, no external deps, fast function calls.
It's the textbook answer.
I did the opposite. yyzTools ships the stock CLI binaries (openssl.exe, ffmpeg.exe, magick.exe, pdfcpu, aria2c, 7z) and spawns them as subprocesses.
The C++ layer is a thin loop: build args → CreateProcess → read stdout → wrap as JSON → return.
It doesn't know what -gravity southeast or sm4-cbc means.
It just passes the algorithm name through.
Why I went this way Upgrades without recompiling This is the big one for a desktop app.
OpenSSL ships a CVE, or adds sm2/sm3/sm4 support in 3.x.
If you've static-linked, you recompile the whole app, run full regression, re-release, and every user reinstalls.
With the subprocess model, I drop in a new openssl.exe.
Zero C++ changes.
The update is a few-MB delta, not a full reinstall.
For a product where users won't tolerate reinstalling for a library bump, this is the deciding factor.
No symbol conflicts OpenSSL, zlib, libpng—multiple libraries want to own these symbols.
Static linking them all into one binary is a recipe for "which inflate did I just call?" With subprocess CLIs, each tool brings its own dependencies in its own process.
No conflict.
Transparent supply chain openssl version, ffmpeg -version—auditing which version of each tool is live is trivial.
It's an independent binary.
Far easier than digging symbols out of a statically-linked blob.
Free crash isolation If ffmpeg.exe misbehaves, it exits non-zero and my host wraps that as an error.
My main process keeps running.
A static-linked bug can take down the whole app.
The process boundary is a free fault domain.
Algorithm support is "front-end list + docs", not C++ rebuild Because the C++ side just passes algorithm names through, adding a new hash algorithm means adding a line to the front-end's ALGORITHMS list and updating docs.
NativeApi doesn't change.
I covered sm2/sm3/sm4 in a day this way.
The cost This isn't free.
Let's be honest: Per-call fork overhead Spawning a process is ~tens of ms.
For "compress this folder" or "hash this file," fine.
For "verify 10,000 HMACs in a loop," brutal.
You'd never build a high-throughput crypto service this way.
I knew this going in—yyzTools hashes one file at a time, not a stream of requests.
Low frequency is the prerequisite for this pattern.
Large-file hashing is slower A static SHA256() call streams bytes in-process.
The subprocess model means bytes cross the process boundary.
For a 4GB ISO, you feel it.
Acceptable for my use case; unacceptable for a backup service.
The stdout contract is the weak point CLIs emit human-readable output, not machine-readable JSON.
I hand-rolled a stable parser for each CLI's output.
When a CLI upgrades and changes its output format, the parser can break.
Easier to debug than a static ABI change (the CLI just fails loudly), but it's a maintenance surface.
I wish there were a standard "CLI emits JSON" convention.
Some tools have it (gh, kubectl), many don't (openssl enc's output is a mess to parse).
The inconsistency is the real friction.
Process lifecycle management You own timeouts, zombie handling, cancellation.
A static function call doesn't hang.
A subprocess can.
I had to build a work-queue with concurrency limits (don't spawn 200 magick.exe for 200 images) and cancellation.
That's extra code you don't write for a static link.
Where I'd draw the line This pattern fits when: Low-frequency calls — desktop tools, not services Fast upstream upgrades matter — security CVEs, new algorithms You want transparent supply chain — auditable binaries The CLI is mature and stable — FFmpeg, OpenSSL have decades of CLI stability It fails when: High throughput — a TLS server, a per-request crypto service Large streaming I/O — backup, media transcoding at scale The CLI output is unstable — you'd spend your life chasing format changes I use the subprocess model for the 9 bundled CLIs in yyzTools.
If I were building a media server or a crypto API gateway, I'd static-link (or use a proper service).
The pattern is frequency-dependent.
A pattern worth naming I haven't seen this called out as a named pattern, but it shows up: VS Code ships ripgrep and calls it.
Many Electron apps bundle CLIs.
The principle—"ship the stock CLI, spawn it, parse stdout"—is a middle ground between static linking and microservices.
Position on the dependency spectrum: Static link Subprocess CLI gRPC service Perf fastest (function call) medium (fork + IPC) slowest (network) Upgrade cost recompile + regression drop a file redeploy service Crash isolation none process boundary network boundary Cross-language compile-time binding natural natural Best for high-throughput core low-freq trusted capability cross-machine The subprocess-CLI slot is underused.
For a desktop app bundling many third-party capabilities, it's often the right slot.
The honest takeaway If your tool calls a third-party engine a few times a session (not thousands of times a second), and you care about fast upgrades and transparent supply chain, shipping the stock CLI as a subprocess beats static-linking.
You pay per-call latency and a parser to maintain; you gain zero-recompile upgrades and crash isolation.
Don't use it for hot paths.
Do use it for "I need OpenSSL/FFmpeg/ImageMagick occasionally, and I don't want to recompile when they patch a CVE." I'm building yyzTools — a free, local-first Windows productivity suite.
Website: yyztools.com.
The 9 bundled CLIs are OpenSSL, 7-Zip, FFmpeg, ImageMagick, pdfcpu, Aria2, RapidOCR, Everything, and Ghostscript.