C++ vs Rust

WARNING: Terrible explainations of Rust, by someone who doesn't really know it, can be found bellow. Additionally, this is not meant as a full guide to the Rust language, but instead an extremely terse jumping off point for "I know how to do this in C++, but what's the Rust equivalent again?" style questions, and nonobvious differences from C++ that may trip you up comming from C++.

Table of Contents

Why Rust
Why Not Rust
Reading List
Watching List
Common Errors
Type Equivalents[see also Notes on Type Layouts ...]
Syntax Equivalents
The Obvious
The Less Obvious
Interior Mutability[see also The Rust Programming Language]
Dynamically Sized Types[see also The Rustonomicon]
Virtual Dispatch
Function Overloading
Are we MaulingMonkey yet?
Crates of Note

Why Rust

C++ is rife with Undefined Behavior and the resulting debugging sessions. Static analysis tools like Clang's Thread Safety Annotations, nullable pointer attributes, or iterator invalidation checks are are opt-in, unevenly applied, limited in scope, and often rife with false positives — although still useful.

Rust's borrow checker is an opt-out, consistently applied, broad and generalized tool for catching data races in general. In general, unless you're using the rarely needed unsafe keyword, your code has defined behavior, despite having stricter aliasing restrictions than C++! It compiles to native using the same LLVM backend Clang does, doesn't add a GC, is portable to consoles and embedded devices.

Why Not Rust

Things like out of bounds indicies still aren't caught until runtime. Fighting the borrow checker will take 2-4 weeks for a C++ programmer to get used to. It's relatively new, and as a result still has gaps in tooling and library ecosystems that need addressing. Fewer gaps than other languages I've seen used in gamedev, but gaps nonetheless.

Slow build times (on par with C) and the need to prove your code safe with clear ownership models can slow down iteration times. For quick and dirty prototyping, gameplay, or UI code, you might consider using scripting, declarative, and/or garbage collected languages instead.

Reading List

