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++.
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 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.
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.
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:
Just use a regular cmd.exe instance. This will cause rustc to find an architecture-appropriate link.exe itself, instead of relying on %PATH%.
Unset %VCINSTALLDIR%. This will also cause rustc to find an architecture-appropriate link.exe itself, instead of relying on %PATH%, even if link.exe can still be found in %PATH%.
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:
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++ |
---|---|
bool td> | ≈ bool , assuming sizeof(bool)==1 |
abibool::* | other boolean types |
char td> | ≈ char32_t + unicode scalar value guarantee |
u8 td> | char as used for typical byte buffers |
u8 ,
u16 ,
u32 ,
u64
|
uint8_t ,
uint16_t ,
uint32_t ,
uint64_t
|
i8 ,
i16 ,
i32 ,
i64
|
int8_t ,
int16_t ,
int32_t ,
int64_t
|
u128 ,
i128
|
__uint128 ,
__int128 ,
but with different alignment
|
f32 ,
f64
|
float ,
double , on most implementations
|
usize ,
isize
(pointer sized)
|
uintptr_t ,
intptr_t
(pointer sized)
|
usize ,
isize
(pointer sized)
|
≈
size_t ,
ptrdiff_t
(could be smaller)
|
() – read as "unit" td> | void |
! – read as "never" td> | [[noreturn]] void ? |
&Struct – "(shareable) reference" td> | const Struct & |
&DynamicallySizedType td> | |
&dyn Trait td> | std::tuple<const TraitVtable *, const Struct &> |
&Trait – Pre 1.27 version td> | std::tuple<const TraitVtable *, const Struct &> |
&mut T – read as "mutable reference" td> | Like !mut, but exclusive access and writable |
std::option::Option<T> td> | std::optional<T> (C++17) |
std::result::Result<T, E> td> | std::expected<T, E> (C++23) |
std::result::Result<_, E>::Err | std::unexpected<E> (C++23) |
std::result::Result<T, _>::Ok | T (implicitly converts to std::expected<T, _> ) |
std::boxed::Box<T> td> | unique_ptr<const T> , !null, value semantics |
std::rc::Rc<T> td> | shared_ptr<const T> , !null, single threaded, value semantics |
std::rc::Weak<T> td> | weak_ptr<const T> , single threaded |
std::sync::Arc<T> td> | shared_ptr<const T> , !null, atomic refcount, value semantics |
std::sync::Weak<T> td> | weak_ptr<const T> , atomic refcounts |
std::option::Option<Box<T>> td> | unique_ptr<const T> |
std::option::Option<Arc<T>> td> | shared_ptr<const T> |
Option<Box<UnsafeCell<T>>> td> | unique_ptr<T> |
Option<Arc<UnsafeCell<T>>> td> | shared_ptr<T> |
Weak<UnsafeCell<T>> td> | weak_ptr<T> – Weak is nullable, so no Option necessary |
RefCell<Weak<Self>> [example] | std::enable_shared_from_this |
std::cell::UnsafeCell<T> | mutable specifier (unchecked access, avoid) |
std::cell::Cell<T> td> | mutable specifier + value/copy semantics |
std::cell::RefCell<T> td> | mutable specifier + run time checked borrows |
std::sync::RwLock<T> td> | mutable specifier + thread safe checked borrows |
std::sync::Mutex<T> td> | mutable specifier + thread safe checked borrows (exclusive) |
std::sync::atomic::AtomicBool td> | std::atomic<bool> |
std::sync::atomic::AtomicUsize td> | std::atomic<size_t> |
std::sync::Mutex<Struct> td> | std::atomic<Struct> , note C++ may not be atomic! |
std::sync::Mutex<Struct> td> | std::mutex m; Struct s GUARDED_BY(m); |
std::string::String td> | std::string , except guaranteed UTF8, no '\0' |
&str td> | std::string_view , except guaranteed UTF8 |
std::ffi::CString td> | std::string |
std::ffi::CStr td> | std::string_view |
std::ffi::OsString td> | ≈std::wstring on windows, but converted to WTF8(!) |
std::path::PathBuf td> | ≈std::wstring on windows (just wraps OsString) |
std::ffi::OsStr td> | ≈std::wstring_view , but wchar_t* isn't WTF8 |
std::path::Path td> | ≈std::wstring_view (just wraps OsStr) |
Rust Container | C++ Container |
std::vec::Vec<T> td> | std::vector<T> |
[T; 12] td> | std::array<T, 12> (C++11), T[12] |
&[T] td> | std::span<T> (C++20) |
std::collections::VecDeque<T> td> | std::deque<T> |
std::collections::LinkedList<T> td> | std::list<T> |
std::collections::BTreeSet<T> td> | std::set<T> |
std::collections::BTreeMap<K, V> td> | std::map<K, V> |
std::collections::HashSet<T> td> | std::unordered_set<T> (C++11) |
std::collections::HashMap<K, V> td> | std::unordered_map<K, V> (C++11) |
std::collections::BinaryHeap<T> td> | std::priority_queue<T> |
(A, B) td> | std::tuple<A, B> |
struct T(A, B); td> | struct T : std::tuple<A, B> {}; |
struct T { a: A, b: B, } td> | struct T { A a; B b; }; |
type Foo = u64; td> | typedef uint64_t Foo; |
enum types td> | std::variant + names and language support |
#[repr(u32)] enum E { A, B, C } | enum class E : uint32_t { A, B, C }; [not ffi safe] |
bitflags! { struct Flags ... } td> | enum class Flags { A=0x1, B=0x2, C=0x4 }; [not ffi safe] |
#[repr(transparent)] | // Required for FFI safety |
struct E(pub u32); impl E { | enum class E : uint32_t { |
const A : E = E(0); | A = 0, |
} | }; |
Exact Rust FFI Type | C++ |
() | void (return type) |
! td> | [[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
|
There are several unlisted differences in the exact behavior of C++ vs Rust code below - type inference being more powerful and extending beyond the current statement in Rust for example - and I'm assuming the size of unsigned and unsigned long long as another.
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 |
} |
} |
trait Interface { |
struct Interface { |
fn foo(&self, _: u32) -> u64; |
virtual uint64_t foo(uint32_t) = 0; |
fn bar(&self, param: u32) -> u64 { |
virtual uint64_t bar(uint32_t param) { |
self.foo(param) |
return this->foo(param); |
} |
} |
} |
} |
Result<T, Err>
s and the like over objects with invalid, error, or null states.Box<T>
, Arc<T>
, Rc<T>
,
&[T]
, or &T
with ==
will compare the T
values not the addresses. If you must compare addresses,
std::ptr::eq
, or ==
on raw pointers like *const T
, will compare T
addresses.42.wrapping_add(1)
and similar can be used to explicitly wrap and never panic.
In C++, several of these operations – especially on signed integers – would be undefined behavior.struct T {}
, sizeof(T) is 1 in C++, but mem::size_of::<T>() is 0 in Rust. No need for EBCO!unsafe { ... }
blocks (or someone
else doing so and exposing an unsound API). Unfortunately, within an unsafe
block, it's
incredibly easy to run afoul of any of the following edge cases:
&mut T
s to the same instance is undefined behavior.&mut T
and &T
s to the same instance is undefined behavior.&mut T
permanently invalidates all existing overlapping *T
s to the same instance.unsafe
if you can avoid it, or failing that, consider using
UnsafeCell, which is
slightly safer.
#[repr(Rust)]
can reorder struct fields. Use #[repr(C)]
for FFI.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. |
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.
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.
Rust doesn't have function overloading, but it has several tricks which can make it look like it does:
AsRef
,
Into
,
IntoIterator
println!
, print!
, format_args!
, etc. in the stdlib.some_function(SomeStruct { field: 42, ..Default::default() })
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 .pdb s, 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 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
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 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 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 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
Once |
Category | Crate | Stars | Commits | Channel | License | Graphics | Audio | Links | |
---|---|---|---|---|---|---|---|---|---|
Engine | amethyst | www docs | |||||||
ggez | www docs | ||||||||
piston | www docs | ||||||||
Wrappers | gfx-hal | www docs | |||||||
winit | docs | ||||||||
wio | docs | ||||||||
rodio | docs | ||||||||
cpal | docs | ||||||||
deprecated | gfx | docs | |||||||
Raw FFI | winapi | docs | |||||||
gl | docs | ||||||||
com-impl | docs | ||||||||
FFI Gen | cbindgen | docs | |||||||
bindgen | docs | ||||||||
rust-ffi | blog post | ||||||||
WASM | wasm-bindgen | guide | |||||||
web-sys | same repo | docs | |||||||
js-sys | same repo | docs | |||||||
wasm-pack | docs | ||||||||
stdweb | docs | ||||||||
cargo-web | docs | ||||||||
Utility | lazy_static | docs | |||||||
microprofile-rust | docs | ||||||||
rocket | guide docs | ||||||||
Unsorted | serde | ||||||||
json | |||||||||
quickcheck | |||||||||
arrayvec | |||||||||
smallvec | |||||||||
cargo-apk | |||||||||
cargo-ndk | |||||||||
cargo-dinghy | |||||||||
rand | |||||||||
rdrand | |||||||||
fs2 | |||||||||
cargo | |||||||||
skeptic | |||||||||
beryllium | |||||||||
quicksilver | |||||||||
tempfile | |||||||||
warmy | |||||||||
dynamic_reload | |||||||||
resource |
Key | Description |
---|---|
Channel | |
This crate can be used from stable rust. | |
This crate requires nightly rust. | |
Graphics | |
This crate wraps gfx-hal , which is writen in Rust | |
This crate wraps gfx (deprecated) | |
This crate has several custom backends for it's graphics | |
This crate wraps multiple C or C++ system APIs (e.g. OpenGL, D3D, Vulkan, ???), probably via winapi and similar. |
|
This crate provides D3D FFI bindings | |
This crate provides OpenGL FFI bindings | |
This crate provides WebGL / Canvas bindings | |
This crate wraps a non-system C or C++ library for providing graphics. | |
Audio | |
This crate wraps rodio | |
This crate wraps cpal | |
This crate provides Web Audio bindings, <audio> , etc. | |
This crate provides XAudio, WinMM, or other system FFI bindings. | |
This crate wraps SDL2 |
— MaulingMonkey