Dynamic borrowing: deferring borrowck to run time


#1

I would like to “disguise” a borrowed object as a 'static value and I’m willing to pay the cost at run time to check that it is being borrowed correctly.

  • I cannot use Arc/Rc because the object really belongs to someone else and I have no control over their API.
  • I wish to avoid using references like &'a T because it will be used as the associated types of a trait. Due to the lack of HKTs, the presence of lifetimes would cause a major ergonomic burden – I don’t want to write things like <X::Ref as MkRef<'a>>::Type all over the place.

To that end, I plan on using a wrapper similar to the code below. The idea is that I create a somewhat deceptive borrowed object without a lifetime, and trust the user to relinquish it when they are done. If they don’t, the program will simply abort (as opposed to panic, since another thread might still have it).

Does this seem sensible? Is the implementation sound? (especially with the Ordering thing which I often get confused about.)

Edit: BTW the code is not panic-safe. The if alive.load(…) check really ought to be done in a destructor to prevent this.

use std::process;
use std::ops::{Deref, DerefMut};
use std::sync::atomic::{AtomicBool, Ordering};

pub struct Borrowed<T> { value: *mut T, alive: *mut AtomicBool }

impl<T> Drop for Borrowed<T> {
    fn drop(&mut self) {
        unsafe { (*self.alive).store(false, Ordering::Release) }
    }
}

impl<T> Deref for Borrowed<T> {
    type Target = T;
    fn deref(&self) -> &Self::Target { unsafe { &*self.value } }
}

impl<T> DerefMut for Borrowed<T> {
    fn deref_mut(&mut self) -> &mut Self::Target { unsafe { &mut *self.value } }
}

pub fn with_ref<T, F, R>(value: &mut T, f: F) -> R
    where F: FnOnce(Borrowed<T>) -> R
{
    let mut alive = AtomicBool::new(true);
    let r = f(Borrowed { value, alive: &mut alive });
    if alive.load(Ordering::Acquire) {
        process::abort();
    }
    r
}

#2

That sounds very much like RefCell, but with data held outside Rust’s heap. Maybe try adapting RefCell's source code?


#3

One thing that’s a bit unclear is how Borrowed will be shared across threads. I’m assuming it’ll be via cloning, ala Arc? But then a single atomic bool isn’t sufficient to detect misuse - you’d need an atomic refcount. Or am I missing something? The code snippet has nothing indicating thread sharing besides the atomic bool and the original post mentioning threads.


#4

FYI, the ordering seems correct, since the main concern is ensuring that any stores to the underlying data made through the Borrowed<T>happen-before’ (i.e. are visible to, and in the case of stores, are overwritten by) any accesses that the caller makes after with_ref returns. That’s the guarantee provided by a release-acquire pair.

You probably want unsafe impl<T: Sync> Sync for Borrowed<T> and ditto for Send.


#5

Borrowed<T> isn’t clonable (its API is intended to be equivalent to &'static mut T). But you can send it to another thread or share it using Arc.


#6

Thanks for verifying and also the suggestion!


#7

Ah ok, understood.