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 self
again! Here I’ll talk aboutGenerator
for 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::swap
and friends. Given&mut MyGenerator
you can move it or (in some cases) extract an ownedMyGenerator
, which breaks the assumption of immovability.
- Because of
- So what if we mark
MyGenerator
as!Sized
? (Notwithstanding the semantic strangeness of making something!Sized
that doesn’t actually have an unknown size.) That would blockmem::swap
and similar.- But there’s one dangerous function that doesn’t have a
Sized
bound:mem::size_of_val
. In particular, theBox
-to-Rc
conversion 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_val
either panic or return 0 for a!DynSized
type, combined with a lint; an alternative mentioned is to makeDynSized
a 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
DynSized
doesn’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?
-
MyGenerator
is!Sized
(extern type
already 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 MyGenerator
instances 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 type
used for FFI! (For example, a version oftake_mut
that takes?Sized
and 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
DynSized
RFC or something similar will land, providing for a lint or error when someone tries to usesize_of_val
here. - In the meantime, since essentially no generic code actually does that, you’ll be able to use the generic ecosystem as usual with
&MyGenerator
and&mut MyGenerator
– with no need for everything to add an extra variant forPin
.?Sized
is enough.
Thoughts?