Skip to content

Instantly share code, notes, and snippets.

@ErnWong
Last active January 17, 2023 08:24
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ErnWong/f5c1e915a4defd32e3cdeb5693ab8062 to your computer and use it in GitHub Desktop.
Save ErnWong/f5c1e915a4defd32e3cdeb5693ab8062 to your computer and use it in GitHub Desktop.

Awesome List of Rust Footguns

This will be a living document where I will ocassionally add new "gotchas" I discover with Rust. Do note that I am still learning Rust. I mean, who isn't?

Drops are not guaranteed

8 May 2021.

Dear diary,

I must never rely on Drop in uncontrolled code to keep our datastructures in a consistent state.

This is because std::mem::forget is safe and it is easy to leak memory using Rc cycles (and probably through many other non-trivial ways).

See http://cglab.ca/~abeinges/blah/everyone-poops/

Truly a Leakpocalypse,
Ernest

Interface compatibility may not be obvious due to the drop checker

8 May 2021.

Dear diary,

Generic types that implement Drop has an additional lifetime requirement that all generic parameters must strictly outlive the type. However, there is an escape hatch where this check is skipped, and future versions of Rust seem to want to perform this check automatically, where this stricter lifetime requirement is applied whenever the references are observed during the Drop. This means that the compatibility of an interface might be tied to the implementation of the type, not just the interface alone.

See https://doc.rust-lang.org/nomicon/dropck.html

More info... but these lists don't mention Drop??

Update (8 May) - This is noted in rust-lang/cargo#8736. Good, I can rest well now.

In the name of SemVer,
Ernest

Reborrowing APIs for subfields allows assignment, not just internal mutation

9 May 2011.

Dear diary,

One cool thing ability of mutable references is that it allows you to assign a different value to that target memory location, referred to as a "direct mutation to the [memory] path" in Niko's talk on Polonius. This is useful in allowing, for example, elements in a Vec to be replaced in-place.

However, sometimes I have a struct containing a field that has a complicated interface, and I want to expose that interface to the user of the struct. It is tempting to do this:

pub struct Inner {
  x: i32,
}

impl Inner {
  pub fn new() -> Self {
    Inner { x: 0 }
  }
  pub fn some_mut_method(&mut self) {
    self.x += 1;
  }
}

pub struct Outer {
  inner: Inner,
}

impl Outer {
  pub fn inner(&mut self) -> &mut Inner {
    &mut self.inner
  }
}

so that you can then call outer.inner().some_mut_method(). However, this allows outside code to also call *outer.inner() = new Inner(), which may not be what you want to allow as it may mess up the consistency of Outer's state.

To prevent this, either

  1. Re-expose the Inner's APIs onto Outer and get rid of the reborrowing method that returns &mut Inner.
  2. Make Inner::new private and make sure it's not possible to get an instance of Inner anywhere.
  3. (My personal favourite solution) Wrap the &mut Inner in a new-type:
struct Inner {
  x: i32,
}

impl Inner {
  pub fn new() -> Self {
    Inner { x: 0 }
  }
  pub fn some_mut_method(&mut self) {
    self.x += 1;
  }
  pub fn some_method(&self) -> i32 {
    self.x + 2
  }
}

pub struct InnerRef<'a>(&'a Inner);
pub struct InnerRefMut<'a>(&'a mut Inner);

impl InnerRef<'_> {
  pub fn some_method(&self) -> i32 {
    self.0.some_method()
  }
}

impl InnerRefMut<'_> {
  pub fn some_method(&self) -> i32 {
    self.0.some_method()
  }
  pub fn some_mut_method(&mut self) -> i32 {
    self.0.some_mut_method()
  }
}

pub struct Outer {
  inner: Inner,
}

impl Outer {
  pub fn inner(&self) -> InnerRef {
    InnerRef(&self.inner)
  }
  pub fn inner_mut(&mut self) -> InnerRefMut {
    InnerRefMut(&mut self.inner)
  }
}

Paranoid lender,
Ernest

Non-terminating side-effect-free loops are undefined behaviour

16 May 2021.

Dear diary,

Today I learned that LLVM (which rust uses) makes infinite loops invoke undefined behaviour if the loop does not contain any side effect, allowing LLVM to fully remove the infinite loop and generate incorrect programs:

rust-lang/rust#28728

Turns out this is because C/C++ compilers like to optimise multiple terminating loops into a single loop, but it can only do that correctly if the comiler can prove that those loops will indeed terminate. But since it is nearly impossible for the compiler to prove that with the limited information it has, the compiler decides to assume that the loops will terminate instead.

http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1528.htm

(The exception is that in C, the loops are allowed to be bound by a constant expression condition)

Turns out non-termination should be treated like a side-effect.

Fortunately, this is solved very recently (March 5) rust-lang/rust#81451 Wohoo!

Non-terminating observer, Ernest

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment