In Rust, integer overflow for both signed and unsigned integers is defined to either (1) wrap around or (2) panic. The choice of which is implementation-defined, but it’s never UB.
Another, more pragmatic, obstacle is conditional compilation on the Rust side, e.g. with
#[cfg(…)]. In many common cases, this could be translated directly to C
#endif, but there’s nothing stopping you from writing silly things like
#[cfg(target_pointer_width = "64")]
type Foo = String;
#[cfg(target_pointer_width = "32")]
type Foo = i32;
…where the rest of the code can have a completely different interpretation (different type inference results, different trait choices, etc.) depending on the
cfg value. Real-world uses won’t be quite that silly, but it’s easy to imagine some code providing two separate implementations of some data structure depending on the pointer width, where the two implementations may be intended to have the same API but could still have subtly different behavior in the type system.
The easiest solution is to just transpile the whole crate twice, once assuming 64-bit pointers and the other assuming 32-bit, then concatenate both outputs into one C file with a giant
#endif surrounding them. Or, if you want to be extra clever, run the two outputs through
diff, and translate the diff itself to
#if blocks. However, that still doesn’t result in 100% portable C code; it assumes pointers are either 64-bit or 32-bit.
And it doesn’t address other cfg keys, such as
target_os. For most portable Rust crates you could probably just define
target_os = "unknown" (or something) and disallow any code that makes OS-specific assumptions. But if you have portable Rust code that uses, say,
libc, you can’t necessarily convert it to portable C code that uses libc. To do so, you’d need a sophisticated mechanism to allow for “constant expressions” that aren’t actually known at compile time, such as
sizeof(some_libc_struct); and even that can’t work correctly with some particularly obtuse code, such as if the constant expression appears in a generic parameter and something dispatches on it using specialization.
With all that said, I’m actually quite eager to see a Rust-to-C transpiler! I think the obstacles I mentioned wouldn’t be that big a deal in practice, and packaging certain Rust crates as “single-file C libraries” could further encourage adoption. Compare to the SQLite “amalgamation” distribution, where they concatenate the entire library into one big .c file, just because it’s easier to integrate into applications that way. That’s for a project that’s originally written in C, so the alternative would “just” be adding N files into your build system rather than 1 – but they still think that’s inconvenient enough to be worth making the amalgamation. On the other hand, with a Rust transpiler, the choice would be between adding one C file or integrating a whole new compiler toolchain (rustc) into your build: the C file provides a much larger advantage.