# 27 FEB 2026 · DEVELOPMENT

Why Rust works for small tools

Why almost everything Humanly ships is written in Rust: one native binary per platform, memory safety for parsers, one shared core, and the costs we pay for it.

>_[ FIG. 00 · DEVELOPMENT ]×
Diagram: one shared Rust core feeding a command-line tool, a desktop GUI, a library crate and mobile apps, with the macOS, Windows and Linux target triples it builds forONE RUST CORE, FOUR SHAPESCARGO WORKSPACE · ONE LANGUAGE FROM PARSER TO PACKAGESHARED RUST CORECRATE: CORE / LIB.RSPARSERSFILE FORMATSNETWORKCHECKSUMSTESTSCOMMAND LINESERVE · IDNX · CAPA_CLIDESKTOP GUIEXIF AI (EGUI) · DSKCOPY (SLINT)LIBRARY / CRATEEXIF-AI · CAPA · IDNXMOBILEZYPHR (IOS, ANDROID)11ONE CORE, WRITTENAND TESTED ONCE22STATIC BINARYNO RUNTIME TO INSTALL33NATIVE WINDOWSAME CORE UNDERNEATH44PUBLISHED CRATEOTHERS BUILD ON IT55NATIVE APPSCALL THE SAME COREONE CODEBASE, FOUR FRONTScargo build --release --targetMACOSaarch64-apple-darwinx86_64-apple-darwinWINDOWSx86_64-pc-windows-msvcaarch64-pc-windows-msvcLINUXx86_64-unknown-linux-gnuaarch64-unknown-linux-gnuDIAGRAM

People sometimes ask why almost every tool we make is written in Rust. serve, Exif AI, dskcopy, idNX, capa-rs and Velocty are all Rust, and the native Zyphr apps share one Rust core. It was not a rule we set on day one. It is where we kept ending up, because Rust works very well for small tools that have to run on other people's machines and read files we did not write. It also has real costs, and we would rather be plain about both.

One binary, nothing to install first

A Rust program compiles to a native executable. There is no interpreter, no virtual machine and no runtime the user has to install before your tool will start. The standard library and every dependency are compiled into the file you ship.

For a tool like serve that is most of the point. You download one file, put it on your path and run it. HTTPS comes from rustls, a TLS library written in Rust, so there is no OpenSSL to find, match or patch on the user's system. Compare that with "first install Python 3.12, then create a virtual environment", or a Java tool that needs the right JDK. Every step before the tool starts is a step where someone gives up.

One detail, because it trips people up. On Linux, the default x86_64-unknown-linux-gnu target still links the system's glibc dynamically. That is fine on nearly every desktop and server distribution. If you want a file with no dynamic dependencies at all, for Alpine or a minimal container, you build for the musl target instead. On macOS and Windows the system libraries are always there, so the question does not come up in the same way.

The compiler supports a long list of targets, named by triples like aarch64-apple-darwin or x86_64-pc-windows-msvc. For most of our tools the download pages offer builds for macOS, Windows and Linux on both x86_64 and ARM, and they come from the same source with a different --target. Adding a target to the toolchain is one command:

rustup target add aarch64-unknown-linux-gnu
cargo build --release --target aarch64-unknown-linux-gnu

That is the easy part. Cross-compiling stays easy while everything is pure Rust. Once a dependency pulls in C code, or you need a platform SDK for signing and linking, it gets harder, and building each operating system on a machine of that operating system is often the calmer route. We wrote more about the rest of the shipping work in one codebase, three desktops.

Parsing files you did not write

Look at what our tools actually read. Exif AI opens photos and parses EXIF, XMP and IPTC blocks across many formats. dskcopy reads disk images and decompresses archives on the fly. capa-rs takes PE, ELF and Mach-O binaries, often malware samples, and picks them apart. idNX decodes network discovery packets from whatever happens to be on the wire. All of that is input from strangers, and some of it is hostile on purpose.

This is exactly the kind of code where C and C++ have a long history of trouble. A length field that lies, an offset that points past the end of a buffer, a nested structure that loops back on itself. In a memory-unsafe language, each of those can become an out-of-bounds read or write, and sometimes code execution. Microsoft and the Chromium team have both reported that roughly 70 percent of their serious security bugs were memory safety issues.

Safe Rust rules out that whole class. Slices carry their length and indexing is bounds-checked. The borrow checker stops use-after-free and data races at compile time. A malformed EXIF block with a bogus length can still make our parser return an error or, if we wrote it badly, panic. It cannot quietly overwrite memory next to it.

We want to be careful not to oversell this. Rust does not stop logic bugs. It does not stop a crafted file from making a parser allocate too much or spin for a long time. unsafe blocks exist, and so do C libraries under safe wrappers, and those are where memory bugs can still live. Parsers still deserve fuzzing, and every file is still untrusted. But the baseline is much higher, and for a studio that makes security tools it would feel strange to write the file readers in a language where one missed check can hand over the process.

