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

[Feature] extend TransparentWrapper conversion functions #58

Merged
merged 8 commits into from
Mar 29, 2021
Merged

[Feature] extend TransparentWrapper conversion functions #58

merged 8 commits into from
Mar 29, 2021

Conversation

luiswirth
Copy link
Contributor

@luiswirth luiswirth commented Mar 21, 2021

This PR extends the TransparentWrapper trait by adding new conversion functions.
Namely:

These three missing wrap_{} functions.

  • TransparentWrapper::wrap(s: Inner) -> Self
  • TransparentWrapper::wrap_slice(s: &[Inner]) -> &[Self]
  • TransparentWrapper::wrap_slice_mut(s: &mut [Inner]) -> &mut [Self]

These five unwrap_{} functions, which are the counterparts of the wrap_{} functions. They just invert the conversion.

  • TransparentWrapper::unwrap(self) -> Inner
  • TransparentWrapper::unwrap_ref(&self) -> &Inner
  • TransparentWrapper::unwrap_mut(&mut self) -> &mut Inner
  • TransparentWrapper::unwrap_slice(s: &[Self]) -> &[Inner]
  • TransparentWrapper::unwrap_slice_mut(s: &mut [Self]) -> &mut [Inner]

This is useful for implementing Zeroable and Pod on types from extern crates in order to use bytemuck::{} functions like bytemuck::cast_slice using the new-ype pattern and TransparentWrapper.

The goal of this PR is to support all bytemuck::{} functions.

This is possible with this PR:

  // An external type defined in a different crate.
  #[derive(Copy, Clone, Default)]
  struct Foreign(u8);

  use bytemuck::TransparentWrapper;

  #[derive(Copy, Clone)]
  #[repr(transparent)]
  struct Wrapper(Foreign);

  unsafe impl TransparentWrapper<Foreign> for Wrapper {}

  // Traits can be implemented on crate-local wrapper.
  unsafe impl bytemuck::Zeroable for Wrapper {}
  unsafe impl bytemuck::Pod for Wrapper {}

  let _: u8 = bytemuck::cast(Wrapper::wrap(Foreign::default()));
  let _: Foreign = Wrapper::unwrap(bytemuck::cast(u8::default()));

  let _: &u8 = bytemuck::cast_ref(Wrapper::wrap_ref(&Foreign::default()));
  let _: &Foreign = Wrapper::unwrap_ref(bytemuck::cast_ref(&u8::default()));

  let _: &mut u8 = bytemuck::cast_mut(Wrapper::wrap_mut(&mut Foreign::default()));
  let _: &mut Foreign = Wrapper::unwrap_mut(bytemuck::cast_mut(&mut u8::default()));

  let _: &[u8] = bytemuck::cast_slice(Wrapper::wrap_slice(&[Foreign::default()]));
  let _: &[Foreign] = Wrapper::unwrap_slice(bytemuck::cast_slice(&[u8::default()]));

  let _: &mut [u8] = bytemuck::cast_slice_mut(Wrapper::wrap_slice_mut(&mut [Foreign::default()]));
  let _: &mut [Foreign] = Wrapper::unwrap_slice_mut(bytemuck::cast_slice_mut(&mut [u8::default()]));

  let _: &[u8] = bytemuck::bytes_of(Wrapper::wrap_ref(&Foreign::default()));
  let _: &Foreign = Wrapper::unwrap_ref(bytemuck::from_bytes(&[u8::default()]));

  let _: &mut [u8] = bytemuck::bytes_of_mut(Wrapper::wrap_mut(&mut Foreign::default()));
  let _: &mut Foreign = Wrapper::unwrap_mut(bytemuck::from_bytes_mut(&mut [u8::default()]));

  // not sure if this is the right usage
  let _ = bytemuck::pod_align_to::<_, u8>(Wrapper::wrap_slice(&[Foreign::default()]));
  // counterpart?

  // not sure if this is the right usage
  let _ = bytemuck::pod_align_to_mut::<_, u8>(Wrapper::wrap_slice_mut(&mut [Foreign::default()]));
  // counterpart?

This PR also includes a test, which uses the exact example code from above. This is for MIRI to check for any potential UB.
As you can see, there currently isn't any checking done on the output of the functions. This is something left todo.

Can somebody please check the soundness of the implementation, because I have very little expertise with unsafe code.

I've written up some docs on how this trait can be used in combination with the newtype pattern to use the bytemuck functions on foreign types from external crates.

