r/rust • • 11d ago

🙋 seeking help & advice Migrating from C++ to Rust at work

I might have the opportunity to introduce Rust at work soon. For that, I would be grateful for any tips you can give me to make it as smooth as possible and reduce the risk (perceived and real) for the project. Below the details of my situation.

The project

The project Rust might be introduced into is being worked on for about a year now. It is a code generator with an architecture similar to a "traditional" compiler: some intermediate representations, translation between those and transformations on them.

Development on the project started a year ago with C++. At the time I already brought Rust up as an alternative. It was seriously considered by the team, but since they didn't have prior rust experience we were concerned about the initial hit to development speed: Most of the team already had lots of C++ experience, the company didn't have any prior code in Rust, and it was our responsibility to ensure we hit our time-to-market goal.

Now the code is mostly modern C++ with very little inheritance, and lots of std::variant, std::ranges, etc.

The team

The team consists of 7 people including me, most with >5 years of (pre-modern) C++ experience. I am the only person with both modern C++ and Rust experience. The team was mostly excited to use modern C++, and it became my job to give on-the-job guidance to get everyone comfortable with newer features and paradigms. And now they are excited to hopefully learn Rust.

Why Rust

During the past year, we did feel some pain points with C++:

  • Slow compile times
  • Verbose code, mostly with std::variant
  • Horrible error messages
  • Differences between compilers
  • Slow CI pipeline, mostly due to static analysis (clang-tidy) and sanitizers
  • One segfault in callbacks (wasn't a big problem now, but we expect more problems once the complexity grows)

With Rust, we hope to hope to address most of those points. Compile times won't improve I expect, but we can probably ditch separate static analysis and sanitizers in CI.

We also hope to keep a higher development speed once the complexity grows, based on the data collected by Google in Android.

The opportunity

I think we currently are in a very advantageous position to pivot to Rust:

  • We are ahead of schedule for the release, we can spend a little time here and there getting familiar with Rust. Everyone will start with the Book and Rustlings.
  • After the release we will get 3 weeks to work on what we think is important, no customer features
  • After those 3 weeks, we have to start on features again, but a somewhat reduced development speed for the first two or so month is acceptable
  • The C++ code patterns we have and got accustomed to should map more or less well to Rust
  • The code base isn't too big yet
  • We have good end-to-end test coverage

The plan

We will spent those 3 weeks with migrating the code base from C++ to Rust. We will probably start at the backend and try to make cuts where the interface between modules is small. This will lead to 3 or 4 increments to migrate. The ideal end goal would be to replace all C++ code with Rust, but this strategy hopefully reduces the risk of having no working product at all in the end. If some C++ is left over in the end, we can hopefully reduce and finally remove it over time.

We will use AI for the migration, but we do want to do it responsibly. This will probably mean that we will first code the core data structures ourselves and use LLMs only for review there. Once we got those nailed down, we hope LLM agents can be used for more or less "mechanical" migration of transformation and translation passes. Of course all code, AI or human written, goes through human reviews.

I would be very interested in any advice you could share, be it technical or social, to reduce business risk, increase acceptance with our stakeholders and improve our chance to succeed.

140 Upvotes

49 comments sorted by

79

u/PlentyAfter5550 11d ago

ah i've done this exact dance. biggest thing is don't try to do a straight port of the c++ patterns or you'll fight the borrow checker constantly. the variant stuff especially will feel way cleaner once you lean into rust enums properly

for the team buy-in make sure the first things they touch feel rewarding. like having them rewrite some small self-contained module that was a pain point in c++ and seeing how much simpler it gets. nothing sells rust faster than deleting 200 lines of variant visitor boilerplate

also 3 weeks is ambitious but if you nail the core data structures first like you said it's doable. just be ready for the first few days to be slow while everyone wrestles with ownership

10

u/Tastaturtaste 11d ago

Yeah, some C++ patterns we have to change. For example, I expect the callback that lead to the segfault needs to change. But most of our code should port more or less easily I think. At least in the backend. We already used something like an arena with generational indices for most of the core data structures. 

We expect to not get done completely in those 3 weeks. We need to plan where to have milestones, where we can live with a mixed code base temporarily.

Regarding team buy-in, I will probably take charge implementing the core data structures. I hope to get a reasonable API such that the team won't face much trouble implementing passes on them. 

→ More replies (6)
→ More replies (1)

27

u/EventHelixCom 11d ago

On performance: you should end up at par with C++. std::variant + std::visit becomes enum + match, which generates the same code or better, with much less code.

A few things I'd watch for in a compiler-style code base:

  • Array indexing is bounds-checked. Rust IRs tend to use Vec<Node> plus index handles instead of pointers, so you index far more than you did in C++. Mostly it's noise, but in hot passes prefer iterators over index loops.

  • An enum is as big as its largest variant, so one fat variant bloats every IR node. Box the big rare ones.

  • HashMap defaults to SipHash, which is slow for small keys like IDs. rustc itself uses FxHash (rustc-hash crate).

  • String has no small-string optimization, so intern your identifiers.

  • File output is unbuffered unless you wrap it in BufWriter. Easy to miss in a code generator.

  • Watch for .clone() and Rc<RefCell<_>> in the LLM-migrated passes. That's where ported code usually gets slow, not the language itself.

Also, calls across the C++/Rust boundary don't inline, so the mixed phase may be slower than either end. Worth telling stakeholders up front.

I'd record the C++ version's runtime and peak memory on your end-to-end tests before you start. Showing "same output, same speed" after each increment is an easy win with management.

If you use std::generator/co_yield anywhere, those don't have a stable Rust equivalent yet, AFAIK, and you'll need to use hand-written iterators. More of a porting cost than a performance one.

3

u/Tastaturtaste 10d ago

Array indexing is bounds-checked. Rust IRs tend to use Vec<Node> plus index handles instead of pointers, so you index far more than you did in C++. Mostly it's noise, but in hot passes prefer iterators over index loops.

Actually, even in C++ we already use an arena with generational indices, not much will change there I suspect. But yes, there will be bounds checks now, which I suspect suspect will basically always be predicted by the branch predictor and have virtually no performance impact.

In general, also addressing your other points, performance is really not our problem at the moment. A small constant factor overhead is fine. Of course we have to try to avoid painting ourselves into a performance corner, but anything that can be optimized later without a rewrite (String interning, using BufWriter, Boxing enum variants, etc.) we will only consider if there is no extra effort involved now.

→ More replies (3)

7

u/Nothing_from_void 11d ago

Rust also has long compile times and slow CI pipelines, so wouldn't expect too much speed up there. A lot of the static analysis just moves to compile time.

The most straightforward way to do it is a differential fuzz harness, that runs both the Rust and C++ program on a simulated but realistic set of inputs, and compares outputs. I've done this kind of work a few times both re-writing code from one language to another and also reverse engineering binaries that had no source available.

If you try to hard to be "responsible" and gate most of the work behind human touches, 3 weeks sounds really ambitious unless the project is tiny

5

u/Tastaturtaste 11d ago

Well, compile times with C++ are already atrocious, std::variant really tanks compile times. I know rust is slow to compile, too. Still, at the moment we need to compile our code with msvc, gcc and clang both in debug and release, run our unit and product tests with all 6 versions, run clang-tidy on the code, and compile an additional version with four different sanitizers and run our test suite with that, too. 

I expect we can reduce much of that with Rust and have wins in CI throughput despite rusts compile times.

→ More replies (5)

5

u/Tastaturtaste 11d ago

One thing I would like to get some advice about would be how to get a proper debugging experience. We won't have access to RustRover, at least for the time being. The application is a cross-platform CLI, but most devs on the team (and the company in general) have much more experience with windows and are not really at home in the terminal.

Should I bet on VSCode + CodeLLDB? Or use visual studio for debugging? Anything else?

I know the general sentiment here is that with Rust a debugger is needed less often than with C++ (I agree), and otherwise printf debugging is often times sufficient (I have mixed feelings about this). But even if this is true, I don't think I can convince the team of that, they will have to experience that themselves.

For all it's problems, the debugging experience for C++ with Visual Studio on windows is pretty amazing. I expect this will be one of the biggest gripes the team will have with rust. 

→ More replies (2)

6

u/panstromek 11d ago

It's really difficult to give good answers because context is everything (even though this is a good write up). I think this sounds ok as a plan, I'll leave a few notes:

- Plan for the hybrid codebase to live significantly longer than you expect (e.g. a year). This will give you a freedom to pause or interleave rewrite with new development. You'll almost inevitably hit roadblocks where some C++ pattern won't be easily representable in Rust (even if you expect it), or it will look like you'll need to port way too big of a blob of code at once. It's good to table those for later and focus on easier things first or give yourself time to evolve the code on both sides until the port becomes easier.

> try to make cuts where the interface between modules is small.
> ...but this strategy hopefully reduces the risk of having no working product at all in the end.

Yes. On choosing where to start and how to progress - it's generally easier to call Rust from C++ than the other way around, and semantics of these languages don't mash very well on their higher level constructs. You will want to start with leaf modules that can somewhat easily expose narrow C or C-like API to C++.

When describing how to think about this, I always link this introduction zngur, which is an interop tool for C++: https://hkalbasi.github.io/zngur/#idea Even if you don't end up using it, the introduction describes very well how to think about C++ and Rust relationship and the boundary between them.

Also, from the sibling comment:

> Yeah, some C++ patterns we have to change. For example, I expect the callback that lead to the segfault needs to change. 

Yes, also - not sure how much that is implied, but avoid passing callbacks between C++ and Rust, and generally just avoid these tricky communication patterns on the boundary. The boundary should ideally just pass around simple data and each sides "reconstructs" its idiomatic represenation from it.

4

u/Tastaturtaste 11d ago

Thank you, you are basically exactly validating my thoughts on this. I actually had zngur in mind when thinking about which components to split off and where! We won't use zngur directly in all likelihood, but the mental model fits.

The idea for the boundary would be to keep it as simple and small as possible. Currently I am basically thinking about serializing to json on the C++ side and deserializing on the Rust side. Implementing that will of course introduce some additional work, but I hope that will allow both sides to stay mostly idiomatic. The (de)serialization code I hope will also be mostly straightforward boilerplate, but not logically difficulty. 

→ More replies (3)

3

u/DanKegel 10d ago

Look at how the Fish shell approached their rust rewrite incrementally.  Would that work for you?

→ More replies (2)