1
votes

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 type T. 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 Foos 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?

2
This code works fine — note that as of Rust 1.25, this code does not work fine and instead causes a segfault.Shepmaster

2 Answers

3
votes

multiple Foos 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 Foos, 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:

  1. aliasing &muts; the two threads could each be executing one of the places with the &mut borrow at the same point.
  2. data races; the two threads are updating the same piece of memory without synchronisation. (This is the important point, the previous rule is designed as a tool to prevent data races, among other things.)

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.

1
votes

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);
}