I also adjusted rustfmt.toml in the first commit, because of the deprecation of merge_imports = true. It is being replaced by imports_granularity = "Crate". Furthermore I ran cargo +nightly fmt to format the codebase. I hope these two changes are fine.

I'm open to suggestions!

This is my first PR ever 🥳 .

replace `merge_imports = true` (deprecated) with `imports_granularity = "Crate"`
@Lokathor
Copy link
Owner

This looks correct.

I'll have @thomcc take a look too, because they wrote this trait.

Copy link
Contributor

@thomcc thomcc left a comment

Choose a reason for hiding this comment

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

Up on line 28 you should add this method to the list of methods that may not be overwridden.

Otherwise it seems fine.

@luiswirth luiswirth changed the title [Feature] add TransparentWrapper::wrap_slice [Feature] add TransparentWrapper::{wrap_slice, wrap_slice_mut} Mar 22, 2021
@luiswirth luiswirth marked this pull request as draft March 22, 2021 17:34
@luiswirth
Copy link
Contributor Author

I've decided to extend this PR some more, therefore I converted it into a draft.
I would like to add all conversions needed, to support all bytemuck functions.

@luiswirth luiswirth changed the title [Feature] add TransparentWrapper::{wrap_slice, wrap_slice_mut} [Feature] extend TransparentWrapper conversion functions Mar 22, 2021
@luiswirth
Copy link
Contributor Author

luiswirth commented Mar 22, 2021

I've now rewritten and extended the PR quite a bit. Please take a look at it again :).

I would also like to introduce the wrap(s: Wrapped) -> Self and unwrap(self) -> Wrapped conversions functions.
This would be nice, because then the conversion for bytemuck::cast would look the same as all other conversions.
I'm not quite sure how we could do this... Maybe a transmute?
This would require a Sized bound for Self and Wrapped on the function.
Is this desirable?

Also I would like to add some docs, mentioning the usefulness of this trait for using bytemuck with types from extern crates.

@Lokathor
Copy link
Owner

I believe that the transmute_copy trick used to wrap can also be used to unwrap.

rewriting some docs for conciseness

rename `Wrapped` to `Inner`, because it's hard to visually differentiate between
`Wrapper` and `Wrapped`
Implement 3 new wrapping functions on `TransparentWrapper` providing new
conversions.

- `TransparentWrapper::wrap(s: Inner) -> Self`
- `TransparentWrapper::wrap_slice(s: &[Inner]) -> &[Self]`
- `TransparentWrapper::wrap_slice_mut(s: &mut [Inner]) -> &mut [Self]`
Implement counterparts to `TransparentWrapper::wrap_{}` functions
providing reverse conversions.

- `TransparentWrapper::unwrap(self) -> Inner`
- `TransparentWrapper::unwrap_ref(&self) -> &Inner`
- `TransparentWrapper::unwrap_mut(&mut self) -> &mut Inner`
- `TransparentWrapper::unwrap_slice(s: &[Self]) -> &[Inner]`
- `TransparentWrapper::unwrap_slice_mut(s: &mut [Self]) -> &mut [Inner]`
This test is only for MIRI to check all trait functions on
`TransparentWrapper` if they cause any UB.
The output of the functions is not checked.
@luiswirth
Copy link
Contributor Author

Okay, another rather big overhaul of this PR is done 😄 .

I adjusted the docs a little to be more concise. Also I renamed Wrapped to Inner to make it visually more distinct from Wrapper.

I've also added the wrap and unwrap functions. I didn't use the transmute_copy trick because I don't have to transmute any pointers since there is a Sized bound on Self and Inner. This is needed because rust needs Sized functions arguments and return types.

I also added a test which currently just checks for UB using MIRI. Thanks to this I already catched a bug causing UB.
There isn't any actual checking done on the results of the functions.

@thomcc
Copy link
Contributor

thomcc commented Mar 23, 2021

I'm not sure unwrap is the right term for these, given that that terminology is already used very commonly in Rust to mean something different...

@luiswirth
Copy link
Contributor Author

@thomcc I agree, but I haven't come up with a better fitting terminology. It is a wrapper after all :D.
I'm open for suggestions.

@luiswirth
Copy link
Contributor Author

I've now added some docs and a example mentioning how TransparentWrapper can be used to implement traits like Zeroable and Pod on external types.

@luiswirth luiswirth marked this pull request as ready for review March 24, 2021 18:09
@luiswirth
Copy link
Contributor Author

This PR is no longer a draft! I'm pretty happy with it.

Now I'm waiting for a review and some feedback on what's left to do :).

