Reputation: 35186
I have a static array type that allows you to create multiple read only "view" slices into the data it holds; but on Drop
it assert!
s that there are no "hanging" views that reference data that no longer exists.
It seems like you could do this by adding a heap-allocated integer to the structure, like in the unsafe guide; something like:
extern crate libc;
use libc::{c_void, calloc, free, size_t};
use std::mem::size_of;
struct Foo {
count: *mut i32,
value: i32,
}
impl Foo {
fn new(parent: Option<&mut Foo>) -> Foo {
match parent {
Some(p) => {
unsafe {
let tmp = &mut *p.count;
*tmp += 1;
println!("Created a new record, the count is now: {}", *tmp);
}
return Foo {
value: 0,
count: p.count,
};
}
None => unsafe {
let counter = calloc(size_of::<i32> as size_t, 1 as size_t) as *mut i32;
println!("counter record: {}", *counter);
return Foo {
value: 0,
count: counter,
};
},
}
}
fn count(&self) -> i32 {
unsafe {
return *self.count;
}
}
}
Where the drop
implementation updates the counter:
impl Drop for Foo {
fn drop(&mut self) {
unsafe {
let tmp = &mut *self.count;
*tmp -= 1;
println!("Dropped a record, the count is now: {}", *tmp);
if *tmp == -1 {
println!("counter record: {}", *self.count);
free(self.count as *mut c_void);
println!("The final record was dropped");
}
}
}
}
This code works fine, the test:
fn main() {
let mut parent = Foo::new(None);
{
let child1: Foo;
let child2: Foo;
let child3: Foo;
let child4: Foo;
let child5: Foo;
{ child1 = Foo::new(Some(&mut parent)); }
{ child2 = Foo::new(Some(&mut parent)); }
{ child3 = Foo::new(Some(&mut parent)); }
{ child4 = Foo::new(Some(&mut parent)); }
{ child5 = Foo::new(Some(&mut parent)); }
assert!(parent.count() == 5);
}
assert!(parent.count() == 0);
}
Yields:
counter record: 0x7f909f7fc010
Created a new record, the count is now: 1
Created a new record, the count is now: 2
Created a new record, the count is now: 3
Created a new record, the count is now: 4
Created a new record, the count is now: 5
Dropped a record, the count is now: 4
Dropped a record, the count is now: 3
Dropped a record, the count is now: 2
Dropped a record, the count is now: 1
Dropped a record, the count is now: 0
Dropped a record, the count is now: -1
counter record: 0x7f909f7fc010
The final record was dropped
Is this actually safe?
The unsafe guide says:
Raw pointers have much fewer guarantees than other pointer types offered by the Rust language and libraries. For example, they
... - are considered sendable (if their contents is considered sendable), so the compiler offers no assistance with ensuring their use is thread-safe; for example, one can concurrently access a
*mut int
from two threads without synchronization.
However...
Going the opposite direction, from
*const
to a reference&
, is not safe. A&T
is always valid, and so, at a minimum, the raw pointer*const T
has to be a valid to a valid instance of typeT
. Furthermore, the resulting pointer must satisfy the aliasing and mutability laws of references.
It looks like although the example above 'works', it's actually undefined behavior. In converting the *const i32
to a an &i32
to increment and decrement the reference count, the &i32
must satisfy the pointer aliasing rules; which it will not, as multiple Foo
s can be dropped at the same time (potentially, although not specifically in the example above).
How do you "correctly" implement this sort of behavior in a way that doesn't result in undefined behavior?
Upvotes: 1
Views: 475
Reputation: 431579
In addition to not being threadsafe, you need to use an UnsafeCell
:
The
UnsafeCell<T>
type is the only legal way to obtain aliasable data that is considered mutable
use std::cell::UnsafeCell;
struct Foo {
counter: UnsafeCell<*mut i32>,
}
impl Foo {
fn new(parent: Option<&mut Foo>) -> Foo {
unsafe {
match parent {
Some(p) => {
let counter = *p.counter.get();
*counter += 1;
println!("Created a new record, the count is now: {}", *counter);
Foo {
counter: UnsafeCell::new(counter),
}
}
None => {
let counter = Box::into_raw(Box::new(0));
println!("counter record: {}", *counter);
Foo {
counter: UnsafeCell::new(counter),
}
}
}
}
}
fn count(&self) -> i32 {
unsafe { **self.counter.get() }
}
}
impl Drop for Foo {
fn drop(&mut self) {
unsafe {
let counter = *self.counter.get();
*counter -= 1;
println!("Dropped a record, the count is now: {}", *counter);
if *counter == -1 {
println!("counter record: {}", *counter);
Box::from_raw(self.counter.get());
println!("The final record was dropped");
}
}
}
}
fn main() {
let mut parent = Foo::new(None);
{
let _child1 = Foo::new(Some(&mut parent));
let _child2 = Foo::new(Some(&mut parent));
let _child3 = Foo::new(Some(&mut parent));
let _child4 = Foo::new(Some(&mut parent));
let _child5 = Foo::new(Some(&mut parent));
assert!(parent.count() == 5);
}
assert!(parent.count() == 0);
}
Upvotes: 1
Reputation: 102216
multiple
Foo
s can be dropped at the same time
No, they can't, at least, not at exactly the same time because the destructors run in sequence. As long as the Foo
objects stay to a single thread, there's never a point in time where there can be multiple &mut
borrows to .count
. Both places with &mut
borrows are very restricted, and there's no other Foo
operations that can happen while the count
is being manipulated.
However, the key point is "stay to a single thread". If you had objects in multiple threads, you could have two Foo
operations happening at once: create two Foo
s, and pass one to another thread, now each thread can do whatever it wants, whenever it wants, but they're all pointing at the same data. This is problematic (and undefined behaviour) for two reasons:
&mut
s; the two threads could each be executing one of the places with the &mut
borrow at the same point.One way to solve this is to prevent the Foo
from being given to another thread, using marker traits. In particular, Send
and Sync
are not automatically implemented for raw pointers, so the default behavior is what you want here.
Another way to solve it, but allow for sharing/sending between threads is changing count
to store an AtomicIsize
, to avoid data races. You will need to be careful about using the correct operations to ensure threadsafety in the destructor, or else you may deallocate while there are still other references around.
Upvotes: 3