We Removed C++
Based on The PrimeTime's video on YouTube. If you like this content, support the original creators by watching, liking and subscribing to their content.
Fish 4.0 shipped with 0% C++ by incrementally porting subsystems to Rust while keeping a working, testable shell at every stage.
Briefing
Fish 4.0 has shipped with 0% C++ and an almost entirely Rust codebase, marking a rare “rewrite-while-staying-shippable” modernization for a widely used Unix shell. The move matters because it targets the pain points that made C++ a poor fit for Fish’s day-to-day needs—especially tooling, safety, and concurrency—while keeping the shell usable throughout the transition.
The port was framed as a component-by-component replacement that preserved a working Fish at every stage, avoiding months-long “disappear into a hole” rewrites that would have stalled development and broken end-to-end testing. Built-ins—small programs with their own argument streams and exit codes—became the first targets because they could be ported independently. The team used a “strangler pattern” approach: Rust and C++ coexisted while calls crossed the boundary through FFI glue, with C++ versions kept around until the Rust replacements were complete. To keep comparisons meaningful, Fish largely retained the existing C++ subsystem structure instead of doing a full redesign.
The C++ critique was practical rather than theoretical. The transition highlighted recurring friction in short tools and compilers, platform differences, and ergonomics—especially around thread safety and ownership. Developers described how C++ changes often triggered cascading debates over pointer types and ownership conventions, and how tooling like autocomplete and definition jumping could fail unpredictably in large codebases. Safety concerns also came up repeatedly: verbose and error-prone string handling, unsafe char-pointer temptations, and modern C++ features like string_view being easy to misuse into use-after-free bugs.
Rust was chosen for its tooling and its concurrency model. The team emphasized rustup for easy setup, strong compiler errors, and the “send”/“sync” type system as a guardrail for cross-thread correctness. While Rust isn’t portrayed as perfect—portability still requires careful target enumeration and can devolve into “ifdef life”—the overall development experience was considered better aligned with Fish’s needs.
Key technical outcomes included replacing the curses dependency with a Rust-based terminfo approach, eliminating a major source of build grief and reducing global-state awkwardness. The port also enabled packaging improvements: Fish can be distributed as self-installable packages that bundle scripts, completions, and assets, and the team produced statically linked Linux binaries as single-file downloads for users without root access.
The project’s timeline stretched from an initial Rust rewrite proposal in 2023 to a beta in 2024, with the full release arriving in February. Along the way, the team hit dead ends and false starts—especially around auto-generated C++ bindings (autocxx), portability edge cases, and translation mismatches that sometimes surfaced as crashes or panics. Even so, performance landed slightly better in time-to-run, with memory showing a modest tradeoff.
Fish 4.0 remains “an odd duck” in Rust terms, retaining some low-level C-like patterns (such as direct file descriptor handling and UTF-32 strings), but the release is positioned as a foundation for further modernization—particularly around unlocking fully multi-threaded execution for features like asynchronous prompts and non-blocking completions. C++ is declared effectively dead for Fish, not as a slogan, but as a shipped, testable reality.
Cornell Notes
Fish 4.0 ships with 0% C++ and an almost entirely Rust implementation, achieved through an incremental “strangler pattern” rewrite that kept the shell working and testable throughout. The team replaced C++ because of persistent pain in tooling, ergonomics, safety, and thread-related correctness, while Rust offered stronger compiler diagnostics, easier setup via rustup, and concurrency guardrails via send/sync. Porting started with relatively self-contained built-ins, then expanded to more entangled subsystems, using FFI glue until Rust equivalents were ready. Major wins included dropping curses in favor of a Rust terminfo crate, improving build-from-source reliability and simplifying packaging. The release is not portrayed as perfect—portability and binding generation still created friction—but the project reports slightly better runtime performance and a manageable memory tradeoff.
Why did Fish’s team treat the rewrite as a “component-by-component” modernization instead of a full replacement?
What concrete C++ problems pushed the project toward Rust?
How did Rust’s concurrency model factor into the decision?
What were the biggest technical wins after the port began?
Where did the port struggle, despite Rust’s advantages?
How did the team handle Rust setup and developer onboarding?
Review Questions
- What rewrite strategy let Fish keep end-to-end tests running while replacing C++ with Rust, and why was that critical?
- Which Rust features (tooling and type-system mechanisms) were credited with improving concurrency safety and developer experience?
- Name two categories of porting friction the team encountered (e.g., portability, bindings, localization, build/test integration) and describe what they did to mitigate them.
Key Points
- 1
Fish 4.0 shipped with 0% C++ by incrementally porting subsystems to Rust while keeping a working, testable shell at every stage.
- 2
The rewrite followed a strangler pattern: built-ins were ported first, with Rust and C++ coexisting via FFI glue until Rust replacements were complete.
- 3
C++ was criticized for practical issues in large projects—tooling inconsistency, ergonomics, and safety problems tied to ownership, strings, and thread interactions.
- 4
Rust was selected largely for its development experience (rustup, strong compiler errors) and concurrency guardrails (send/sync) that support safer multi-threaded execution.
- 5
A major functional improvement came from replacing curses with a Rust terminfo crate, reducing build-from-source friction and global-state awkwardness.
- 6
Packaging improved through self-installable Fish packages and statically linked Linux binaries distributed as single-file downloads for users without root access.
- 7
The port still faced real challenges in portability, binding generation (autocxx), and translation correctness, but reported slightly better runtime performance with a modest memory tradeoff.