Skip to content

Commit

Permalink
Allow using #[pin_project] type with private field types
Browse files Browse the repository at this point in the history
Previously, given code such as:

```rust

struct Private<T>;

pub struct Public<T> {
    #[pin] private: Private<T>
}
```

we would generate an Unpin impl like this:

```rust
impl Unpin for Public where Private: Unpin {}
```

Unfortunately, since Private is not a public type,
this would cause an E0446 ('private type `Private` in public interface)

When RFC 2145 is implemented (rust-lang/rust#48054),
this will become a lint, rather then a hard error.

In the time being, we need a solution that will work with the current
type privacy rules.

The solution is to generate code like this:

```rust

fn __private_scope() {
    pub struct __UnpinPublic<T> {
        __field0: Private<T>
    }
    impl<T> Unpin for Public<T> where __UnpinPublic<T>: Unpin {}
}
```

That is, we generate a new struct, containing all of the pinned
fields from our #[pin_project] type. This struct is delcared within
a function, which makes it impossible to be named by user code.
This guarnatees that it will use the default auto-trait impl for Unpin -
that is, it will implement Unpin iff all of its fields implement Unpin.
This type can be safely declared as 'public', satisfiying the privacy
checker without actually allowing user code to access it.

This allows users to apply the #[pin_project] attribute to types
regardless of the privacy of the types of their fields.
  • Loading branch information
Aaron1011 committed Aug 25, 2019
1 parent c3f6a41 commit 6da1242
Show file tree
Hide file tree
Showing 6 changed files with 168 additions and 11 deletions.
4 changes: 2 additions & 2 deletions pin-project-internal/src/pin_project/enums.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ fn named(
for Field { attrs, ident, ty, .. } in fields {
if let Some(attr) = attrs.find_remove(PIN) {
let _: Nothing = syn::parse2(attr.tokens)?;
cx.push_unpin_bounds(ty);
cx.push_unpin_bounds(ty.clone());
let lifetime = &cx.lifetime;
proj_body.push(quote!(#ident: ::core::pin::Pin::new_unchecked(#ident)));
proj_field.push(quote!(#ident: ::core::pin::Pin<&#lifetime mut #ty>));
Expand Down Expand Up @@ -108,7 +108,7 @@ fn unnamed(
let x = format_ident!("_x{}", i);
if let Some(attr) = attrs.find_remove(PIN) {
let _: Nothing = syn::parse2(attr.tokens)?;
cx.push_unpin_bounds(ty);
cx.push_unpin_bounds(ty.clone());
let lifetime = &cx.lifetime;
proj_body.push(quote!(::core::pin::Pin::new_unchecked(#x)));
proj_field.push(quote!(::core::pin::Pin<&#lifetime mut #ty>));
Expand Down
103 changes: 96 additions & 7 deletions pin-project-internal/src/pin_project/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ struct Context {
/// Where-clause for conditional Unpin implementation.
impl_unpin: WhereClause,

pinned_fields: Vec<Type>,

unsafe_unpin: bool,
pinned_drop: Option<Span>,
}
Expand Down Expand Up @@ -97,6 +99,7 @@ impl Context {
generics,
lifetime,
impl_unpin,
pinned_fields: vec![],
unsafe_unpin: unsafe_unpin.is_some(),
pinned_drop,
})
Expand All @@ -109,21 +112,107 @@ impl Context {
generics
}

fn push_unpin_bounds(&mut self, ty: &Type) {
// We only add bounds for automatically generated impls
if !self.unsafe_unpin {
self.impl_unpin.predicates.push(syn::parse_quote!(#ty: ::core::marker::Unpin));
}
fn push_unpin_bounds(&mut self, ty: Type) {
self.pinned_fields.push(ty);
}

/// Makes conditional `Unpin` implementation for original type.
fn make_unpin_impl(&self) -> TokenStream {
let orig_ident = &self.orig_ident;
let (impl_generics, ty_generics, _) = self.generics.split_for_impl();
let type_params: Vec<_> = self.generics.type_params().map(|t| t.ident.clone()).collect();
let where_clause = &self.impl_unpin;

quote! {
impl #impl_generics ::core::marker::Unpin for #orig_ident #ty_generics #where_clause {}
if self.unsafe_unpin {
quote! {
impl #impl_generics ::core::marker::Unpin for #orig_ident #ty_generics #where_clause {}
}
} else {
let struct_ident = format_ident!("__UnpinStruct{}", orig_ident);

// Generate a field in our new struct for every
// pinned field in the original type
let fields: Vec<_> = self
.pinned_fields
.iter()
.enumerate()
.map(|(i, ty)| {
let field_ident = format_ident!("__field{}", i);
quote! {
#field_ident: #ty
}
})
.collect();

let fields_comma = if fields.is_empty() { quote!() } else { quote!(,) };

// We could try to determine the subset of type parameters
// and lifetimes that are actually used by the pinned fields
// (as opposed to those only used by unpinned fields).
// However, this would be tricky and error-prone, since
// it's possible for users to create types that would alias
// with generic parameters (e.g. 'struct T').
//
// Instead, we generate a use of every single type parameter
// and lifetime used in the original struct. For type parameters,
// we generate code like this:
//
// ```rust
// struct AlwaysUnpin<T: ?Sized>(PhantomData<T>) {}
// impl<T: ?Sized> Unpin for AlwaysUnpin<T> {}
//
// ...
// _field: AlwaysUnpin<(A, B, C)>
// ```
//
// This ensures that any unused type paramters
// don't end up with Unpin bounds
let lifetime_fields: Vec<_> = self
.generics
.lifetimes()
.enumerate()
.map(|(i, l)| {
let field_ident = format_ident!("__lifetime{}", i);
quote! {
#field_ident: &#l ()
}
})
.collect();

let fn_ident = format_ident!("__unpin_scope_{}", orig_ident);

let full_generics = &self.generics;
let mut full_where_clause = where_clause.clone();

let unpin_clause: WherePredicate = syn::parse_quote! {
#struct_ident #ty_generics: ::core::marker::Unpin
};

full_where_clause.predicates.push(unpin_clause);

quote! {
// By writing our type inside a function scope,
// we guarantee that it cannot be named by another
// other code
#[allow(non_snake_case)]
fn #fn_ident() {

struct AlwaysUnpin<T: ?Sized> {
val: ::core::marker::PhantomData<T>
}

impl<T: ?Sized> ::core::marker::Unpin for AlwaysUnpin<T> {}

pub struct #struct_ident #full_generics #where_clause {
__pin_project_use_generics: AlwaysUnpin<(#(#type_params),*)>,

#(#fields),* #fields_comma
#(#lifetime_fields),*
}

impl #impl_generics ::core::marker::Unpin for #orig_ident #ty_generics #full_where_clause {}
}
}
}
}

Expand Down
4 changes: 2 additions & 2 deletions pin-project-internal/src/pin_project/structs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ fn named(
for Field { attrs, ident, ty, .. } in fields {
if let Some(attr) = attrs.find_remove(PIN) {
let _: Nothing = syn::parse2(attr.tokens)?;
cx.push_unpin_bounds(ty);
cx.push_unpin_bounds(ty.clone());
let lifetime = &cx.lifetime;
proj_fields.push(quote!(#ident: ::core::pin::Pin<&#lifetime mut #ty>));
proj_init.push(quote!(#ident: ::core::pin::Pin::new_unchecked(&mut this.#ident)));
Expand All @@ -79,7 +79,7 @@ fn unnamed(
let i = Index::from(i);
if let Some(attr) = attrs.find_remove(PIN) {
let _: Nothing = syn::parse2(attr.tokens)?;
cx.push_unpin_bounds(ty);
cx.push_unpin_bounds(ty.clone());
let lifetime = &cx.lifetime;
proj_fields.push(quote!(::core::pin::Pin<&#lifetime mut #ty>));
proj_init.push(quote!(::core::pin::Pin::new_unchecked(&mut this.#i)));
Expand Down
24 changes: 24 additions & 0 deletions tests/pin_project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,3 +238,27 @@ fn combine() {
#[allow(unsafe_code)]
unsafe impl<T: Unpin> UnsafeUnpin for Foo<T> {}
}

#[test]
// This 'allow' is unrelated to the code
// generated by pin-project - it's just to
// allow us to put a private enum in a public enum
#[allow(private_in_public)]
fn test_private_type_in_public_type() {
#[pin_project]
pub struct PublicStruct<T> {
#[pin]
inner: PrivateStruct<T>,
}

struct PrivateStruct<T>(T);

#[pin_project]
pub enum PublicEnum {
Variant(#[pin] PrivateEnum),
}

enum PrivateEnum {
OtherVariant(u8),
}
}
25 changes: 25 additions & 0 deletions tests/ui/pin_project/proper_unpin.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// compile-fail

#![deny(warnings, unsafe_code)]

use pin_project::{pin_project, pinned_drop};
use std::pin::Pin;

struct Inner<T> {
val: T
}

#[pin_project]
struct Foo<T, U> {
#[pin]
inner: Inner<T>,
other: U
}

fn is_unpin<T: Unpin>() {}

fn bar<T, U>() {
is_unpin::<Foo<T, U>>(); //~ ERROR E0277
}

fn main() {}
19 changes: 19 additions & 0 deletions tests/ui/pin_project/proper_unpin.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
error[E0277]: the trait bound `T: std::marker::Unpin` is not satisfied in `__unpin_scope_Foo::__UnpinStructFoo<T, U>`
--> $DIR/proper_unpin.rs:22:5
|
22 | is_unpin::<Foo<T, U>>(); //~ ERROR E0277
| ^^^^^^^^^^^^^^^^^^^^^ within `__unpin_scope_Foo::__UnpinStructFoo<T, U>`, the trait `std::marker::Unpin` is not implemented for `T`
|
= help: consider adding a `where T: std::marker::Unpin` bound
= note: required because it appears within the type `Inner<T>`
= note: required because it appears within the type `__unpin_scope_Foo::__UnpinStructFoo<T, U>`
= note: required because of the requirements on the impl of `std::marker::Unpin` for `Foo<T, U>`
note: required by `is_unpin`
--> $DIR/proper_unpin.rs:19:1
|
19 | fn is_unpin<T: Unpin>() {}
| ^^^^^^^^^^^^^^^^^^^^^^^

error: aborting due to previous error

For more information about this error, try `rustc --explain E0277`.

0 comments on commit 6da1242

Please sign in to comment.