@luiswirth
Copy link
Contributor Author

I just noticed, that deriving TransparentWrapper, Zeroable and Pod on a struct doesn't work as expected. The derive fails, because the inner type doesn't implement Zeroable and Pod. We should lift this restriction if TransparentWrapper is also implemented (manually or derived).
I feel like this is hard to check with a proc macro. This might be a challenge for another PR.

@Lokathor
Copy link
Owner

I feel like the derive should continue to fail even then. You can still write it manually. The fact that you didn't just derive it is a good time to say "i know the base type doesn't impl the trait but i trust i can bend the rule in this case"

@luiswirth
Copy link
Contributor Author

@Lokathor Hmm, It's just unfortunate, that in this case you can't have the derive check, if the safety guarantees hold.

Does the current proc-macro implementation check all aspects of the unsafe contract? It should, because otherwise there would be a possibility of introducing UB without having an unsafe block...

If the derive can check all aspects of the unsafe contract, then you shouldn't need to think about bending the rules, because it's guaranteed to be safe and sound :).
I'm just not sure if these new restrictions are easily checkable.

@Lokathor
Copy link
Owner

It may be possible to fix. We could probably finish off the PR noe and file an issue for the derives later.

@luiswirth
Copy link
Contributor Author

Sounds good to me!

src/lib.rs Outdated
@@ -30,6 +30,17 @@
//! these traits are `unsafe`, and you should carefully read the requirements
//! before adding the them to your own types.
//!
//! ## Using Types from Extern Crates
Copy link
Owner

Choose a reason for hiding this comment

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

I don't think that we should document this idea.

It's possible, but it's kinda extra dodgy, particularly because it runs explicitly against the claimed rules of Pod and Zeroable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I get your point. It's getting kinda convoluted.

But I don't know how obvious this use of TransparentWrapper is. It is a pretty nice feature to have, so people should know about it.

Maybe we need to reformulate the docs some more? I'll give it a try and if it looks bad, we probably have to remove it...

What is most desirable?

  • Mentioning TransparentWrapper everywhere where it's relevant (so also in Pod).
  • Being subtle about it and just mentioning it in it's own docs.
  • Not mention it at all.

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think we should mention it, since it's not sound unless you pin the version exactly. If other crates haven't opted into the bytemuck requirements, they're free to break them at any time without a semver-major change.

Copy link
Owner

Choose a reason for hiding this comment

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

Yeah, the reason that the docs for Pod and Zeroable require that the field type also is Pod or Zeroable is because then SemVer kicks in, specifically that it's a SemVer Break to remove a trait impl.

If you newtype a struct and declare the wrapper type Pod, then the inner type adds a &'static str field in a minor update (not unreasonable!) then suddenly you're hosed.

So I think that this should stay as a "secret thing the type system technically allows but officially you shouldn't so legally you can't complain when this blows up in your face (and it easily might)" sort of thing.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Okay, I see. That's a reasonable argument.
In this case I'm going to remove all mentions of this.

@thomcc
Copy link
Contributor

thomcc commented Mar 25, 2021

I think I'm not really a fan of having this many functions on the trait. It would probably be better to move some/most of them to free functions, since at this point there's a real risk of collision with inherent or deref impls, especially with the generic naming scheme...

@luiswirth
Copy link
Contributor Author

luiswirth commented Mar 25, 2021

@thomcc
I'm not quite sure how collisions might happen?
Is it enough to change the self to Self in the arguments of unwrap, unwrap_ref and unwrap_mut?
This is what I did for now.

Methods on `TransparentWrapper` trait are now associated functions.
They now take `Self` instead of `self` as argument)

- `TransparentWrapper::unwrap(s: Self)`
- `TransparentWrapper::unwrap_ref(s: &Self)`
- `TransparentWrapper::unwrap_mut(s: &mut Self)`
@luiswirth
Copy link
Contributor Author

Anything left to do?

@Lokathor
Copy link
Owner

I haven't had time to get to this. My "weekend" is Sun/Mon, so I'll try to get this merged.

Copy link
Owner

@Lokathor Lokathor left a comment

Choose a reason for hiding this comment

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

I'm going to approve and merge now, and I'll think hard for a bit about if there's a better name than unwrap to use.

I think I also want to sort out how we're going to adapt to min_const_generics being available before the next crates.io release.

@Lokathor Lokathor merged commit 30a9606 into Lokathor:main Mar 29, 2021
@luiswirth
Copy link
Contributor Author

Yeah, my first PR was merged 🥳. Thanks :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants