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

Preserve original lifetime when performing pin-projection #67

Closed
wants to merge 9 commits into from

Conversation

Aaron1011
Copy link
Collaborator

As described in issue #65, the project method currently loses lifetime information.

Specifically, given a &'a mut Pin<&'pin mut T>, we would like project to return a ProjectionType<'pin> - that is, a type with a lifetime tied to the lifetime of the inside of the original Pin.

However, project currently returns a ProjectionType<'a> - that is, a type with a lifetime tied to the lifetime of the 'outside' of the Pin. This means that pin-projected types will not live as long as the inside of the Pin they were derived from - which prevents returning fields from methods.

This PR lifts this limitation, by ensuring that the returning projection struct/enum has a lifetime of 'pin - that is, the lifetime in the Pin<&'pin mut T> that the projection was performed on.

Most of this PR is just boilerplate to make things work on arbitrary structs and enums. The important part is the additional unsafe code added. Ultimately, it relies on this code being valid:

use std::ops::DerefMut;

fn upgrade_deref_mut<'a, T>(mut val: &'a mut T) -> &'a mut T {
    let my_ref: &mut T = val.deref_mut();
    let transmuted: &'a mut T = unsafe { core::mem::transmute(my_ref) };
    transmuted
}

That is, we need to be able to 'upgrade' the lifetime of the mutable reference returned from the <&mut T as DerefMut>::deref_mut. I believe this is sound - DerefMut is essentially a no-op for mutable references. We're only adjusting the compiler's view of the lifetime, not performing any raw pointer tricks that might make Stacked Borrows angry.

That being said, I'm not 100% sure that this is correct.

@RalfJung: Would you be able to take a look at this?

Note that this only relies on the built-in DerefMut impl for &mut T, not any user-provided &mut DerefMut impls. I think that this kind of 'upgrading' logic could potentially be extended to user-provided types that fulfill the requirement of StableDeref - that is, the mutable references they return live for as long as the original type. However, pin-project doesn't need to care about this.

@Aaron1011
Copy link
Collaborator Author

Assuming that this is indeed sound, it might be useful for Rust to provide this kind of upgrading behavior on Pin itself.

Making it would for arbitrary user types would require introducing a weaker form of StableDeref into the compiler (it only needs to allow transmuting mutable references, not any of the raw pointer operations that have given us trouble). However, it could be added only for Pin<&mut T>:

impl<'a, T> Pin<&'a mut T> {
	fn as_mut_better(&mut self) -> Pin<'a mut T> {
        /// The impl from this crate
    }
}

fn proj_trait_generics(&self) -> ProjTraitGenerics<'_> {
//let mut orig_generics_for_impl = orig.clone();
//crate::utils::proj_generics(&mut orig_generics_for_impl, self.lifetime.clone());
//let (impl_generics, modified_ty_generics, _) = orig_generics_for_impl.split_for_impl();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove the above 3 lines.

let (orig_generics, _, orig_where_clause) = self.generics.split_for_impl();
let mut trait_generics = self.generics.clone();
crate::utils::proj_generics(&mut trait_generics, lifetime.clone());
//trait_generics.params.push(GenericParam::Lifetime(LifetimeDef::new(lifetime.clone())));
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
//trait_generics.params.push(GenericParam::Lifetime(LifetimeDef::new(lifetime.clone())));

crate::utils::proj_generics(&mut orig_generics_for_impl, lifetime.clone());
let (impl_generics, modified_ty_generics, _) = orig_generics_for_impl.split_for_impl();

let (_, ty_generics, where_clause) = item.generics.split_for_impl();*/
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove the above 5 lines.

#[allow(unsafe_code)]
let raw_mut: &mut T = unsafe { Pin::get_unchecked_mut(as_mut) };

// NExt, we transmute the mutable reference, changing its lifetime to 'a.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// NExt, we transmute the mutable reference, changing its lifetime to 'a.
// Next, we transmute the mutable reference, changing its lifetime to 'a.

/// Normally, you'll never need to interact with this type
/// directly - it's used internally by pin-project to allow expressing
/// the proper lifetime.
pub trait ProjectThrough<'a> {
Copy link
Owner

@taiki-e taiki-e Sep 4, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this trait should be sealed?

mod private_project_through {
    use core::pin::Pin;

    pub trait Sealed {}

    impl<T> Sealed for Pin<&mut T> {}
}

pub trait ProjectThrough<'a>: private_project_through::Sealed {
    // ...
}

impl #impl_generics #proj_trait #ty_generics for ::core::pin::Pin<&mut #orig_ident #ty_generics> #where_clause {
fn project<#lifetime>(&#lifetime mut self) -> #proj_ident #proj_ty_generics #where_clause {
impl #impl_generics #proj_trait #ty_generics for ::core::pin::Pin<&#lifetime mut #orig_ident #orig_ty_generics> #where_clause {
fn project(&mut self) -> #proj_ident #proj_ty_generics #where_clause {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fn project(&mut self) -> #proj_ident #proj_ty_generics #where_clause {
fn project(&mut self) -> #proj_ident #proj_ty_generics {

Preexisting, but as it is already declared in the impl block, it looks unnecessary.


