DEV Community

yyzTools
yyzTools

Posted on

Shipping Stock CLIs as Subprocess Instead of Static-Linking SDKs

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

  1. 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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

Top comments (0)