You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository has been archived by the owner on Aug 19, 2024. It is now read-only.
Currently to write a constructor/move-constructor/copy-constructor, you have to deal with a lot of unsafe code and boilerplate steps. Each constructor has to do:
un-pin a MaybeUninitialized reference
create the being-constructed type T
initialize the MaybeUninitialized reference with the object from (2)
get a pointer, and then a reference, to the MaybeUninitialized memory
use the reference from (4) to set up self-referential pointers in the object from (2)
The only steps that the type T actually cares about and wants to define are (2) and (5), but today it has to write the whole algorithm in each constructor/move-constructor/copy-constructor. Like so:
fnmyctor(dest:Pin<&mutMaybeUninit<<TasSafeCtor>::Output>>){unsafe{let maybe_uninit_ref = Pin::into_inner_unchecked(dest);// 1let(out, init) = T{ ...};// 2 (using the copy- or moved-from object in copy- or move-constructors)*maybe_uninit_ref = MaybeUninit::new(out);// 3let t_ref = &mut*maybe_uninit_ref.as_mut_ptr();// 4setup_self_references(t_ref, init);// 5}}
I have implemented a set of "SafeCtor" safe traits that a type can implement in order to have minimal unsafe code in their implementations (even none if they do not require moving/offsetting pointers).
These traits expose the step (2) above as construct() -> (Self, Data) or copy_construct(from: &Self) -> (Self, Data) or move_construct(from: Pin<&mut Self>) -> (Self, Data). These trait functions provide the "constructed-from" object as a reference that can be directly used from safe rust. And they output Data that can be collected from the "constructed-from" object and represented in a useful way for initialization of self-references.
And they expose step (5) above as an initialize(this: &mut Self, d: Data) that consumes the constructor's side-channel Data to set up any self-referential state in the constructed object.
The other steps (1), (3) and (4) are performed in unsafe implementations in the unsafe CopyCtor and unsafe MoveCtor traits, or in a ctor::construct() function that replaces the direct use of ctor::from_placement_fn() from the being-constructed type.
@mcy has pointed out initial self-referential object construction could be done by exposing this 2-stage construction paradigm (i.e. steps (2) and (5)) through the constructing function such as ctor::new_with<T>(init: T, setup: FnOnce(Pin<&mut T>)), instead of using traits and requiring T : SafeCtor<Output = T>.
It presents a nice advantage: It would force the caller to construct the type T, which is reasonable for initial construction, and it even allows calling different constructors.
But it has a challenge as well, as copy- and move-constructors can not be implemented in the same way:
The copy and move requires boilerplate to execute before constructing the object T, such that a reference to the copied-from or moved-from object can be given to it. So the caller can not just construct a T themselves.
A data channel between the copied-from/moved-from object and the self-referential initialization step is required, which is provided by the Data passed between the traits' copy/move constructors and initialize.
Here is an example of a self-referential type that points into a buffer. When moving, the move-constructed type must be able to set up a new self-reference at the same offset (or in this case, the next offset).
use moveit::*;use std::marker::PhantomPinned;use std::pin::Pin;use std::ptr::null_mut;structCycle{num:[i32;10],ptr:*muti32,_pin:PhantomPinned,}implCycle{}structCycleInit{offset:usize,}impl ctor::SafeCtorforCycle{typeOutput = Self;typeInitializeData = CycleInit;fnconstruct() -> (Self,CycleInit){(Cycle{num:[9,8,7,6,5,4,3,2,1,0],ptr:null_mut(),// Set by initialize()._pin:PhantomPinned,},CycleInit{offset:0},)}fninitialize(this:&mutSelf,init:CycleInit){
this.ptr = unsafe{(&mut this.num).as_mut_ptr().add(init.offset)}}}impl ctor::SafeMoveCtorforCycle{fnmove_construct(other:Pin<&mutSelf>) -> (Self,CycleInit){(Cycle{num: other.num,ptr:null_mut(),// Set by initialize()._pin:PhantomPinned,},CycleInit{// Move-constructing will move the `ptr` to the right one// so we can observe it.offset:1 + unsafe{ other.ptr.offset_from(&other.num[0])asusize},},)}}
The only unsafe code here is that which deals with the pointer into num. The same self-referential initialization is shared between each of construction, copy-construction, and move-construction. It's not obvious if forcing that behaviour to be shared is undesirable.
Here are the traits, which would appear in ctor.rs, as they provide generic implementations of CopyCtor and MoveCtor for any type implementing SafeCopyCtor or SafeMoveCtor respectively. The SafeCtor trait provides the self-referential setup step (2nd stage of construction) used in all construction paths, but only provides a single type-construction function without any parameters. A way to provide multiple constructors is needed.
Using the safe traits looks much like using the existing ones, except that T::new() (or similar) is not called directly.
fnmain(){moveit!(let x = new ctor::construct::<Cycle>());moveit!(let y = new ctor::mov(x));println!("{}", unsafe{ *y.ptr });}
Perhaps SafeCtor could be renamed to SelfReferenceSetup, the contruct() trait functions removed, and the ctor::construct() function modified to receive a T by value instead. That would handle both multiple construction paths, while still sharing the setup of self-references.
The text was updated successfully, but these errors were encountered:
The new docs for the front matter of the crate demonstrate this pattern on Unmoveable. I need to page in most of this design work at some point, but this should take care of almost all of the easy cases.
(Oh, and FYI: all of the names in the crate have changed to reflect what I used in the RustConf talk, since I felt those were more succinct.)
Sign up for freeto subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Currently to write a constructor/move-constructor/copy-constructor, you have to deal with a lot of unsafe code and boilerplate steps. Each constructor has to do:
The only steps that the type T actually cares about and wants to define are (2) and (5), but today it has to write the whole algorithm in each constructor/move-constructor/copy-constructor. Like so:
I have implemented a set of "SafeCtor" safe traits that a type can implement in order to have minimal unsafe code in their implementations (even none if they do not require moving/offsetting pointers).
These traits expose the step (2) above as
construct() -> (Self, Data)
orcopy_construct(from: &Self) -> (Self, Data)
ormove_construct(from: Pin<&mut Self>) -> (Self, Data)
. These trait functions provide the "constructed-from" object as a reference that can be directly used from safe rust. And they output Data that can be collected from the "constructed-from" object and represented in a useful way for initialization of self-references.And they expose step (5) above as an
initialize(this: &mut Self, d: Data)
that consumes the constructor's side-channel Data to set up any self-referential state in the constructed object.The other steps (1), (3) and (4) are performed in unsafe implementations in the
unsafe CopyCtor
andunsafe MoveCtor
traits, or in actor::construct()
function that replaces the direct use ofctor::from_placement_fn()
from the being-constructed type.@mcy has pointed out initial self-referential object construction could be done by exposing this 2-stage construction paradigm (i.e. steps (2) and (5)) through the constructing function such as
ctor::new_with<T>(init: T, setup: FnOnce(Pin<&mut T>))
, instead of using traits and requiringT : SafeCtor<Output = T>
.It presents a nice advantage: It would force the caller to construct the type T, which is reasonable for initial construction, and it even allows calling different constructors.
But it has a challenge as well, as copy- and move-constructors can not be implemented in the same way:
Here is an example of a self-referential type that points into a buffer. When moving, the move-constructed type must be able to set up a new self-reference at the same offset (or in this case, the next offset).
The only unsafe code here is that which deals with the pointer into
num
. The same self-referential initialization is shared between each of construction, copy-construction, and move-construction. It's not obvious if forcing that behaviour to be shared is undesirable.Here are the traits, which would appear in
ctor.rs
, as they provide generic implementations ofCopyCtor
andMoveCtor
for any type implementingSafeCopyCtor
orSafeMoveCtor
respectively. TheSafeCtor
trait provides the self-referential setup step (2nd stage of construction) used in all construction paths, but only provides a single type-construction function without any parameters. A way to provide multiple constructors is needed.Using the safe traits looks much like using the existing ones, except that
T::new()
(or similar) is not called directly.Perhaps
SafeCtor
could be renamed toSelfReferenceSetup
, thecontruct()
trait functions removed, and thector::construct()
function modified to receive aT
by value instead. That would handle both multiple construction paths, while still sharing the setup of self-references.The text was updated successfully, but these errors were encountered: