How to make an object always outlive a reference

Hi, I am creating a component to hold and allocate data in a contiguous memory array, kinda like an arena allocator for single typed data with some additional features.
The main idea is that memory is pre-allocated and other components can just overwrite this memory block and get back a reference counted pointer for it (Similar to Arc).
I currently have all this working, having implemented the allocator and also an Arc structure (because i didn't find a way to use Arc without allocating memory). Although i am a bit concerned about lifetimes of the pointers, as the pointer can never outlive the underlaying memory they are pointing to.

Below is a stripped down version of what i have, that is mostly based on the implementation of Arc.

struct Element<'a, T: ?Sized + Default> {
    ref_count: AtomicUsize,
    el: T,
    
    mark: PhantomData<&'a ()>
}
impl<'a, T: ?Sized + Default> Element<'a, T>{
    pub fn set(&mut self,val: T) -> ElementAtomicRef<'a,T>{
        let res = self.ref_count.load(Ordering::Relaxed);
        if res == 0{
            self.el = val;
        }else{
            panic!("This element cannot be set, as there are still references to it");
        }
        return ElementAtomicRef::new(self);
    }

}

pub struct ElementAtomicRef<'a, T: ?Sized + Default>{
    ptr: NonNull<Element<'a, T>>
}

impl<'a, T: ?Sized + Default> ElementAtomicRef<'a, T>{
    fn new(el : &mut Element<'a, T>) -> ElementAtomicRef<'a, T>{
        let old_rc = el.ref_count.fetch_add(1, Ordering::Relaxed);

        if old_rc >= isize::MAX as usize {
            std::process::abort();
        }
        ElementAtomicRef{
            ptr: NonNull::new(el as *mut _).unwrap()
        }
    }
}

impl<'a, T: ?Sized + Default> Clone for ElementAtomicRef<'a,T> {
    fn clone(&self) -> ElementAtomicRef<'a,T> {
        let inner = unsafe { self.ptr.as_ref() };
        let old_rc = inner.ref_count.fetch_add(1, Ordering::Relaxed);

        if old_rc >= isize::MAX as usize {
            std::process::abort();
        }

        Self {
            ptr: self.ptr
        }
    }
}

pub struct DatabaseMemory<'a, T: ?Sized + Default>{
    object_memory: [Element<'a, T>]>

    pub fn add(&self, element: T) -> Option<ElementAtomicRef<'a, T>>{
        let idx; // Get available index from some place

        if let Some(idx) = idx{
            let memory_element = self.object_memory[idx as usize];
            Some(memory_element.set(element))
        }else{
            None
        }
    }
}

Currently, i have the lifetime of the atomic references/pointers tied to the underlaying structure that holds the data, but I am afraid this might not be enough as the memory could possibly get dropped before the pointer. My questions are:

  • Is my assumption correct and the current implementation can lead to undefined behavior, due dangling references?
  • Is there a way to ensure the lifetime of the element always outlives the pointer? (I read about subtyping and variance, even tried to implement it without success) If so, how can I do it?

In the case where the problem exists and cannot be solved with lifetimes the only possible way that I can think of is to use the static lifetime for the underlaying memory structure.

This is unsound in two ways:

  1. The DatabaseMemory stores the elements, but the lifetime 'a is not the loan of DatabaseMemory, so it won't stop drop(database), and all the elements can be destroyed while 'a is still "valid".

  2. There is absolutely no limit on what 'a is. It's possible to create DatabaseMemory<'static> and completely ignore the lifetimes.

In your current design, the lifetimes are useless. You can remove them completely, as they are not, and can't be part of the safety.

You're trying to emulate Arc, but Arc is safe because it owns the allocation, and will keep it alive even after the "original" Arc instance is destroyed. In your case ElementAtomicRef does not keep the element's memory valid when DatabaseMemory is destroyed, so it doesn't behave like Arc.

  • Maybe this could be made safe if Drop of DatabaseMemory checked refcounts of all elements, and abort() the process if any refcount != 0. There's no other way to handle this error than to immediately crash the whole program. Even panic would allow use-after free, because unwinding code can access the elements.

  • Or don't use atomic references at all. Make it a memory pool/arena, and give out &T. If the loans ensure safety, you never need anything more.

  • Or make every ElementAtomicRef element hold a Arc<Database> reference.

5 Likes

If you store the element array on the heap instead of inline, it might be feasible to leak it instead of aborting the program. It's still bad, but gives a different set of tradeoffs: You're replacing a noisy, easy-to-detect, and fatal error with one that will silently let the program continue operating in a degraded state.

4 Likes

Good point!

Or don't use atomic references at all. Make it a memory pool/arena, and give out &T. If the loans ensure safety, you never need anything more.

There are other requirements that I have, like storing a reference in other databases as well as moving it across threads and copying it. Using references i am not sure if this would still be possible, it probably would be a lifetime hell :sweat_smile: But definitely something to investigate.

Or make every ElementAtomicRef element hold a Arc<Database> reference.

Didn't thought of this one... This might also be a viable, although it would be one more pointer to be moved around the entire system.

I think i am gonna make this a completely private module and just publicly allow access to a static global instance of DatabaseMemory, for now this fits the requirements.

Either way, I will be having a bit more investigation on this options and see what fits best. Thanks for the answers.

This doesn't sound right. If you can't make these lifetimes actually understood and enforced by the borrow checker, then your API is unsafe, and crates a loophole that brakes Rust's safety guarantees. It may also cause UB when the actual durations of loans aren't tracked.

OTOH if you can make the lifetimes real and actually describe lifetimes of objects in the database, then this is a guarantee much stronger, and always precisely enforced, without need for reference counting.

Note that temporary loans can be copied and can be used across threads. They need a guarantee that the thread won't outlive the temporary stack the references are borrowing from, and apis like scoped threads can do that. Rayon can do it too.

2 Likes

Something I didn't show in the previous examples, is a secondary deque in the DatabaseMemory struct that keeps track of which element indexes can be overwritten. Whenever an Element refcount drops to 0 (using the Drop trait for the Element struct), i just append the current index to the deque of available indexes.
Didn't mention this before, just keep things simples and short.

With this in mind, I went back to thinking about your proposal about hand out borrows like you suggested, i don't think there is a way to keep track of when all the borrows have been dropped without somekind of refcount to ensure i place the "freed" memory index into the deque of available indexes.

At the beginning I even thought about using the allocator-api and reuse Rc/Arc, but this is only available in nightly and I didn't found a way to enforce the compiler to only be able to use that allocator for my particular object.

Indeed, there's no way to observe when reference/loan stops being used. Arenas relying on lifetimes have to be append-only.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.