quote! {
trait #proj_trait #orig_generics {
fn project<#lifetime>(&#lifetime mut self) -> #proj_ident #proj_ty_generics #orig_where_clause;
fn project(&mut self) -> #proj_ident #proj_ty_generics #orig_where_clause;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fn project(&mut self) -> #proj_ident #proj_ty_generics #orig_where_clause;
fn project(&mut self) -> #proj_ident #proj_ty_generics;

impl #impl_generics #proj_trait #ty_generics for ::core::pin::Pin<&mut #orig_ident #ty_generics> #where_clause {
fn project<#lifetime>(&#lifetime mut self) -> #proj_ident #proj_ty_generics #where_clause {
impl #impl_generics #proj_trait #ty_generics for ::core::pin::Pin<&#lifetime mut #orig_ident #orig_ty_generics> #where_clause {
fn project(&mut self) -> #proj_ident #proj_ty_generics #where_clause {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
fn project(&mut self) -> #proj_ident #proj_ty_generics #where_clause {
fn project(&mut self) -> #proj_ident #proj_ty_generics {

/// A helper trait to allow projecting through a mutable reference,
/// while preserving lifetimes.
///
/// Normally, `Pin::as_mut` can be used to convert a '&mut Pin' to a owned 'Pin'
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Normally, `Pin::as_mut` can be used to convert a '&mut Pin' to a owned 'Pin'
/// Normally, `Pin::as_mut` can be used to convert a '&mut Pin' to an owned 'Pin'

/// while preserving lifetimes.
///
/// Normally, `Pin::as_mut` can be used to convert a '&mut Pin' to a owned 'Pin'
/// However, since `Pin::as_mut'`needs to work with generic `DerefMut` impls,
Copy link
Owner

@taiki-e taiki-e Sep 4, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// However, since `Pin::as_mut'`needs to work with generic `DerefMut` impls,
/// However, since `Pin::as_mut` needs to work with generic `DerefMut` impls,

// We accomplish this in four steps:
//
// First, call Pin::as_mut(self). This
// converts our '&'p mut Pin<&'a mut T>` to
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// converts our '&'p mut Pin<&'a mut T>` to
// converts our `&'p mut Pin<&'a mut T>` to

// described above.
//
// This is the core of this method - everything else is just to deconstruct
// and reconstrut a Pin
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// and reconstrut a Pin
// and reconstruct a Pin.

@taiki-e taiki-e added this to the v0.4 milestone Sep 4, 2019
@Aaron1011
Copy link
Collaborator Author

Aaron1011 commented Sep 4, 2019

It turns out that this is actually unsound - the output lifetime isn't tied to the input lifetime, so it's possible to create aliasing mutable references by calling project twice.

I'm investigating if this can be made sound.

@Aaron1011
Copy link
Collaborator Author

Unfortunately, I think it's impossible to do this soundly in the project method.

There are two conflicting requirements:

  • The projection type returned from Project should not be required to outlive 'a in Pin<&'a mut MyType>. This allows us to call project() multiple times, and to use the original Pin<&'a mut MyType> after calling project().
  • THe projection type returned from Project should be required to outlive 'a in Pin<&'a mut MyType>. This allow us to write code like:
fn get_pin_mut<'a>(mut self: Pin<&'a mut Self>) -> Pin<&'a mut T> {
    self.project().pinned
}

where the returned type outlives the self parameter passed into the method.

It is impossible for one lifetime that both outlives and does not outlive another lifetime.

Instead, I think we need to add a new method: project_into. This method will take self by value (effectively adding the pre-#47 behavior back). Users will choose between project or project_into based on their needs.

@Aaron1011
Copy link
Collaborator Author

Aaron1011 commented Sep 4, 2019

Unfortunately, I think it's impossible to do this soundly in the project method.

There are two conflicting requirements:

  • The projection type returned from Project should not be required to outlive 'a in Pin<&'a mut MyType>. This allows us to call project() multiple times, and to use the original Pin<&'a mut MyType> after calling project().
  • The projection type returned from Project should be required to outlive 'a in Pin<&'a mut MyType>. This allow us to write code like:
fn get_pin_mut<'a>(mut self: Pin<&'a mut Self>) -> Pin<&'a mut T> {
    self.project().pinned
}

where the returned type outlives the self parameter passed into the method.

It is impossible for one lifetime to both outlive and not outlive another lifetime.

Instead, I think we need to add a new method: project_into. This method will take self by value (effectively adding the pre-#47 behavior back). Users will choose between project or project_into based on their needs.

@Aaron1011
Copy link
Collaborator Author

Aaron1011 commented Sep 4, 2019

Superseded by #69

@Aaron1011 Aaron1011 closed this Sep 4, 2019
@Aaron1011 Aaron1011 deleted the fix/proj-lifetime branch September 4, 2019 17:35
@RalfJung
Copy link
Contributor

I am very confused by this PR, this seems entirely unnecessary -- why don't you reborrow the pinned reference? See what I wrote at #47 (comment).

@taiki-e taiki-e added the A-pin-projection Area: #[pin_project] label Sep 28, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-pin-projection Area: #[pin_project]
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants