Sysadmin

Why I Chose Rust for Desktop Tools

When I started building desktop tools, C# with WPF was the natural choice. The ecosystem was mature, Windows was the only target, and the tooling was genuinely good. Then I wrote DiskSleuth — a disk space analyser that needed to walk the entire NTFS volume tree quickly — and C# made that painful enough that I rewrote it.

The rewrite was the tutorial I needed. I’ve shipped half a dozen tools in Rust since then, and this is the honest account of what Rust gives you, what it costs, and when I still reach for something else.

The DiskSleuth problem

DiskSleuth’s job is to enumerate every file on a drive and build an aggregated size tree. On a drive with a million files, that means a million MFT entries. My C# prototype used Directory.EnumerateFiles recursively. It worked, but it would balloon to 800 MB of RAM and trigger GC pauses that would freeze the UI mid-scan. On large drives the scan took minutes.

The Rust rewrite introduced two things that changed the picture entirely.

FSCTL_ENUM_USN_DATA via the MFT. When you’re running elevated, the NTFS Master File Table is directly accessible through a Windows IOCTL. FSCTL_ENUM_USN_DATA lets you walk every file record on the volume without touching the directory structure at all. SwatLab’s DiskSleuth uses a 256 KB IOCTL buffer (versus the typical 64 KB) and reads MFT entries in parallel with rayon. The entire enumeration for a 500 GB volume happens in a few seconds rather than minutes.

Arena-allocated tree. The file tree is stored as Vec<FileNode> with NodeIndex(u32) offsets instead of pointer-based nodes. This is cache-friendly — iterating over children means sequential memory reads, not pointer chasing. Aggregating sizes bottom-up is O(n) over a flat array. The GC pauses went away because there’s no GC.

The Rust version does the same work in under 40 MB with no pauses. That’s not a marginal improvement — it’s a different class of software.

The workspace pattern

Rust’s Cargo workspace feature encourages a kind of structure that I wish I’d been enforcing in C# all along. DiskSleuth is two crates: disksleuth-core (scanning logic, data structures, tree operations) and disksleuth-gui (the egui application). The dependency is one-directional. The core crate has no knowledge of the UI.

This isn’t just organizational. It means you can write tests for the scanning logic without ever touching the GUI. It means if egui ever gets replaced, the core logic doesn’t change. In C# I was using MVVM patterns to achieve the same separation; in Rust the crate boundary enforces it.

What the type system actually catches

The borrow checker is Rust’s headline feature and also its biggest learning curve. What nobody tells you until you’ve used it for a while is that the type system catches things you wouldn’t think to test.

In DiskSleuth, there’s a real-time scanning mode where the UI thread needs to read the tree while a background thread is still writing to it. The naive approach is to share the tree with a mutex. The safe approach — which the borrow checker essentially forces you toward — is Arc<RwLock<FileTree>> with batched writes to minimize contention. The Rust version batches 2,000 entries per write-lock acquisition. Without the borrow checker, you don’t get nudged toward thinking about contention at all.

In SwatCrypt, #![forbid(unsafe_code)] is a lint directive in the crate root. It means any unsafe block — anywhere in the SwatCrypt code — fails to compile. The cryptography is in audited upstream crates that have their own unsafe budgets. The application code has none. This isn’t possible to express in C# or TypeScript in the same way.

Binary size and deployment

Every tool I’ve shipped in Rust is a single executable. No installer, no runtime, no DLL dependencies. LockSmith (the SMB file lock tool) is about 4 MB. EventSleuth (the Windows Event Log viewer) is comparable. These ship as a single file you can copy to a server and run.

The comparison that matters most is HandleHunter, which does the same job as LockSmith but in pure C with no runtime. HandleHunter is 134 KB. LockSmith is 4 MB. The difference is egui — the immediate-mode UI framework adds bulk but saves significant development time. For a tool where binary size actually matters (emergency recovery, USB stick deployment), I’d use the C version. For everything else, the Rust version’s 4 MB is fine.

Performance-wise, LockSmith starts in about 0.5 seconds. That’s native code with no JIT warmup and no WebView2 spin-up. The previous Tauri version of a similar tool used roughly twice the memory and took about 1.5 seconds to show its first frame. egui compiles to the executable directly — there’s no embedded browser.

egui: good enough and no more

egui is an immediate-mode GUI framework for Rust. It renders with whatever backend you configure — typically OpenGL or wgpu — and has no external dependencies. Every widget call is a function call that runs every frame. There’s no DOM, no event listeners, no retained widget state tracked separately from your application state.

The tradeoff is apparent polish. egui apps look like egui apps. They don’t look like native Windows applications with Fluent UI components. For tools primarily used by people who know what they’re doing, this is fine. For consumer-facing software, it’s a real limitation.

The alternative I use for tools with more complex UIs is Tauri — a Rust backend with a web frontend. QuickProbe (server fleet monitoring), BitBurn (file wiping), and PSForge (PowerShell IDE) all use Tauri. The frontend can use anything — React, TypeScript, Tailwind. The backend is still Rust. The ergonomics of web development work for complex interactive UIs where egui’s simplicity would become a constraint.

Where I still don’t use Rust

PSBench, the PowerShell module explorer, is WPF with C#. The reason is direct: it hosts a PowerShell runspace (PowerShell.SDK via InitialSessionState.CreateDefault2()), which is a managed runtime that already has a CLR dependency. There’s no advantage to a Rust wrapper around a managed object. C# is the right tool because the heavy lifting is already a .NET concern.

If your tool is primarily a wrapper around a .NET API, don’t pretend Rust is simpler. PSBench uses MVVM, dependency injection, and two isolated runspaces (one for module introspection, one for command execution). That’s a well-understood C# architecture that would be significantly more work to replicate via FFI.

The tradeoffs that remain true

The borrow checker will fight you. The compile times are slower than Go or C#. The GUI ecosystem — while genuinely improving — still can’t match WPF or SwiftUI for polish out of the box. These are real costs.

What you get in return: predictable memory usage, no runtime to distribute, no GC pauses under load, and a compiler that catches whole categories of concurrency bug before the code ships. For security-adjacent tools where “it crashes in production” is particularly bad, those guarantees are worth the compile-time friction.

The concrete answer to “should I use Rust?” is: if you’re building something where performance, binary size, or memory predictability are material concerns — write it in Rust. If you’re wrapping a managed API or building something where rich ecosystem integrations matter more than the runtime footprint — use the language that fits the dependency.

All of my Rust tools are on GitHub, MIT licensed.

← All articles