Originally posted at https://tech.labs.oliverwyman.com/blog/2018/11/27/experiments-in-converting-code-from-c-to-rust/
I’m quite fond of Rust (as a few blog posts on the topic may indicate), but one item I hadn’t really explored was replacing/rewriting existing C code bases in Rust. There’s a general joke about the general notion of “rewriting everything in Rust is of course always the right thing to do!” (Google “rust evangelism strike force”, or see a Twitter account on the subject for more details), but there’s some details in there that actually make this slightly less of a joke, namely that Rust is capable of C-level performance while having a much better story on runtime safety.
On the other hand, you’re basically facing a from-scratch rewrite as Rust and C are very different languages, and rewrites are always tempting when something is being annoying, but it’s generally a bad idea. Ok, but what if someone had written a C-to-Rust translator? As it turns out there’s now two of them – Corrode (mostly now deprecated, but an interesting early step in the process) and now C2Rust (which the author of Corrode was at least partially involved with). I’d tried using the former last year but ran into some issues, and I’ve just been having a go with C2Rust this month. It’s got a few issues (no provided binaries, and the build process of it is pretty slow; various code it can’t translate), but overall it’s a fairly usable tool.
I’ve now done a rewrite of ashuffle using this, and I’ve got a few bits of notes that should be of use to anyone else doing this. Firstly, make sure your target project is C, not C++. I was going to convert ninja but that’s C++ and no-one’s written a converter for that. I’d advise using the Dockerised version of C2Rust, just because it wants to install many things and that makes your life easier. You’ll start by running your existing build system under bear to get a set of compilation commands that you can feed into the transpiler e.g.
... cd to ashuffle ...
bear make
... cd to C2Rust ...
./scripts/transpile.py -e <path to ashuffle>/compile_commands.json -m ashuffle
If you see an error like args.c:27:6: warning: variadic functions are not fully supported
, what that actually means is "that variadic function isn’t getting converted at all". Hopefully you’ve got an easy case like I did and all the variadic args are the same type, and you can make it into something that takes an array as the last argument, as Rust doesn’t support variadic functions yet. Once you’ve fixed that, re-run the transpiler.
You’ll now have a c2rust-build
folder containing a main.rs
and a Cargo.toml
. Move these into your top directory, and fix all the mod paths in main.rs (e.g. #[path = “../src/shuffle.rs”] pub mod shuffle;
becomes just pub mod shuffle
, but you might need the path still if you’ve got a more complex project). At this point, cargo build
should just work. Congratulations, you’ve got a Rust project! It’s pretty crap Rust, and everything is marked as unsafe, but it’s a good base.
So, you could just stop now. I’d advise against it, as there’s some fairly easy fixes that get you a definitely better code base, and a bunch of others that let you progressively fix improve things, but this is a decent start point. Assuming you want to go further, install cargo-watch and set it rebuilding your project in a terminal you can see, as we’re going to be doing some fairly extensive refactoring and that will just make your life much easier.
Each of your rust files will have an extern “C” {
block at the top. Now, these are various C functions that your code needs. For each of these, you should track down what library needs it and either find the existing Rust FFI library that defines this (e.g. libc for all the C library core), or split it out into a source file for that library (e.g. see the libmpdclient example from ashuffle-rs). Especially in the latter case, you may find multiple separate Rust source files all redefining the same functions/structures, and that’s because they were in a header file that C file referred to, and because Rust doesn’t have headers they haven’t gotten split out properly. You’ll see a few things like “pub struct _IO_FILE {” that you can delete and replace with various libc features (in this case libc::FILE) – a mix of underscores and ALL_CAPS is a dead giveaway for these. They come out like this because the underlying C headers define it like that, but mostly C programmers don’t notice this because they’re using the typedef’ed versions. One exception here is stdio/stdout/stderr – libc doesn’t include them and I made my own hacky temporary replacements.
Another item that you probably want to tidy up is that the C2Rust code comes out as nightly-only, because it needs various features like “extern_types“. I got rid of those using the uninhabited type hack from the RFC, and most of the others can just be deleted from your main.rs, as they’re mostly not needed most of the time.
Ok, so now we’re at the hard bit – rewriting code as better Rust. Generally you don’t want to be calling libc most of the time in Rust, as there’s usually better options (if you’re particularly unlucky, it’ll be missing an FFI construct for the function you want). Some of this is easy – replacing “__assert_fail” calls with “assert!”, “libc::exit” to “std::process::exit”, but some will require more work. For example, I rewrote some curses code for set_echo completely using easycurses, but figuring out how to do this is tricky and requires case-by-case knowledge. Eventually you’ll be able to get rid of most if not all of the unsafe’s and you’ll have a decent Rust codebase to build on further.
This is all a lot of work, even in ashuffle which is a relatively small project. For larger codebases, there’s been some work on using Corrode for incremental replaces but I haven’t tried the same thing with C2Rust yet. YMMV, and possibly just writing new code in Rust instead might be a better plan in many cases.