Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UnsafeCell access APIs #521

Closed
pitaj opened this issue Jan 18, 2025 · 4 comments
Closed

UnsafeCell access APIs #521

pitaj opened this issue Jan 18, 2025 · 4 comments
Labels
ACP-accepted API Change Proposal is accepted (seconded with no objections) api-change-proposal A proposal to add or alter unstable APIs in the standard libraries T-libs-api

Comments

@pitaj
Copy link

pitaj commented Jan 18, 2025

Proposal

Problem statement

In the wild, the following patterns are pretty common:

// Setting value inside `UnsafeCell`
unsafe { *my_unsafe_cell.get() = new_value; }
// Getting reference to value inside `UnsafeCell`
let value = unsafe { &*my_unsafe_cell.get(); }

These operation involve raw pointer dereferences. Many users of Rust prefer to avoid raw pointers if at all possible, do to the numerous invariants that must be upheld to avoid UB (non-null, aligned, plus initialized in the case of assignment).

If you already have &UnsafeCell, why should you be required to go through a raw pointer just to assign to or take a reference to the value within?

Motivating examples or use cases

Consider this example from once_cell, which splits the .get() from the assignment:

    /// Safety: synchronizes with store to value via SeqCst read from state,
    /// writes value only once because we never get to INCOMPLETE state after a
    /// successful write.
    #[cold]
    pub(crate) fn initialize<F, E>(&self, f: F) -> Result<(), E>
    where
        F: FnOnce() -> Result<T, E>,
    {
        let mut f = Some(f);
        let mut res: Result<(), E> = Ok(());
        let slot: *mut Option<T> = self.value.get();
        initialize_or_wait(
            &self.queue,
            Some(&mut || {
                let f = unsafe { f.take().unwrap_unchecked() };
                match f() {
                    Ok(value) => {
                        unsafe { *slot = Some(value) };
                        true
                    }
                    Err(err) => {
                        res = Err(err);
                        false
                    }
                }
            }),
        );
        res
    }

If you were to audit the lower unsafe block in this example:

unsafe { *slot = Some(value) };

The safety comment on the function itself handles the synchronization, but you would have to look at context to see that it is derived from the UnsafeCell, and therefore fulfills the following invariants:

  • non-null
  • aligned
  • value within is initialized

If safety comments were required, you might see a comment like this:

// SAFETY: Pointer is valid and old value is initialized, because it comes from the `UnsafeCell` above.

Which would be unnecessary if just an &UnsafeCell reference could be used instead.

Solution sketch

impl<T> UnsafeCell<T> {
    /// Replace the value in this `UnsafeCell` and return the old value.
    ///
    /// # SAFETY
    /// - Allowing calls to race with any other access
    ///   to the wrapped value is Undefined Behavior.
    /// - Calling this while any other references to the
    ///   wrapped value are alive is Undefined Behavior.
    unsafe fn replace(&self, new_value: T) -> T;
    /// Get a shared reference to the wrapped value.
    ///
    /// # SAFETY
    /// - Calling this while any mutable references to the
    ///   wrapped value are alive is Undefined Behavior.
    /// - Mutating the wrapped value while the returned
    ///   reference is alive is Undefined Behavior.
    unsafe fn get_ref(&self) -> &T;
}

The above added APIs allow us to rewrite the once_cell code like this:

    pub(crate) fn initialize<F, E>(&self, f: F) -> Result<(), E>
    where
        F: FnOnce() -> Result<T, E>,
    {
        let mut f = Some(f);
        let mut res: Result<(), E> = Ok(());
        let slot: &UnsafeCell<Option<T>> = &self.value; // Just a reference
        initialize_or_wait(
            &self.queue,
            Some(&mut || {
                let f = unsafe { f.take().unwrap_unchecked() };
                match f() {
                    Ok(value) => {
                        unsafe { slot.replace(Some(value)) }; // Just an unsafe function call
                        true
                    }
                    Err(err) => {
                        res = Err(err);
                        false
                    }
                }
            }),
        );
        res
    }

And more generally, users can use references instead of raw pointers in more places:

  • &UnsafeCell<T> does not currently have a pointer equivalent
  • &UnsafeCell<MaybeUninit<T>> roughly equivalent to NonNull<T>
  • Option<&UnsafeCell<MaybeUninit<T>>> roughly equivalent to *mut T

Alternatives

Do nothing and people continue to just use the raw pointer operations.

Links and related work

What happens now?

This issue contains an API change proposal (or ACP) and is part of the libs-api team feature lifecycle. Once this issue is filed, the libs-api team will review open proposals as capability becomes available. Current response times do not have a clear estimate, but may be up to several months.

Possible responses

The libs team may respond in various different ways. First, the team will consider the problem (this doesn't require any concrete solution or alternatives to have been proposed):

  • We think this problem seems worth solving, and the standard library might be the right place to solve it.
  • We think that this probably doesn't belong in the standard library.

Second, if there's a concrete solution:

  • We think this specific solution looks roughly right, approved, you or someone else should implement this. (Further review will still happen on the subsequent implementation PR.)
  • We're not sure this is the right solution, and the alternatives or other materials don't give us enough information to be sure about that. Here are some questions we have that aren't answered, or rough ideas about alternatives we'd want to see discussed.
@pitaj pitaj added api-change-proposal A proposal to add or alter unstable APIs in the standard libraries T-libs-api labels Jan 18, 2025
@Amanieu
Copy link
Member

Amanieu commented Jan 21, 2025

We discussed this in the libs-api meeting. We're generally happy to add convenience methods to UnsafeCell, however we felt that it would be better to also have a way to get a mutable reference to the inside of the cell (UnsafeCell::get_mut is safe and requires &mut self).

As such we're accepting this ACP with a slight change:

impl<T> UnsafeCell<T> {
    /// Replace the value in this `UnsafeCell` and return the old value.
    unsafe fn replace(&self, new_value: T) -> T;
    /// Get a shared reference to the wrapped value.
    unsafe fn get_ref_unchecked(&self) -> &T;
    /// Get a mutable reference to the wrapped value.
    unsafe fn get_mut_unchecked(&self) -> &mut T;
}

@Amanieu Amanieu closed this as completed Jan 21, 2025
@Amanieu Amanieu added the ACP-accepted API Change Proposal is accepted (seconded with no objections) label Jan 21, 2025
@programmerjake

This comment has been minimized.

@Amanieu

This comment has been minimized.

@ibraheemdev
Copy link
Member

Should these be named as_ref_unchecked and as_mut_unchecked per rust-lang/rust#122034?

jhpratt added a commit to jhpratt/rust that referenced this issue Feb 4, 2025
add UnsafeCell direct access APIs

- Implementation for ACP: rust-lang/libs-team#521
- Tracking issue rust-lang#136327
rust-timer added a commit to rust-lang-ci/rust that referenced this issue Feb 4, 2025
Rollup merge of rust-lang#136398 - pitaj:unsafecell_access, r=dtolnay

add UnsafeCell direct access APIs

- Implementation for ACP: rust-lang/libs-team#521
- Tracking issue rust-lang#136327
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
ACP-accepted API Change Proposal is accepted (seconded with no objections) api-change-proposal A proposal to add or alter unstable APIs in the standard libraries T-libs-api
Projects
None yet
Development

No branches or pull requests

4 participants