The Rust Programming Language – Rust 101
The Rustonomicon – Rust for the person who no longer values their sanity.
Unsafe Code Guidelines – Official recommendations and soft (subject-to-change) guarantees.
What Not to Do in Rust – Touches on someone's mindset shifts when going from C++ -> Rust
kyren from Chucklefish in /r/rust – Talks about their experiences with rust gamedev
Notes on Type Layouts and ABIs in Rust
Rust Case Study: Chucklefish Taps Rust to Bring Safe Concurrency to Video Games (NOTE: Chucklefish stopped using Rust :()
I Made a Game in Rust — Android/iOS "A Snake's Tale" Dev's experience with Rust in 2017

Watching List

Using Rust For Game Development – RustConf 2018 Closing KeyNote

Common Errors

LNK1112: module machine type 'x86' conflicts with target machine type 'x64'

Developer Command Prompt for VS 2017 adds ...\Hostx86\x86\link.exe (targeting 32-bit) to %PATH%, but cargo build is building 64-bit by default. You have two good options to fix this:

If you must use a manually configured shell/environment (because, say, you're using not yet supported VS previews), note that your target and host architectures should be kept the same, or any build.rs scripts will fail to link, as a single linker from %PATH% will be used. Then you have a few options:

Type Equivalents

See also Notes on Type Layouts and ABIs in Rust.

This is an intentionally incomplete list with exemplar types and some overlap. Also note that for FFI, the Rust types sometimes impose additional restrictions over the equivalent C types. Bool must be 0/1 (not merely truthy), non-listed enum values have undefined behavior (maybe use a u?? instead at the FFI layer), etc.

Rust C++
boolbool, assuming sizeof(bool)==1
charchar32_t, "unicode scalar value"
u8char as used for typical byte buffers
u8, u16, u32, u64, u128 uint8_t, uint16_t, uint32_t, uint64_t, __uint128 [bug!]
i8, i16, i32, i64, i128 int8_t, int16_t, int32_t, int64_t, __int128 [bug!]
f32, f64 float, double, on most implementations
usize, isize uintptr_t (~size_t), intptr_t (~ptrdiff_t),
() – read as "unit"void
! – read as "never"[[noreturn]] void ?
&Struct – "(shareable) reference"const Struct &
&DynamicallySizedTypestd::tuple<const DynamicallySizedType *, size_t>
&dyn Traitstd::tuple<const TraitVtable *, const Struct &>
&Trait – Pre 1.27 versionstd::tuple<const TraitVtable *, const Struct &>
&mut T – read as "mutable reference"Like !mut, but exclusive access and writable
 
std::cell::Cell<T>mutable keyword + compile time checking
std::cell::RefCell<T>mutable keyword + run time checking
 
std::boxed::Box<T>unique_ptr<const T>, !null, value semantics
std::rc::Rc<T>shared_ptr<const T>, !null, single threaded, value semantics
std::rc::Weak<T>weak_ptr<const T>, single threaded
std::sync::Arc<T>shared_ptr<const T>, !null, atomic refcount, value semantics
std::sync::Weak<T>weak_ptr<const T>, atomic refcounts
std::option::Option<Box<T>>unique_ptr<const T>
std::option::Option<Arc<T>>shared_ptr<const T>
Option<Box<Cell<T>>>unique_ptr<T>
Option<Arc<Cell<T>>>shared_ptr<T>
Weak<Cell<T>>weak_ptr<T> – Weak is nullable, so no Option necessary
RefCell<Weak<Self>> [example]enable_shared_from_this
 
std::sync::atomic::AtomicBoolstd::atomic<bool>
std::sync::atomic::AtomicUsizestd::atomic<size_t>
std::sync::Mutex<Struct>std::atomic<Struct>, note C++ may not be atomic!
std::sync::Mutex<Struct>std::mutex m; Struct s GUARDED_BY(m);
 
std::string::Stringstd::string, except guaranteed UTF8, no '\0'
&strstd::string_view, except guaranteed UTF8
std::ffi::CStringstd::string
std::ffi::CStrstd::string_view
std::ffi::OsString~std::wstring on windows, but converted to WTF8(!)
std::path::PathBuf~std::wstring on windows (just wraps OsString)
std::ffi::OsStr~std::wstring_view, but wchar_t* isn't WTF8
std::path::Path~std::wstring_view (just wraps OsStr)
 
std::vec::Vec<T>std::vector<T>
[T; 12]std::fixed_array<T, 12> , T[12]
&[T]std::array_view<T> , gsl::span<T>
std::collections::VecDeque<T>std::deque<T>
std::collections::LinkedList<T>std::list<T>
std::collections::BTreeSet<T>std::set<T>
std::collections::BTreeMap<K, V>std::map<K, V>
std::collections::HashSet<T>std::unordered_set<T>
std::collections::HashMap<K, V>std::unordered_map<K, V>
 
(A, B)std::tuple<A, B>
struct T(A, B);struct T : std::tuple<A, B> {};
struct T { a: A, b: B, }struct T { A a; B b; };
enum typesstd::variant + names and language support
enum typesC++ enums (although use #[repr(C)] for FFI)
type Foo = u64;typedef uint64_t Foo;
 
Exact Rust FFI TypeC++
()void (return type)
![[noreturn]] void (return type) ?
std::os::raw::c_void*void*
...::raw::c_float, c_double float, double
...::raw::c_char, c_schar, c_uchar char, signed char, unsigned char
...::raw::c_ushort, c_short unsigned short, short
...::raw::c_uint, c_int unsigned int, int
...::raw::c_ulong, c_long unsigned long, long
...::raw::c_ulonglong, c_longlong unsigned long long, long long

Syntax Equivalents

There are several unlisted differences in the exact behavior of C++ vs Rust code bellow - type inference being more powerful and extending beyond the current statement in Rust for example.

Rust C++
assert!(condition); assert(condition);
print!("Hello {}\n", world); printf("Hello %s\n", world);
 
if a == b { ... } if (a == b) { ... }
else if a == b { ... } else if (a == b) { ... }
else { ... } else { ... }
 
let foo = 42; const auto foo = 42;
let foo = 42.0f32; const auto foo = 42.0f;
let foo = 42u32; const auto foo = 42u;
let foo = 42u64; const auto foo = 42ull;
let mut foo = 42; auto foo = 42;
let mut foo : i32 = 42; int32_t foo = 42;
let mut (x,y) = some_tuple; float x,y; std::tie(x,y) = some_tuple;
let some_tuple = (1,2); const auto some_tuple=std::make_tuple(1,2);
let a : [i32; 3] = [1, 2, 3]; int32_t a[3] = { 1, 2, 3 };
let v : Vec<i32> = vec![1, 2, 3]; std::vector<int32_t> v = { 1, 2, 3 };
 
for i in v { ... } for (auto i : v) { ... }
for i in 1..9 { ... } for (size_t i = 1; i < 9; ++i) { ... }
for i in 1..=9 { ... } for (size_t i = 1; i <= 9; ++i) { ... }
while i < 9 { ... } while (i < 9) { ... }
 
loop { while (true) {
    break;     break;
    continue;     continue;
} }
'label: loop { while (true) {
    break 'label;     goto label_break;
    continue 'label;     goto label_continue;
} label_continue: } label_break:
 
other_var as f32 (float)other_var
 
match var { ... } switch (var) { ... }
42 => foo(), case 42: foo(); break;
42 => { foo(); bar(); }, case 42: foo(); bar(); break;
_ => ..., default: ... break;
 
mod foo { ... } namespace foo { ... }
use foo as bar; using namespace bar = foo;
use foo::*; using namespace foo;
use foo::some_func; using foo::some_func;
 
/// Doc comment // \brief Doc comment
fn add (a: i32, b: i32) -> i32 { int32_t add (int32_t a, int32_t b) {
    return a + b;     return a + b;
    a + b // Final statement, no semicolon     return a + b; // A regular comment
} }

The Obvious

The Less Obvious

Interior Mutability

See also The Rust PL 2nd Ed. on Interior Mutability.

Like in C++, the constness of the outer object can affect access to it's members. For example, given const std::vector<int> & v, you cannot modify the elements of the vector with v[0] = 42; - this will fail to compile. Given a const gsl::span<int> & av, however, av[0] = 42; will compile just fine. Rust calls this second case "interior mutability".

Another example: In C++, given const some_struct & s, you cannot modify members with s.some_field = 42; unless some_field used the mutable keyword. Similarly, in Rust, given s : &SomeStruct, you cannot modify members with s.some_field = 42; unless some_field was wrapped in Cell or another type that provides "interior mutability".

Because immutability is tied to the ability to share a value in Rust, "interior mutability" is used in some places that may be suprising to a C++ programmer. For example, you can modify an "immutable" Rust std::sync::atomic::AtomicUsize, even though you wouldn't be able to modify a C++ const std::atomic<size_t>, because AtomicUsize has been given "interior mutability". Without interior mutability, it'd be impossible to share a AtomicUsize between multiple threads while allowing write access - defeating the entire purpouse of having atomic types in the first place.

Some of the types in Rust providing interior mutability - generally, you only need one at a time:

Rust Type Description
std::cell::Cell<T> Exists specifically to give you Interior Mutability for "Free". This type works by giving you methods like get(), and set(value), and enforcing at compile time that you never hold onto a &T or &mut T (unless you have an exclusive mutable reference to the whole Cell at once.) Cannot be shared between threads. T must be Copy able.
std::cell::RefCell<T> Exists specifically to give you Interior Mutability. Unlike Cell<T>, this allows you to get a &T or &mut T from an immutable instance of RefCell<T>, and instead enforces at run time that you'll never have a &mut T and any other references at the same time. Methods like borrow() can panic, but non-panicing options like try_borrow() are also available. Cannot be shared between threads.
std::cell::UnsafeCell<T> This exists, but you'll need to use unsafe code and raw pointers. Best of luck!
std::sync::Mutex<T> Similar to RefCell<T>, but thread safe, and so you lock() it instead of borrow()ing it. Doing so multiple times from the same thread is less specified: It may deadlock, or it may panic. It may also panic if another thread paniced while holding onto the mutex.
std::sync::RwLock<T> Similar to Mutex<T>, only allowing multiple readers simultaniously.
std::sync::AtomicUsize Atomic types also implement interior mutability so they can be shared between multiple threads.

Dynamically Sized Types

See also The Rustonomicon on Dynamically Sized Types.

In C++, all types have a single size, known at compile time. Structs with a final array of size 1 (or 0, if you're comfortable relying on compiler extensions) are often (ab)used to fake "Dynamically Sized Types" (or DSTs), where more memory is allocated than the C++ type alone needs, so the array can be (ab)used to index multiple elements. You would typically store the size in the type itself. For example:

struct Str {
    size_t length;
    char   characters[0];
};

static inline Str * create (const char * c_string) {
    size_t len = strlen(c_string);
    Str * s = (Str *)malloc(sizeof(Str) + len);
    memcpy(s->characters, c_string, len);
    return s;
}

In Rust, Dynamically Sized Types are natively supported by the language, although they have several limitations. We could write:

#[repr(C)]
struct MyStr {
    length:     usize, // You wouldn't actually do this - keep reading!
    characters: [u8],  // [u8] is also a DST - an array of an unknown number of elements.
                       // We can't use DSTs as fields, except as the last field like here.
};

References to DSTs (like &MyStr or &[u8]) in Rust are larger than regular references (like &NormalStruct or &i32). References to DSTs know the dynamic size of the type they're referencing, not just the address, so in Rust we typically wouldn't store the length inside the struct as well (it's redundant!). This also lets us do things like have multiple `&[u8]`s referencing different subsets of the same data.

The two most common DSTs you'll run into are str and [T], which is why you'll almost always seem them dealt with by reference (e.g. &str and &[T]) – you can't use them as standard values on the stack.

Virtual Dispatch

Rust structs don't contain vtable pointers. Instead, the reference or pointer to a trait object is a "fat pointer" - in other words, &dyn SomeTrait conceptually maps to two C++ pointers: One to the struct being passed in, and one to the trait vtable for that type.

Function Overloading

Rust doesn't have function overloading, but it has several tricks which can make it look like it does:

Are we MaulingMonkey yet?

Maybe for gamedev middleware or other greenfield projects.
No for agitating to replace existing C++ in gamedev codebases.
See Are we game yet? and Are we web yet? for more options.

Area Status Notes
HTTPS Good Rocket.rs is pretty neat and acceptable for the kinds of basic REST APIs I like to make. I haven't properly evaluated database and other web tech options for suitability for others however. AWWY?
ECS Good specs is neato and I should use it. No entity archtype optimizations yet, but I could always roll my own if I need that. See also the Using Rust For Game Development (RustConf 2018 Closing KeyNote) which talks about specs.
Debugging OK I've actually contributed patches to help improve things! Vec and other relatively basic types have .natvis visualizers which MSVC-linked rust executables will automatically embed into your .pdbs, allowing Visual Studio to debug your executables with ~0 configuration. Enums and more complicated types still need some work. Things like source code breakpoints work fine. The standard Visual Studio Code C/C++ Extension can also debug and launch Rust executables. GDB support also exists.
VS Code Good

Use ms-vscode.cpptools for debugging. I'm using rust-lang.rust for intellisense. template_rust_vscode has a superset of the .vscode build settings I copy into new projects.

I still need to give rust-analyzer a try, which is supposedly the newfangled, sponsored, hotness. I've also previously used kalitaalexey.vscode-rust, which worked OK at the time, although it could be tempermental to set up.

Consoles OK

From the Rust Chucklefish Whitepaper (emphasis mine):

"Even after taking into account the ten days needed to customize Rust for the Xbox, PS4, and Nintendo Switch consoles, Chucklefish saved time previously spent debugging cross-platform idiosyncrasies."
That's the platforms I care about, and in a reasonable timeframe. Ten days is nothing compared to the time I've sunk into improving interop and debugging for various scripting languages. I expect the C style FFI will work out of the box, as will the debugger (already works on Windows/Linux MSVC, GDB, LDB.) The rust stdlib on the other hand might be a different matter.
Graphics Workable

I've experimented with directly using D3D11 via winapi-rs. Using the bindings straight up from unsafe code is actually quite a reasonable experience, not much worse than using D3D11 in C++. If you use D3DCompile instead of fxc.exe, you probably don't even need to mess with getting the right SDK installed and paths configured like you must with C++! Wrapping it in safe rust code is more involved, but I think mostly due to my inexperience with wrapping COM in Rust. The code – once written – doesn't appear to be much if any worse than wrapping D3D11 in C++, although I need more (hygenic) macros in Rust due to the lack of winapi-rs traits for IUnknown and the like.

Are we game yet? has plenty of 3D and 2D rendering APIs, not to mention various "engines" with their own graphics abstractions. I need to give some of these a shot. They seem reasonable, even if not a single one of them is willing to commit to a v1 yet.

I forked the abandoned bgfx-rs to try and modernize it, but was quickly put off - by C++ build chain fustrations, nothing to do with Rust!

C++ FFI Workable

We've got several options.

Manual C ABI FFI. Fundamentally lame, but Rust's version is as workable as any other I've encountered. For fancy stuff like COM interop, beware of unslain dragons.

cbindgen (export Rust to C/C++): Lets you generate C/C++ headers for whatever C style ABI you expose from rust. C style ABIs only, with many unhandled edge cases (generates unusable headers for tuples, won't export constants using type aliases, etc.), but usable. Can be intergrated into your build.rs scripts with relative ease. You'll likely need to hand-write some interop types for strings, references, slices, etc.

rust-ffi (export Rust to C/C++): Same author as cbindgen, maybe better (looks to handle more edge cases). I need to try it out some.

bindgen (export C/C++ to Rust): Used by bgfx-rs. Relatively limited when it comes to C++, but usable.

Passing Rust's unterminated string slices to pure C interfaces adds friction/allocation (well discussed by imgui-rs#7), although it's not a problem if you can pass ptr+length and simply convert to std::string_view s or similar on the C/C++ side of things. Similarly, Rust's insistence that &str must be a known-length UTF8 slice for defined behavior adds error checking/friction when passing strings back to Rust, although the standard library has * const c_char and CStr which you may be able to use instead. The Unsafe Code Guidelines actually standardize the layout of &[T] (and thus by extension &str), so you can write ABI compatible types in C++ – with the appropriate conversion operators for implicit conversion to/from, say, std::string_View, std::span, gsl::span, or your own custom slice types.

Finally, it's worth mentioning that there's several crates that already provide FFI bindings, meaning you likely don't have to write your own.

Visual Studio Workable

For standalone rust projects, I'd recommend just using VS Code. However, limited VS integration is possible for those of you with large existing VS based C++ toolchains they (understandably) are beholden to.

I'd recommend rls-vs2017 for intellisense, and a vanilla C++ Makefile project wrapping cargo build commands (letting you build with no extra dependencies other than installing rust via rustup, simplifying your CI server install.) rls-vs2017 leverages VS2017's built-in Language Server Protocol support, and seems actively maintained.

Previously, I forked the abandoned Visual Rust to try and make it usable, got partway there, then gave up. It has bits that might be worth salvaging for rls-vs2017 but it simply does not scale well, with intellisense queries blocking the main thread while racer desparately tries to compile enough Rust to get back to your query.

WASM Workable

The built-in cargo wasm32-unknown-unknown target on it's own is pretty bloated (0.9MB for trivial demos!) due to have a self contained stdlib implementation, allocators and all. Raw JS <-> WASM interop is extremely limited as well, and annoying to work with. Finally, the current linker doesn't support outputing sourcemaps, so despite FireFox supporting wasm sourcemaps, you'll be looking at raw WASM instead while debugging.

wasm32-unknown-emscripten supposedly has sourcemaps, but the several global dependencies (python, emscripten, env setup) make this annoying to use.

wasm-bindgen fixes a lot - trimming wasm binaries to tens of kilobytes, allowing a lot more expressive interop, providing web-sys and js-sys crates adapting common web APIs to Rust so you don't need to, all without needing node.

cargo-web also provides webserver support, so you can test over localhost http instead of being limited by Chrome's file protocol CORS limitations - just replace cargo build ... with cargo web start ...!

Once wasm32-unknown-unknown has sourcemaps (or other browser supported debug info), I'll bump this all the way up to good.

Crates of Note

Category Crate Stars Commits Channel License Graphics Links
Engine amethyst Crates.io Github Commits stable License Rust: gfx-hal www docs
ggez Crates.io Github Commits stable License Rust: gfx2 www docs
piston Crates.io Github Commits nightly License Rust: * www docs
Wrappers gfx-hal Crates.io Github Commits stable License C++: * www docs
winit Crates.io Github Commits stable License docs
wio Crates.io Github Commits stable License docs
Raw FFI winapi Crates.io Github Commits stable License C++: D3D docs
gl Crates.io Github Commits stable License C: OpenGL docs
com-impl Crates.io Github Commits nightly License docs
WASM wasm-bindgen Crates.io Github Commits stable License guide
web-sys Crates.io same repo stable License JS: * docs
js-sys Crates.io same repo stable License docs
wasm-pack Crates.io Github Commits stable License docs
stdweb Crates.io Github Commits stable License JS: * docs
Utility lazy_static Crates.io Github Commits stable License docs
microprofile-rust Crates.io Github Commits stable License docs
rocket Crates.io Github Commits nightly License guide docs

— MaulingMonkey