From 6da124253341cba3ed3c3726987508c0b25b8db9 Mon Sep 17 00:00:00 2001 From: Aaron Hill Date: Sun, 25 Aug 2019 16:46:02 -0400 Subject: [PATCH] Allow using #[pin_project] type with private field types Previously, given code such as: ```rust struct Private; pub struct Public { #[pin] private: Private } ``` 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 (https://github.com/rust-lang/rust/issues/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 { __field0: Private } impl Unpin for Public where __UnpinPublic: 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. --- pin-project-internal/src/pin_project/enums.rs | 4 +- pin-project-internal/src/pin_project/mod.rs | 103 ++++++++++++++++-- .../src/pin_project/structs.rs | 4 +- tests/pin_project.rs | 24 ++++ tests/ui/pin_project/proper_unpin.rs | 25 +++++ tests/ui/pin_project/proper_unpin.stderr | 19 ++++ 6 files changed, 168 insertions(+), 11 deletions(-) create mode 100644 tests/ui/pin_project/proper_unpin.rs create mode 100644 tests/ui/pin_project/proper_unpin.stderr diff --git a/pin-project-internal/src/pin_project/enums.rs b/pin-project-internal/src/pin_project/enums.rs index 03f753af..648e2be6 100644 --- a/pin-project-internal/src/pin_project/enums.rs +++ b/pin-project-internal/src/pin_project/enums.rs @@ -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>)); @@ -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>)); diff --git a/pin-project-internal/src/pin_project/mod.rs b/pin-project-internal/src/pin_project/mod.rs index 5d35a8ea..d53ffbc7 100644 --- a/pin-project-internal/src/pin_project/mod.rs +++ b/pin-project-internal/src/pin_project/mod.rs @@ -64,6 +64,8 @@ struct Context { /// Where-clause for conditional Unpin implementation. impl_unpin: WhereClause, + pinned_fields: Vec, + unsafe_unpin: bool, pinned_drop: Option, } @@ -97,6 +99,7 @@ impl Context { generics, lifetime, impl_unpin, + pinned_fields: vec![], unsafe_unpin: unsafe_unpin.is_some(), pinned_drop, }) @@ -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(PhantomData) {} + // impl Unpin for AlwaysUnpin {} + // + // ... + // _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 { + val: ::core::marker::PhantomData + } + + impl ::core::marker::Unpin for AlwaysUnpin {} + + 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 {} + } + } } } diff --git a/pin-project-internal/src/pin_project/structs.rs b/pin-project-internal/src/pin_project/structs.rs index b60a3dd8..af805d30 100644 --- a/pin-project-internal/src/pin_project/structs.rs +++ b/pin-project-internal/src/pin_project/structs.rs @@ -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))); @@ -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))); diff --git a/tests/pin_project.rs b/tests/pin_project.rs index 716a80de..b20afa13 100644 --- a/tests/pin_project.rs +++ b/tests/pin_project.rs @@ -238,3 +238,27 @@ fn combine() { #[allow(unsafe_code)] unsafe impl UnsafeUnpin for Foo {} } + +#[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 { + #[pin] + inner: PrivateStruct, + } + + struct PrivateStruct(T); + + #[pin_project] + pub enum PublicEnum { + Variant(#[pin] PrivateEnum), + } + + enum PrivateEnum { + OtherVariant(u8), + } +} diff --git a/tests/ui/pin_project/proper_unpin.rs b/tests/ui/pin_project/proper_unpin.rs new file mode 100644 index 00000000..1eac4780 --- /dev/null +++ b/tests/ui/pin_project/proper_unpin.rs @@ -0,0 +1,25 @@ +// compile-fail + +#![deny(warnings, unsafe_code)] + +use pin_project::{pin_project, pinned_drop}; +use std::pin::Pin; + +struct Inner { + val: T +} + +#[pin_project] +struct Foo { + #[pin] + inner: Inner, + other: U +} + +fn is_unpin() {} + +fn bar() { + is_unpin::>(); //~ ERROR E0277 +} + +fn main() {} diff --git a/tests/ui/pin_project/proper_unpin.stderr b/tests/ui/pin_project/proper_unpin.stderr new file mode 100644 index 00000000..1066084e --- /dev/null +++ b/tests/ui/pin_project/proper_unpin.stderr @@ -0,0 +1,19 @@ +error[E0277]: the trait bound `T: std::marker::Unpin` is not satisfied in `__unpin_scope_Foo::__UnpinStructFoo` + --> $DIR/proper_unpin.rs:22:5 + | +22 | is_unpin::>(); //~ ERROR E0277 + | ^^^^^^^^^^^^^^^^^^^^^ within `__unpin_scope_Foo::__UnpinStructFoo`, 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` + = note: required because it appears within the type `__unpin_scope_Foo::__UnpinStructFoo` + = note: required because of the requirements on the impl of `std::marker::Unpin` for `Foo` +note: required by `is_unpin` + --> $DIR/proper_unpin.rs:19:1 + | +19 | fn is_unpin() {} + | ^^^^^^^^^^^^^^^^^^^^^^^ + +error: aborting due to previous error + +For more information about this error, try `rustc --explain E0277`.