Wrapper for unsafe multiple mutable reference

I am not good at English. Sorry if there are any funny expressions.


I have almost always failed with unsafe.
I'm sure I'm being stupid again. :blush:
So, please teach me loophole I am missing.

Background

Safe Rust does not allow multiple mutable references to same data.
I believe I also understand that this is important for our safety.

My idea (Probably stupid or pre-existing)

Today, I just had a thought. The reason why multiple mutable references are not allowed is to prevent conflicts between them. If so, how about creating a wrapper type for those mutable references and that guarantees their exclusion?

For example, like MutPair below. This type ensures the mutual exclusion of major and minor. However, if something like this were possible, someone would have already done it. In other words, there must be an unsafe escape route somewhere.

pub struct MutPair<'a, T, U> {
    mode: bool,
    major: &'a mut T,
    minor: U,
}

impl<'a, T, U> MutPair<'a, T, U> {
    pub fn new<F>(major: &'a mut T, f: F) -> Self
    where
        F: FnOnce(&'a mut T) -> U,
    {
        let mode = false;
        let dup = unsafe { &mut *(major as *mut _) };
        let minor = f(dup);
        Self { mode, major, minor }
    }

    pub fn major(&mut self) -> &mut T {
        assert!(self.mode);
        self.major
    }

    pub fn minor(&mut self) -> &mut U {
        assert!(!self.mode);
        &mut self.minor
    }

    pub fn mode_major(&mut self) {
        self.mode = true;
    }

    pub fn mode_minor(&mut self) {
        self.mode = false;
    }
}

Use case?

Probably wasteful, but I created a use case. Vector-like type SparseVec does not use memory if the element values are zero. When the mutable iterator of none_zeros_mut sets elements to zero, it removes those entries at the timing when the iterator is dropped.

The moment you create a second exclusive (i.e. mutable) reference to the data, the first one is invalidated, and compiler as allowed to optimize based on that. So no, this will not work.

4 Likes

The moment you create a second exclusive (i.e. mutable) reference to the data

This is following line?

let dup = unsafe { &mut *(major as *mut _) };

the first one is invalidated

This is major.

In summary, the compiler optimizes access to major after the unsafe timing, correct? It would be helpful if you explain specifically what kind of optimization leads to failure.

I apologize for my lack of understanding.

Someone can probably make an example.

But even if no one makes an example, it is still undefined behavior (still not okay). The language says two &mut to the same place, which you can use at the same time, is undefined behavior.

It's a fundamental part of the language.

Raw pointers (*mut, *const) can often be used instead.

1 Like

I'm starting to understand. So, regardless of how I access it, having two mutable references is already a violation of the rules.

Is it safe to use pointers to make MutPair safe?

It’s irrelevant. You are invoking UB so compiler is free to do anything.

TBF, I believe let dup = unsafe { &mut *(major as *mut _) }; is a reborrow, so dup does not immediately invalidate major. But using major afterwards will invalidate dup.

2 Likes

@zirconium-n Thank you. I understand that it's UB when there are multiple mutable references.


...Understanding that, I am thinking of other ways to solve this problem. How about using RefCell?

Should I just give up?

Raw pointer usage to create multiple borrows looks like this:

  1. Start with some reference r: &mut T.
  2. Convert it to a raw pointer p: *mut T.
  3. Derive whatever other pointers you need from p β€” but while doing that, you must not use r at all, nor convert p into a reference.
  4. Use those other pointers however you like, as long as their uses do not alias with each other. Convert them to &mut, even, as long as those uses do not alias.

This does not work for your API as written for two reasons:

  • f takes a reference, not a raw pointer, so the derivation doesn't meet the condition I stated.
  • minor does alias with major, because major isn't a disjoint part of T.

The minimum changes to make it valid would be, I think:

  • Change the field major to be a raw pointer, not a reference.
  • Remove mode_major() and mode_minor(); the β€œmode” must be always minor.
  • You may access *major only after minor is dropped.

This is very similar to the functionality yoke offers (and ouroboros is similar). You should see if you can implement the thing you want using yoke.

3 Likes

@kpreid Thank you for your reply.

Oh, I see, I understand what needs to be changed. It's cool to get rid of the mode and make it one-way.

I didn't know about yoke (even though it has a huge number of downloads). I'll look into it.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.