rustscopelifetimeentity-component-systemrefcell

Pass borrowed ref cell value into function trait


I'm working on an ECS system, but in order to iterate across pairs of queries, I need to be able to fetch components only for a limited scope when executing a system. To do this, I'm trying to make a trait that can return direct references to components for a limited scope. I then use nested tuples to apply this trait to any combination of components. However, I can't to enforce the borrow with the compiler. I have two solutions, both break in different ways.

Solution 1

Playground link

trait ScopedFetch {
    type Result<'r>: 'r;
    
    fn fetch<'scope>(&'scope self, func: impl FnOnce(Self::Result<'scope>));
}

impl<T: 'static> ScopedFetch for &RefCell<T> {
    type Result<'r> = &'r T;
    
    fn fetch<'scope>(&'scope self, func: impl FnOnce(Self::Result<'scope>)) {
        let data = self.borrow();
        func(&data); // Error here
    }
}

impl<SF1: ScopedFetch, SF2: ScopedFetch> ScopedFetch for (SF1, SF2) {
    type Result<'r> = (SF1::Result<'r>, SF2::Result<'r>);
    
    fn fetch<'scope>(&'scope self, func: impl FnOnce(Self::Result<'scope>)) {
        let (sf1, sf2) = self;
        sf1.fetch(|data1| {
            sf2.fetch(|data2| {
                func((data1, data2));
            });
        });
    }
}
error[E0597]: `data` does not live long enough
  --> src/lib.rs:14:14
   |
12 |     fn fetch<'scope>(&'scope self, func: impl FnOnce(Self::Result<'scope>)) {
   |              ------ lifetime `'scope` defined here
13 |         let data = self.borrow();
   |             ---- binding `data` declared here
14 |         func(&data);
   |         -----^^^^^-
   |         |    |
   |         |    borrowed value does not live long enough
   |         argument requires that `data` is borrowed for `'scope`
15 |     }
   |     - `data` dropped here while still borrowed

Solution 2

I tried to generalize the function scope, but then my tuple implementation gives me trouble instead

Playground link

trait ScopedFetch {
    type Result<'r>: 'r;
    
    fn fetch(&self, func: impl FnOnce(Self::Result<'_>));
}

impl<T: 'static> ScopedFetch for &RefCell<T> {
    type Result<'r> = &'r T;
    
    fn fetch(&self, func: impl FnOnce(Self::Result<'_>)) {
        let data = self.borrow();
        func(&data);
    }
}

impl<SF1: ScopedFetch, SF2: ScopedFetch> ScopedFetch for (SF1, SF2) {
    type Result<'r> = (SF1::Result<'r>, SF2::Result<'r>);
    
    fn fetch(&self, func: impl FnOnce(Self::Result<'_>)) {
        let (sf1, sf2) = self;
        sf1.fetch(|data1| {
            sf2.fetch(|data2| {
                func((data1, data2)); // Error here
            });
        });
    }
}
error[E0521]: borrowed data escapes outside of closure
  --> src/lib.rs:25:17
   |
23 |         sf1.fetch(|data1| {
   |                    ----- `data1` declared here, outside of the closure body
24 |             sf2.fetch(|data2| {
   |                        ----- `data2` is a reference that is only valid in the closure body
25 |                 func((data1, data2));
   |                 ^^^^^^^^^^^^^^^^^^^^ `data2` escapes the closure body here

error[E0521]: borrowed data escapes outside of closure
  --> src/lib.rs:25:17
   |
23 |         sf1.fetch(|data1| {
   |                    -----
   |                    |
   |                    `data1` is a reference that is only valid in the closure body
   |                    has type `<SF1 as ScopedFetch>::Result<'1>`
24 |             sf2.fetch(|data2| {
25 |                 func((data1, data2));
   |                 ^^^^^^^^^^^^^^^^^^^^
   |                 |
   |                 `data1` escapes the closure body here
   |                 argument requires that `'1` must outlive `'static`

I feel like what I'm trying to do should be expressable. Solution 1 works with an UnsafeCell, which I eventually plan to do for speed, however I want to make this work safely first. The fact it works for one and not the other is a red flag the UnsafeCell version would be... well... unsafe.


Solution

  • The reborrow trick from kmdreko was almost right, you just have to reborrow both data1 and data2: (playground)

    use std::cell::RefCell;
    
    trait ScopedFetch {
        type Result<'r>: 'r;
        
        fn fetch<F>(&self, func: F) where F: for<'a> FnOnce(Self::Result<'a>);
        fn reborrow_result<'a: 'b, 'b>(result: Self::Result<'a>) -> Self::Result<'b>;
    }
    
    impl<T: 'static> ScopedFetch for &RefCell<T> {
        type Result<'r> = &'r T;
        
        fn fetch<F>(&self, func: F) where F: for<'a> FnOnce(Self::Result<'a>) {
            let data = self.borrow();
            func(&data);
        }
        fn reborrow_result<'a: 'b, 'b>(result: Self::Result<'a>) -> Self::Result<'b> {
            result
        }
    }
    
    impl<SF1: ScopedFetch, SF2: ScopedFetch> ScopedFetch for (SF1, SF2) {
        type Result<'r> = (SF1::Result<'r>, SF2::Result<'r>);
        
        fn fetch<F>(&self, func: F) where F: for<'a> FnOnce(Self::Result<'a>) {
            let (sf1, sf2) = self;
            sf1.fetch(|data1| {
                sf2.fetch(|data2| {
                    let data1 = SF1::reborrow_result(data1);
                    let data2 = SF2::reborrow_result(data2);
                    func((data1, data2));
                });
            });
        }
        fn reborrow_result<'a: 'b, 'b>(result: Self::Result<'a>) -> Self::Result<'b> {
            (SF1::reborrow_result(result.0), SF2::reborrow_result(result.1))
        }
    }