-
Notifications
You must be signed in to change notification settings - Fork 94
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
First draft of a sound ShallowCopy #81
Conversation
Okay, so, there are a couple of concerns here. I'll try to write a little bit about each one in the hopes that it sparks ideas in others. At the core of
The way this works in
Now, there are at least two problems with this approach:
Unfortunately, this has pretty far-reaching implications for First, it changes the definition of pub trait ShallowCopy {
type SplitType;
type Target: ?Sized;
fn split(self) -> (Self::SplitType, Self::SplitType);
unsafe fn deref(split: &Self::SplitType) -> &Self::Target;
unsafe fn drop(split: &mut Self::SplitType);
fn deref_self(&self) -> &Self::Target;
} This is a major change, but it's the cleanest I could come up with. For a type to be shallow-copiable, it must be possible to turn it into "raw" (and crucially, aliasable) parts — that's Since enum MaybeShallowCopied<T>
where
T: ShallowCopy,
{
Owned(T),
Copied(T::SplitType),
} Given something of that type from the oplog, we now need a way to insert the aliased struct ForwardThroughShallowCopy<T>
where
T: ShallowCopy,
{
split: T::SplitType,
} Now we just have to implement all the necessary traits on impl<T> MaybeShallowCopied<T> where T: ShallowCopy {
unsafe fn shallow_copy_first(&mut self) -> ForwardThroughShallowCopy<T> { ... }
unsafe fn shallow_copy_second(self) -> ForwardThroughShallowCopy<T> { ... }
} They must be For the final trick, we then So far so good. Unfortunately, wrapper types get painful pretty quickly, because we cannot quite make them transparent. In particular, we run into problems when the user provides a value of type This gets worse when we take into account the fact that The first problem is fixable by making enum ForwardThroughShallowCopy<T>
where
T: ShallowCopy,
{
Split(T::SplitType),
Ref(*const T::Target),
} The Okay, where does that leave us, beyond making a lot of types much uglier? Well, there are three major downsides to this design. First, there is a decent amount more Imagine that you wanted to implement impl<T> ShallowCopy for Option<T> where T: ShallowCopy {
type SplitType = Option<T::SplitType>;
type Target = Option<T::Target>;
fn split(self) -> (Self::SplitType, Self::SplitType) {
if let Some(this) = self {
let (a, b) = this.split();
(Some(a), Some(b))
} else {
(None, None)
}
}
unsafe fn drop(split: &mut Self::SplitType) {
if let Some(split) = split {
T::drop(split);
}
}
// So far so good — now we just need deref...
unsafe fn deref(split: &Self::SplitType) -> &Self::Target {
// We need to return an `&Option<T>` given an `&Option<T::SplitType>`. Uh oh...
// We can call `T::deref` on the `&T::SplitType` inside the `Option`, but that gives
// us an `Option<&T>`, not a `&Option<T>`.
panic!();
}
} A similar issue arises for any compound type. The basic problem is that the impl<T> ShallowCopy for Option<T> where T: ShallowCopy {
type Target<'a> = Option<&'a T::Target>;
fn deref<'a>(&'a Self::SplitType) -> Self::Target<'a> { ... }
} But, that doesn't exist at the moment. So, what do we do? I honestly don't know. This is the way I found to make Thoughts welcome. |
Here's another fun observation: since this version of |
7418172
to
6b542fc
Compare
I think this is not true. A
Isn't this what the |
@digama0 Ah, I think maybe I phrased myself poorly. The reason
Well, so, sort of. |
Oh, I just got to the part about the blanket implementation. But that sounds surprising to me. I just tried the following and it worked: use std::borrow::Borrow;
struct MyWrapper<T>(T);
impl<T> Borrow<T> for MyWrapper<T> {
fn borrow(&self) -> &T {
&self.0
}
} |
@digama0 Yes, you're right, as of Rust 1.41.0 it's possible to write a "covered" implementation of impl<T> Borrow<T::Target> for ForwardThroughShallowCopy<T>
where
T: ShallowCopy,
{
fn borrow(&self) -> &T {
self.as_ref()
}
} which gives
|
Ah right, because it could be that an implementation of Regarding the problem with |
Thinking about this some more, I think this isn't necessary. I'm working from the model of evmap as of your latest stream, where the concurrency stuff is in a separate Suppose you have a If we assume that pub unsafe trait ShallowCopy {
type SplitType;
fn split(self) -> (Self::SplitType, Self::SplitType);
} which we can implement as: unsafe impl<K, V> ShallowCopy for HashMap<K, Box<V>> {
type SplitType = HashMap<K, *mut V>;
fn split(self) -> (Self::SplitType, Self::SplitType) {
assert!(self.is_empty());
(HashMap::new(), HashMap::new())
}
} We could support nonempty maps in For a few more example implementations: unsafe impl<T> ShallowCopy for Box<T> {
type SplitType = *mut T;
fn split(self) -> (Self::SplitType, Self::SplitType) {
let p = Box::into_raw(self);
(p, p)
}
}
unsafe impl<T: ShallowCopy> ShallowCopy for Option<T> {
type SplitType = Option<T::SplitType>;
fn split(self) -> (Self::SplitType, Self::SplitType) {
match self {
None => (None, None),
Some(val) => {
let (left, right) = val.split();
(Some(left), Some(right))
}
}
}
}
struct SplitRawVec<T> {
ptr: NonNull<T>,
cap: usize,
}
impl<T> Clone for SplitRawVec<T> {
fn clone(&self) -> Self {
Self { ptr: self.ptr, cap: self.cap }
}
}
impl<T> Copy for SplitRawVec<T> {}
pub struct SplitVec<T> {
buf: SplitRawVec<T>,
len: usize,
}
impl<T> Clone for SplitVec<T> {
fn clone(&self) -> Self {
Self { buf: self.buf, len: self.len }
}
}
impl<T> Copy for SplitVec<T> {}
unsafe impl<T: ShallowCopy> ShallowCopy for Vec<T> {
type SplitType = SplitVec<T>;
fn split(self) -> (Self::SplitType, Self::SplitType) {
let split: SplitVec<T> = unsafe { std::mem::transmute(self) };
(split, split)
}
} For The oplog doesn't need to store an enum, although it logically is so; instead you just always use the Now in the functions like unsafe fn deref<T: ShallowCopy>(t: &T::SplitType) -> &T {
std::mem::transmute(t)
}
unsafe fn deref_mut<T: ShallowCopy>(t: &mut T::SplitType) -> &mut T {
std::mem::transmute(t)
} which are safe by the contract of Finally, to avoid the extra unsafe impl<K: Clone + Hash + Eq, V: ShallowCopy> ShallowCopy for HashMap<K, V> {
type SplitType = HashMap<K, V::SplitType>;
fn split(self) -> (Self::SplitType, Self::SplitType) {
let mut left = HashMap::new();
let mut right = HashMap::new();
for (k, v) in self {
let (v1, v2) = v.split();
left.insert(k.clone(), v1);
right.insert(k, v2);
}
(left, right)
}
} It's debatable whether I think that covers all the use cases in your post, but let me know if I missed something since I'm mostly just projecting what the code change would look like. |
This is trying to address the unsoundness that arises from the current version of `ShallowCopy` (see #74). In the process, it also deals with the fact that casting between `Inner<.., T, ..>` and `Inner<.., ManuallyDrop<T>, ..>` is likely not sound (rust-lang/unsafe-code-guidelines#35 (comment)). It does not yet work.
Here is a new take on fixing the soundness bugs with `ShallowCopy`: get rid of `ShallowCopy` entirely and instead store the aliased `T` in a `MaybeUninit` that we keep two copies of and access unsafely.
6b542fc
to
928d94e
Compare
Codecov Report
@@ Coverage Diff @@
## master #81 +/- ##
==========================================
- Coverage 79.39% 79.16% -0.23%
==========================================
Files 16 16
Lines 1286 1296 +10
==========================================
+ Hits 1021 1026 +5
- Misses 265 270 +5
Continue to review full report at Codecov.
|
@digama0 A simple layout assertion doesn't work, because @jonhoo One approach you could use without changing the interface, and possibly removing the |
@RustyYato Hehe, that's exactly the same thought I had last night. Take a look at the latest commits, and in particular the new |
I'm going to close this in favor of #83. |
This is trying to address the unsoundness that arises from the current version of
ShallowCopy
(see #74). In the process, it also deals with the fact that casting betweenInner<.., T, ..>
andInner<.., ManuallyDrop<T>, ..>
is likely not sound (rust-lang/unsafe-code-guidelines#35 (comment)).It does not yet work. More notes to come.
This change is