One core, several front ends

The drawing at the top is the shape most of our projects take. One library crate holds the real work: the parsers, the file formats, the network code, the checksums and the tests. Around it sit thin front ends.

  • A command-line tool. serve, idNX and capa-rs all ship as one. dskcopy and Exif AI have a CLI next to their desktop app.
  • A desktop GUI. Exif AI uses egui, an immediate-mode toolkit. dskcopy uses Slint, which describes the interface in its own markup language. Both call the same core as the CLI.
  • A library. Exif AI is published as a crate on crates.io, capa-rs is a library as well as a command, and idNX can be used as a Rust library. If the core is good enough for our front ends, it is usually good enough for someone else's program.
  • Mobile. Zyphr's native apps share one Rust core. The screens are native to each platform, and the logic underneath is the same code.

Cargo workspaces make this cheap. Each front end is its own crate with its own dependencies, so the CLI never pulls in a GUI toolkit and the core never learns what a window is. When we fix a parsing bug, the fix lands in the CLI, the desktop app and the library at the same time, and the tests that caught it run against all of them. For mobile, tools such as Mozilla's UniFFI can generate Swift and Kotlin bindings from a Rust interface, which saves writing the glue by hand.

Cargo, speed and size

Cargo is one of the quieter reasons we stay. Building, testing, documentation, benchmarks and dependency management come from one tool with one lockfile, and every Rust project we open works the same way. cargo test runs the tests. cargo doc builds the docs. Nobody on the team has to learn a project's private build system before they can change a line.

Performance is predictable more than it is magic. There is no garbage collector, so there are no collection pauses, and memory use is roughly what the code asks for. That matters when you copy a large disk image or scan a subnet. It does not mean Rust code is automatically fast. A slow algorithm is slow in any language, and we still profile before we optimise.

Binary size needs a little care. A default release build carries symbols and code you do not need. A few settings in Cargo.toml make a real difference:

[profile.release]
lto = true
codegen-units = 1
strip = true
panic = "abort"

With those, a command-line tool usually ends up in the low megabytes. A GUI app is bigger, because the toolkit and its renderer come along, but it is still far smaller than an app that bundles a whole browser engine. People notice a small download, and they notice a tool that opens instantly.

What it costs us

None of this is free.

Compile times. A clean release build of a mid-sized project with a GUI toolkit can take several minutes, and link-time optimisation makes it longer. Incremental debug builds are much faster, and splitting the code into crates helps, but anyone coming from Go or a scripting language will find the wait annoying. CI builds for six targets add up.

The learning curve. Ownership, borrowing and lifetimes take a while to click. For the first weeks the compiler feels like it is arguing with you. Later you notice it was mostly right, but that period is real, and it matters when you want a new person to contribute quickly.

GUI toolkits. This is the weakest part of the story. egui is quick to build with and portable, but it does not look or behave like a native Mac or Windows app, and it draws its own widgets. Slint is more polished for designed interfaces and is worth reading the licence for. Accessibility, text input for complex scripts and platform conventions are all still catching up with AppKit, WinUI or Qt. For our tools, where the window is a simple front for real work, that is acceptable. For an app where the interface is the product, it might not be.

Async. Async Rust works well once it is set up, and most networking crates assume the Tokio runtime. But it brings its own complexity: Send bounds, pinning, error messages that point at the wrong place, and a split between async and blocking code that spreads through a codebase. We try to keep async at the edges, where the network is, and keep the core plain synchronous code that is easy to test.

Dependency trees. Crates are small and easy to add, so a modest tool can end up with a couple of hundred of them once you count everything underneath. Each one is code someone else wrote that ends up in your binary. The habits that help: read cargo tree before adding a crate, commit the lockfile, and check dependencies against the RustSec advisory database with cargo audit. It is manageable, but it is work, and it never quite goes away.

When we would pick something else

Rust is our default, not a belief. A throwaway script that renames some files is faster to write in Python or shell, and nobody needs it to be memory-safe. A web front end belongs in the browser's own languages. A mobile app where every screen follows the platform's design language is better served by Swift and Kotlin for the interface, even when a Rust core sits underneath. And for client work, the team that will maintain the code afterwards matters more than our preference. If they write TypeScript, a tool in TypeScript they can change beats a Rust tool they cannot.

Where Rust earns its place is the kind of tool we keep making: small, native, cross-platform, reading untrusted input, and meant to run for years with little attention. For those, one binary per target and a compiler that refuses a whole class of bugs are worth the minutes spent waiting for a build. The official Rust site has the installer and the book if you want to try. Start with a command-line tool that parses a file format you care about. That is where the language makes the most sense.