fish starts an incremental Rust port with a temporary C++ boundary
fish's Rust migration translates existing modules while preserving shell behavior. Its plan accepts temporary interop costs and keeps the work on the main development branch.
The difficult part of changing a shell's implementation language is preserving the behavior around the code being replaced. A shell must continue to interpret existing scripts and operate on the same filenames while its internals change. On February 19, fish merged the initial Rust-port implementation in pull request 9512. Its accompanying plan deliberately prioritizes translation of existing behavior over redesign.
The proposal's title uses the familiar rewrite-in-Rust phrase, but the implementation strategy is incremental. Rust and C++ will coexist while modules move across a temporary foreign-function boundary. The project intends to keep tests and continuous integration passing through the transition, with development on the main branch rather than a long-lived parallel implementation.
I like fish for its interactive behavior and the clarity of its own scripting language. An implementation port should preserve those properties before asking users to adapt to new semantics. The plan's explicit separation between translation and redesign gives the work a scope that can be reviewed against the existing shell.
Concurrency is a motivation, not a completed feature
The port plan at the merge commit names concurrent function execution as a reason to choose Rust. Background and concurrent functions have been a longstanding goal. The author describes an existing experimental branch but says that reasoning about which internal objects cross thread boundaries has made proposing it difficult in C++.
The intended advantage is to encode thread requirements into types through Rust's Send and Sync. That makes some constraints explicit and compiler-checked. The claim is about making concurrency manageable in the implementation. Merging the initial port does not itself make concurrent function execution available as a finished shell feature.
This distinction matters because language choice and program architecture are different decisions. A compiler can enforce properties that the program expresses through types. It does not decide which shell state should be shared or what concurrent execution should mean to scripts. The port creates a different set of tools for expressing those decisions.
The plan also names contributor access and toolchain maintenance. Those are the maintainers' reasons for the transition, not measured outcomes of the merge. Their expectation is that Rust's community and tooling can make future contributions easier. Existing contributors must also learn the new language, which the plan lists among the risks.
Translation proceeds inward through the implementation
The strategy starts with components near the outside of the dependency structure, such as builtins, and moves toward the core. Some utility components can temporarily retain both implementations. Major components should instead have callers cross the language boundary so the project does not maintain two independent versions of substantial behavior throughout the port.
The plan asks contributors to keep Rust recognizable against the replaced C++ where practical. Fidelity aids review and bisection. A reviewer can compare corresponding operations without also evaluating an unrelated algorithm change or a new internal abstraction. This is a temporary engineering priority, with judgment allowed where literal translation would be unreasonable.
The same document explicitly preserves the fish scripting language. It also retains locales, gettext, printf-style format strings, and PCRE2. These are compatibility decisions about the surrounding implementation, not evidence that every C++ design must remain permanently unchanged. The port allows a few selected string changes while postponing broader redesign.
I find that scope appropriate for a shell. Users depend on many small behaviors that an implementation author may regard as incidental. Translating those behaviors first keeps a later design change identifiable as a separate decision with its own compatibility consequences.
The build links a Rust library into the existing program
The development guide at the same commit places Rust code in the fish-rust crate. Cargo builds libfish_rust.a, which links with the existing C++ libfish.a. CMake remains the top-level build system during this stage, with Corrosion invoking Cargo.
The guide requires Rust 1.67 or later for the partially ported tree. Running Cargo directly builds the Rust library, not a complete fish executable. CMake must first generate configuration needed by the binding generator. A contributor therefore cannot assume that the presence of a Cargo manifest means the whole project already builds as a standalone Rust application.
The planned sequence replaces CMake after the C++ port. That avoids combining the entire implementation transition with an immediate replacement of every build path. It also means the project temporarily has both Cargo and CMake to maintain. The plan identifies the resulting period of complicated builds as a risk.
The pull-request discussion includes practical objections about distribution packaging and older systems. Maintainers ask how a recent Rust requirement affects targets whose C++ builds used much older toolchains. Questions about obtaining build dependencies in distribution infrastructure also appear. These are part of implementation feasibility, since a shell needs to remain buildable on the systems its users run.
Interop workarounds are deliberately temporary
The transition uses cxx and autocxx for Rust and C++ communication, including fish-specific forks. The development guide describes limitations around constants, aliases, and some combinations of C++ container types. Contributors sometimes need wrapper structures or pointer-based representations where direct bindings are unavailable.
The plan accepts inefficiency such as additional string copying at this boundary because it intends to remove the boundary after the port. That is a bounded design tradeoff. Optimizing every temporary conversion could consume time without improving the final implementation, while an inaccurate conversion could change behavior immediately.
The guide asks contributors to confine interop types to the relevant boundary modules. That containment matters beyond tidiness. If an implementation-specific wrapper appears throughout newly translated code, removing C++ later also requires changing those new modules. Keeping wrappers local reduces the amount of Rust that depends on the temporary arrangement.
Utility duplication has a similar rationale. A small logging facility can exist on both sides when connecting it would impose disproportionate complexity. That exception does not justify independently translating every major subsystem twice. The plan distinguishes short-lived duplication used to support migration from competing implementations of the shell.
The guide also makes the temporary safety tradeoff visible. Its forks relax some pointer-related requirements in the binding tools and add wide-string support specific to fish. Moving a component to Rust therefore does not establish that every operation across the mixed-language boundary has become safe by construction. Contributors still need to preserve pointer validity and representation assumptions where the languages meet.
That is consistent with the migration's staged purpose. Rust can check ownership within the translated code while interop still relies on contracts with existing C++ callers. Removing those callers later can remove the associated workarounds. Until then, reviewing boundary behavior remains part of reviewing each translated module. The development guide's explicit catalog of those workarounds gives contributors a place to understand what is temporary and why it exists.
Filenames prevent a simple switch to ordinary Rust strings
The string section is one of the most consequential parts of the plan. Rust's ordinary UTF-8 strings cannot directly represent all filename bytes using fish's existing encoding scheme. fish uses a private-use-area representation for invalid byte sequences. Converting everything through ordinary strings would lose the relationship between internal values and the original data.
The guide therefore uses WString and &wstr as the main owned and borrowed string types, backed by UTF-32 strings and slices. Existing fish encoding and decoding remain in use. The plan specifically warns that opening a file through a normal string path can decode the name incorrectly under this scheme.
Two selected changes accompany that continuity. Internal Rust strings need not remain nul-terminated, and the port drops support for a 16-bit wide-character representation in favor of UTF-32. Passing a string back to C++ code that expects termination may require allocating a terminated buffer. Separate interop string types make that requirement explicit during the mixed-language stage.
These details explain why the work is larger than converting syntax. A shell's strings represent paths, command data, and translated messages under established rules. The plan also preserves runtime-provided printf-style formatting because Rust's ordinary formatting system does not accept those runtime format strings in the same way. Retaining the representation prevents a language migration from becoming an accidental data-format change.
The merged work establishes a buildable mixed-language implementation and guidance for continuing the translation. Its schedule remains an estimate, with the plan explicitly acknowledging uncertainty about the remaining work. The immediate engineering commitment is incremental translation with tests maintained and interop costs contained until the C++ boundary can be removed.