Inspired by this thread and the recent DynSized RFC, I had an idea for how to replace Pin with a forwards-compatible, library-only subset of !Move. Which is actually just !DynSized.
In ‘short’:
-
Pin<'a, T>is replaced by&'a mut T. SoFuture,Generator, etc. can just take&mut selfagain! Here I’ll talk aboutGeneratorfor convenience, but the same applies toFuture. - To enforce immovability, users never get access to owned generators directly; thus, instead of generator functions returning
impl Generator, they would return something likeimpl Anchor<Inner=impl Generator>, for some traittrait Anchor { type Inner: ?Sized; // PinBox and friends would use this method to implement DerefMut; // it's unsafe because you should only call it if you can guarantee // that `self` will never move again. unsafe fn get_mut(&mut self) -> &mut Self::Inner; } - Okay, the hard part. Why doesn’t the above ‘just work’?
- Because of
mem::swapand friends. Given&mut MyGeneratoryou can move it or (in some cases) extract an ownedMyGenerator, which breaks the assumption of immovability.
- Because of
- So what if we mark
MyGeneratoras!Sized? (Notwithstanding the semantic strangeness of making something!Sizedthat doesn’t actually have an unknown size.) That would blockmem::swapand similar.- But there’s one dangerous function that doesn’t have a
Sizedbound:mem::size_of_val. In particular, theBox-to-Rcconversion works even for unsized types and assumes it’s kosher to move them by memcpyingmem::size_of_val(foo)bytes. - Although you couldn’t actually use that conversion to break the above scheme, since you’d never be allowed to get a
Box<MyGenerator>, its presence in the standard library is a license for unsafe code to use the same technique in broader scenarios.
- But there’s one dangerous function that doesn’t have a
- But this is the same problem meant to be solved by
!DynSized!- The above-linked RFC proposes having
size_of_valeither panic or return 0 for a!DynSizedtype, combined with a lint; an alternative mentioned is to makeDynSizeda new default bound and add?DynSized. - Meant to be used with
extern type, i.e. FFI opaque pointers, which itself is already accepted and implemented (but not stable). - But it would work just as well for immovable types. The desired semantics are effectively the same at least from a generic code perspective.
- The above-linked RFC proposes having
- But even ignoring how crazy that sounds, how is it a library-only solution if
DynSizeddoesn’t even exist yet?
Well, for now we can simulate it with a silly hack. Generator functions would expand to something like:
// The anchor contains the actual state...
struct MyGeneratorAnchor {
// local variables go here...
x: i32,
}
// The generator type is just an extern type!
// In other words, &MyGenerator is an 'opaque pointer' that secretly points
// to MyGeneratorAnchor.
extern { type MyGenerator; }
impl Anchor for MyGeneratorAnchor {
type Inner = MyGenerator;
unsafe fn get_mut(&mut self) -> &mut Self::Inner {
// create the opaque pointer by casting to &mut MyGenerator:
&mut *(self as *mut MyGeneratorAnchor as *mut MyGenerator)
}
}
impl Generator for MyGenerator {
fn resume(&mut self) -> whatever {
// undo the above cast:
let anchor: &mut MyGeneratorAnchor = unsafe { &mut *(self as *mut MyGenerator as *mut MyGeneratorAnchor) };
// … now we can use local variables …
}
}
So, how does this solve the problem?
-
MyGeneratoris!Sized(extern typealready has that effect), so anything that tries to use the standardmem::swap,mem::size_of, etc. just won’t compile. -
mem::size_of_val(foo: &MyGenerator)will work but return 0 (because it’s anextern type). So if some tricky unsafe code tries to, say, swap two&mut MyGeneratorinstances by memcpying bytes around, that’ll just silently do nothing, leaving the generators’ actual data intact. That’s not great, in the sense that it’s a surprising result, but it’s not unsound either. And again, this is the exact same problem that applies to ‘real’ FFI opaque pointers.- …There might be some even weirder things unsafe code might try to do that would be unsound – though it’s hard to say it’d be justified in doing so – but that would be equally unsound with
extern typeused for FFI! (For example, a version oftake_mutthat takes?Sizedand returns the existing object copied to a box.)
- …There might be some even weirder things unsafe code might try to do that would be unsound – though it’s hard to say it’d be justified in doing so – but that would be equally unsound with
- In the hopefully near future, the
DynSizedRFC or something similar will land, providing for a lint or error when someone tries to usesize_of_valhere. - In the meantime, since essentially no generic code actually does that, you’ll be able to use the generic ecosystem as usual with
&MyGeneratorand&mut MyGenerator– with no need for everything to add an extra variant forPin.?Sizedis enough.
Thoughts?