Get AI summaries of any video or article — Sign up free
We Removed C++ thumbnail

We Removed C++

The PrimeTime·
5 min read

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.

TL;DR

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?

They needed Fish to remain runnable and testable during the transition. A long rewrite that “disappears into a hole” would have delayed releases and prevented most end-to-end tests from running. The team kept the existing C++ subsystem structure to make before/after comparisons possible, and ported built-ins first because they’re easier to isolate (each has its own arguments, streams, and exit codes). Rust and C++ coexisted while calls crossed the boundary via FFI glue, and C++ implementations stayed around until the Rust versions fully replaced them.

What concrete C++ problems pushed the project toward Rust?

The complaints were operational: inconsistent tooling and compiler behavior across platforms, poor ergonomics in large codebases, and recurring thread-safety/ownership confusion. Developers described how teammates could disagree on pointer/ownership conventions (e.g., unique_ptr expectations), and how autocomplete/definition-jumping could work only partially due to macros and templates. Safety issues also featured heavily: unsafe string handling, confusing overloads, and the ease of triggering use-after-free bugs with string_view when lifetimes aren’t handled carefully.

How did Rust’s concurrency model factor into the decision?

Rust’s send/sync rules were presented as a practical safety net for cross-thread correctness. The team noted that Fish currently runs internal commands serially while external commands run in parallel, and that lifting this limitation would enable asynchronous prompts and non-blocking completions. Rust’s type system was viewed as a way to unlock fully multi-threaded execution with fewer “accidentally share objects across threads” hazards than C++ tooling alone could prevent.

What were the biggest technical wins after the port began?

One major win was replacing curses with a Rust terminfo-based approach, which removed build-time grief and reduced awkward global state. Another was packaging: Fish can ship self-contained packages that include functions, completions, and assets, and the team produced statically linked Linux binaries as single-file downloads for users who can’t install system packages (e.g., no root access). The port also reported slightly better runtime performance, with memory showing a modest increase at rest.

Where did the port struggle, despite Rust’s advantages?

Portability still required careful target handling, and the team described it as “ifdef life” because Rust portability often depends on enumerating OS/version targets and feature-detecting at build time. Binding generation was another pain point: autocxx sometimes failed to understand specific C++ constructs, forcing wrapper types (e.g., wrapping C++ vectors) and workarounds for wide-character support. The team also hit translation mismatches that could lead to crashes/panics when C++ assumptions didn’t map cleanly to Rust error handling.

How did the team handle Rust setup and developer onboarding?

They leaned on rustup as a key enabler: installing and upgrading Rust tooling is treated as “magic” compared with the C++ reality of finding repositories with root permissions, compiling compilers, or using Docker images. The project also highlighted Rust’s error messages as a major productivity boost, alongside the ability to quickly install dependencies through Cargo.

Review Questions

  1. What rewrite strategy let Fish keep end-to-end tests running while replacing C++ with Rust, and why was that critical?
  2. Which Rust features (tooling and type-system mechanisms) were credited with improving concurrency safety and developer experience?
  3. 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. 1

    Fish 4.0 shipped with 0% C++ by incrementally porting subsystems to Rust while keeping a working, testable shell at every stage.

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

    C++ was criticized for practical issues in large projects—tooling inconsistency, ergonomics, and safety problems tied to ownership, strings, and thread interactions.

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

    A major functional improvement came from replacing curses with a Rust terminfo crate, reducing build-from-source friction and global-state awkwardness.

  6. 6

    Packaging improved through self-installable Fish packages and statically linked Linux binaries distributed as single-file downloads for users without root access.

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

Highlights

Fish 4.0 is a shipped Rust rewrite: the release is described as having 0% C++ and nearly all logic in Rust.
The team avoided a “big rewrite” trap by keeping Fish runnable throughout, porting built-ins first and using FFI glue until each Rust component was ready.
Replacing curses with a Rust terminfo approach removed a major build dependency and simplified how terminfo data is handled.
Rust’s send/sync model was presented as the key to enabling fully multi-threaded execution without relying on fragile discipline alone.

Topics

  • Fish Shell Rewrite
  • Rust Port
  • Concurrency
  • FFI Migration
  • Portability
  • Packaging

Mentioned

  • LTS
  • LSP
  • FFI
  • LTO
  • LTS
  • C++
  • C++11
  • C++17
  • UTF-32
  • API
  • CLI