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

Tracking Issue for mapped_lock_guards (MappedMutexGuard, MappedRwLockReadGuard, MappedRwLockWriteGuard) #117108

Open
2 of 4 tasks
zachs18 opened this issue Oct 23, 2023 · 7 comments
Labels
C-tracking-issue Category: An issue tracking the progress of sth. like the implementation of an RFC T-libs-api Relevant to the library API team, which will review and decide on the PR/issue.

Comments

@zachs18
Copy link
Contributor

zachs18 commented Oct 23, 2023

Feature gate: #![feature(mapped_lock_guards)]

This is a tracking issue for MappedMutexGuard, MappedRwLockReadGuard, and MappedRwLockWriteGuard.

This adds types analogous to lock_api::MappedMutexGuard, MappedRwLockReadGuard, MappedRwLockWriteGuard) to std::sync, and methods MutexGuard::map and MutexGuard::try_map (same for `RwLock*Guard) to create them

Public API

// std::sync::mutex

pub struct MappedMutexGuard<'mutex, T: ?Sized>;

impl<'mutex, T: ?Sized> MutexGuard<'mutex, T> {
    pub fn map<U, F>(orig: Self, f: F) -> MappedMutexGuard<'mutex, U>
      where 
        F: FnOnce(&mut T) -> &mut U,
        U: ?Sized;
    pub fn try_map<U, F>(orig: Self, f: F) -> Result<MappedMutexGuard<'mutex, U>, Self>
      where 
        F: FnOnce(&mut T) -> Option<&mut U>,
        U: ?Sized;
}

impl<'mutex, T: ?Sized> MappedMutexGuard<'mutex, T> {
    pub fn map<U, F>(orig: Self, f: F) -> MappedMutexGuard<'mutex, U>
      where 
        F: FnOnce(&mut T) -> &mut U,
        U: ?Sized;
    pub fn try_map<U, F>(orig: Self, f: F) -> Result<MappedMutexGuard<'mutex, U>, Self>
      where 
        F: FnOnce(&mut T) -> Option<&mut U>,
        U: ?Sized;
}

impl<T: ?Sized> Deref/DerefMut for MappedMutexGuard<'_, T>;
impl<T: ?Sized + Debug/Display> Debug/Display for MappedMutexGuard<'_, T>;
// std::sync::rwlock

pub struct MappedRwLockReadGuard<'rwlock, T: ?Sized>;
pub struct MappedRwLockWriteGuard<'rwlock, T: ?Sized>;

impl<'rwlock, T: ?Sized> RwLockReadGuard<'rwlock, T> {
    pub fn map<U, F>(orig: Self, f: F) -> MappedRwLockReadGuard<'rwlock, U>
      where 
        F: FnOnce(&T) -> &U,
        U: ?Sized;
    pub fn try_map<U, F>(orig: Self, f: F) -> Result<MappedRwLockReadGuard<'rwlock, U>, Self>
      where 
        F: FnOnce(&T) -> Option<&U>,
        U: ?Sized;
}
impl<'rwlock, T: ?Sized> MappedRwLockReadGuard<'rwlock, T> {
    pub fn map<U, F>(orig: Self, f: F) -> MappedRwLockReadGuard<'rwlock, U>
      where 
        F: FnOnce(&T) -> &U,
        U: ?Sized;
    pub fn try_map<U, F>(orig: Self, f: F) -> Result<MappedRwLockReadGuard<'rwlock, U>, Self>
      where 
        F: FnOnce(&T) -> Option<&U>,
        U: ?Sized;
}

impl<'rwlock, T: ?Sized> RwLockWriteGuard<'rwlock, T> {
    pub fn map<U, F>(orig: Self, f: F) -> MappedRwLockWriteGuard<'rwlock, U>
      where 
        F: FnOnce(&mut T) -> &mut U,
        U: ?Sized;
    pub fn try_map<U, F>(orig: Self, f: F) -> Result<MappedRwLockWriteGuard<'rwlock, U>, Self>
      where 
        F: FnOnce(&mut T) -> Option<&mut U>,
        U: ?Sized;
}
impl<'rwlock, T: ?Sized> MappedRwLockWriteGuard<'rwlock, T> {
    pub fn map<U, F>(orig: Self, f: F) -> MappedRwLockWriteGuard<'rwlock, U>
      where 
        F: FnOnce(&mut T) -> &mut U,
        U: ?Sized;
    pub fn try_map<U, F>(orig: Self, f: F) -> Result<MappedRwLockWriteGuard<'rwlock, U>, Self>
      where 
        F: FnOnce(&mut T) -> Option<&mut U>,
        U: ?Sized;
}

impl<T: ?Sized> Deref for MappedRwLockReadGuard<'_, T>;
impl<T: ?Sized> Deref/DerefMut for MappedRwLockWriteGuard<'_, T>;
impl<T: ?Sized + Debug/Display> Debug/Display for MappedRwLockReadGuard<'_, T>;
impl<T: ?Sized + Debug/Display> Debug/Display for MappedRwLockWriteGuard<'_, T>;

Steps / History

Unresolved Questions

  • None yet.

Footnotes

  1. https://std-dev-guide.rust-lang.org/feature-lifecycle/stabilization.html

@zachs18 zachs18 added C-tracking-issue Category: An issue tracking the progress of sth. like the implementation of an RFC T-libs-api Relevant to the library API team, which will review and decide on the PR/issue. labels Oct 23, 2023
bors added a commit to rust-lang-ci/rust that referenced this issue Feb 25, 2024
Implement `MappedMutexGuard`, `MappedRwLockReadGuard`, and `MappedRwLockWriteGuard`.

ACP: rust-lang/libs-team#260
Tracking issue: rust-lang#117108

<details> <summary> (Outdated) </summary>

`MutexState`/`RwLockState` structs

~~Having `sys::(Mutex|RwLock)` and `poison::Flag` as separate fields in the `Mutex`/`RwLock` would require `MappedMutexGuard`/`MappedRwLockWriteGuard` to hold an additional pointer, so I combined the two fields into a `MutexState`/`RwLockState` struct. This should not noticeably affect perf or layout, but requires an additional field projection when accessing the former `.inner` or `.poison` fields (now `.state.inner` and `.state.poison`).~~ If this is not desired, then `MappedMutexGuard`/`MappedRwLockWriteGuard` can instead hold separate pointers to the two fields.

</details>

The doc-comments are mostly copied from the existing `*Guard` doc-comments, with some parts from `lock_api::Mapped*Guard`'s doc-comments.

Unresolved question: Are more tests needed?
bors added a commit to rust-lang/miri that referenced this issue Feb 25, 2024
Implement `MappedMutexGuard`, `MappedRwLockReadGuard`, and `MappedRwLockWriteGuard`.

ACP: rust-lang/libs-team#260
Tracking issue: rust-lang/rust#117108

<details> <summary> (Outdated) </summary>

`MutexState`/`RwLockState` structs

~~Having `sys::(Mutex|RwLock)` and `poison::Flag` as separate fields in the `Mutex`/`RwLock` would require `MappedMutexGuard`/`MappedRwLockWriteGuard` to hold an additional pointer, so I combined the two fields into a `MutexState`/`RwLockState` struct. This should not noticeably affect perf or layout, but requires an additional field projection when accessing the former `.inner` or `.poison` fields (now `.state.inner` and `.state.poison`).~~ If this is not desired, then `MappedMutexGuard`/`MappedRwLockWriteGuard` can instead hold separate pointers to the two fields.

</details>

The doc-comments are mostly copied from the existing `*Guard` doc-comments, with some parts from `lock_api::Mapped*Guard`'s doc-comments.

Unresolved question: Are more tests needed?
lnicola pushed a commit to lnicola/rust-analyzer that referenced this issue Apr 7, 2024
Implement `MappedMutexGuard`, `MappedRwLockReadGuard`, and `MappedRwLockWriteGuard`.

ACP: rust-lang/libs-team#260
Tracking issue: rust-lang/rust#117108

<details> <summary> (Outdated) </summary>

`MutexState`/`RwLockState` structs

~~Having `sys::(Mutex|RwLock)` and `poison::Flag` as separate fields in the `Mutex`/`RwLock` would require `MappedMutexGuard`/`MappedRwLockWriteGuard` to hold an additional pointer, so I combined the two fields into a `MutexState`/`RwLockState` struct. This should not noticeably affect perf or layout, but requires an additional field projection when accessing the former `.inner` or `.poison` fields (now `.state.inner` and `.state.poison`).~~ If this is not desired, then `MappedMutexGuard`/`MappedRwLockWriteGuard` can instead hold separate pointers to the two fields.

</details>

The doc-comments are mostly copied from the existing `*Guard` doc-comments, with some parts from `lock_api::Mapped*Guard`'s doc-comments.

Unresolved question: Are more tests needed?
RalfJung pushed a commit to RalfJung/rust-analyzer that referenced this issue Apr 27, 2024
Implement `MappedMutexGuard`, `MappedRwLockReadGuard`, and `MappedRwLockWriteGuard`.

ACP: rust-lang/libs-team#260
Tracking issue: rust-lang/rust#117108

<details> <summary> (Outdated) </summary>

`MutexState`/`RwLockState` structs

~~Having `sys::(Mutex|RwLock)` and `poison::Flag` as separate fields in the `Mutex`/`RwLock` would require `MappedMutexGuard`/`MappedRwLockWriteGuard` to hold an additional pointer, so I combined the two fields into a `MutexState`/`RwLockState` struct. This should not noticeably affect perf or layout, but requires an additional field projection when accessing the former `.inner` or `.poison` fields (now `.state.inner` and `.state.poison`).~~ If this is not desired, then `MappedMutexGuard`/`MappedRwLockWriteGuard` can instead hold separate pointers to the two fields.

</details>

The doc-comments are mostly copied from the existing `*Guard` doc-comments, with some parts from `lock_api::Mapped*Guard`'s doc-comments.

Unresolved question: Are more tests needed?
@Fuuzetsu
Copy link

Is there anything needed to start FCP? Seems it's all implemented and just waiting around.

@Earthcomputer
Copy link

Is there anything needed to start FCP? Seems it's all implemented and just waiting around.

I think it's this, there has been activity on zulip about this since that comment.

@RaitoBezarius
Copy link

From the Zulip activity, it seems like this has nothing to do with mapped lock guards, or am I missing something?

@tgross35
Copy link
Contributor

This feature adds three structs and we would get three more with the nonpoison module (#134645), plus one more for ReentrantLockGuard (#121440). What if instead we added a perma-unstable LockGuard trait and then replaced these Mapped*Guard types with a single struct Mapped<G: LockGuard> { /* ... */ }? This would save us from having the same user-facing API repeated in seven different locations.

@ultimaweapon
Copy link

With the above API how to use with a method that return a value instead of a reference? e.g.:

let mtx = Mutex::new(HashMap::new());
let lock = mtx.lock().unwrap();
let mapped = MutexGuard::try_map(lock, |v| match v.entry(0) {
    Entry::Occupied(e) => Some(e), // This line won't work.
    Entry::Vacant(_) => None,
});

@zachs18
Copy link
Contributor Author

zachs18 commented Jan 10, 2025

These APIs can only be used with functions that return references. There are for two main reasons that I can think of for this design:

  1. A *Guard<'a, T> does not itself own a T; it has temporary exclusive1 access to a T that is owned by a lock somewhere else. Changing this API to instead allow owning a T that itself borrows something owned by a lock (like HypotheticalGuard<OccupiedEntry<'a, K, V>> would in your Mutex<HashMap<K, V>> example) would be a nontrivial change2.

  2. The returned reference needs to be converted into a raw pointer to store3. There's no "raw" version of a std::collections::hash_map::OccupiedEntry (or other types in general) that could be used for this in general4.

For your specific example, if you only want to access the value in the hashmap (not the key or other OccupiedEntry functionality), you could use Some(e.into_mut()) instead of Some(e), or just replace the whole closure with |v| v.get_mut(&0).

Footnotes

  1. or shared, in the case of (Mapped)RwLockReadGuard

  2. There's the issue that Rust doesn't (yet) have higher-ranked types other than dyn Trait IIUC, so writing the for<'a> OccupedEntry<'a, K, V> type or the for<'a> FnOnce(T<'a>) -> U<'a> bound generically might be impossible currently, or I think would at least require using a trait and a marker type. Also it would need to be ensured that T<'a> is not-contravariant in 'a. 2

  3. IIUC this is for aliasing reasons. For one example: a MutexGuard<'a, T> cannot hold a &'a mut T, since it would become invalid after dropping the MutexGuard, and (most) references that are function parameters currently must be valid for the whole function call IIUC, making fn foo(x: &Mutex<T>, y: MutexGuard<'_, T>) { drop(y); let z = x.lock(); } possibly unsound if MutexGuard<'a, T> held a &'a mut T.

  4. well, maybe MaybeDangling/MaybeUninit? I think it would be sound for HypotheticalGuard<T> to hold a MaybeDangling<T> and just not access it after unlocking, but that's not the only issue: see the other footnote2.

@zachs18
Copy link
Contributor Author

zachs18 commented Jan 10, 2025

What if instead we added a perma-unstable LockGuard trait and then replaced these Mapped*Guard types with a single struct Mapped<G: LockGuard> { /* ... */ }? This would save us from having the same user-facing API repeated in seven different locations.

I think this is a good idea if possible, like how std::num::NonZero is now generic. One possible problem I can think of is issues with variance if the Mapped type's API is implemented using associated types from trait LockGuard, as IIUC using an associated type projection from a type parameter as a struct field makes that parameter invariant (playground). This could maybe be bypassed by implementing LockGuard for marker types instead of the guards themselves, and having the lifetime and pointee as type parameters on Mapped, like struct Mapped<'a, GuardKind: LockGuardKind, T>. Though how to specify the variance of T there is still difficult, since *MutexGuard and *RwLockWriteGuard should be invariant, but *RwLockReadGuard should be covariant in T.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C-tracking-issue Category: An issue tracking the progress of sth. like the implementation of an RFC T-libs-api Relevant to the library API team, which will review and decide on the PR/issue.
Projects
None yet
Development

No branches or pull requests

6 participants