Idiomatic method of caching items

Hi there!

I have an interface which helps me interact with a list in a foreign language. I want to create an idiomatic binding to this in rust, or at least try.

The thing is, is that since I cannot hand out references to items in this foreign list, my options are to either copy the list, hand out references to that, and then update it, or to "cache" acquired items.

I want to do the latter, so I have the following idea:

pub struct List<T> {
    // Implementation details like a handle, etc.
}

impl<T: ListItem> List<T> {
    //We want to avoid calling this multiple times
    //for the same item since we can do better than
    //that. 
    pub fn get_at(&self, idx: usize) -> T {
        //
    }

    pub fn get_view(&'_ self) -> ListView<'_, T> {
        //
    }

    pub fn set_at(&mut self, idx: usize) -> T {
        //
    }
    
    pub fn get_view_mut(&'_ mut self) -> ListViewMut<'_, T> {
        //
    }
}

pub struct ListView<'a, T: ListItem> {
    list: &'a List<T>,
    len: usize, // Used in the construction of this,
                // and to facilitate panics on rust's
                // side instead of over an ffi. 
    cached_items: Vec<UnsafeCell<Option<T>>>,
}
impl<'a, T: ListItem> Index<usize> for ListView<'a, T> {
    type Output = T;
    fn index(&self, idx: usize) -> &T {
        //SAFETY:
        // Make sure that the list's items are never changed after
        // handing out a reference to them. Since references are
        // only ever handed out when there is a `Some` variant in
        // self.cached_items[idx], there is always an item there,
        // and the values at those locations are only ever modified
        // when the spot is populated by a `None`. 
        unsafe {
            let item = &self.cached_items[idx];
            let item = item.get();
            if (*item).is_none() {
                *item = Some(self.list.get_at(idx));
            }
            (*(item as *const Option<T>)).as_ref().unwrap()
        }
    }
}

enum Item<T> {
    Read(T),
    PossiblyModified(T),
}

impl<T> Item<T> {
    fn get_ref(&self) -> &T {
        match self { /**/ }
    }
    fn make_mut(&mut self) -> &mut T {
        match self { 
            Item::Read(x) => { 
                /* Go about swapping self to be Item::PossiblyModified */
            },
            Item::PossiblyModified(x) => x,
        }
    }
}

pub struct ListViewMut<'a, T: ListItem> {
    list: &'a mut List<T>,
    len: usize,
    cached_items: Vec<UnsafeCell<Option<Item<T>>>>,
}

impl<'a, T: ListItem> Index<usize> for ListViewMut<'a, T> {
    type Output = T;
    fn index(&self, idx: usize) -> &T {
        //SAFETY:
        // Since rust prevents us from indexing mutably _and_
        // [im]mutably at the same time, we don't have to worry about
        // overwriting a preexisting entry. 
        // For more, please see `<ListView<'a, T> as Index<usize>>::index`'s
        // unsafety note.
        unsafe {
            let item = &self.cached_items[idx];
            let item = item.get();
            if (*item).is_none() {
                *item = Some(Item::Read(self.list.get_at(idx)));
            }
            (*(item as *const Option<Item<T>>)).as_ref().unwrap().get_ref()
        }
    }
}
impl<'a, T: ListItem> IndexMut<usize> for ListViewMut<'a, T> {
    fn index_mut(&self, idx: usize) -> &mut T {
        //SAFETY:
        // Make sure that the list's item states are never changed after
        // handing out a mutable reference to them. Since the references
        // have a lifetime attached to them, it is effectively impossible
        // for this to be called in a potentially conflicting way. 
        // For more: See `<ListView<'a, T> as Index<usize>>::index`'s
        // unsafety note. 
        unsafe {
            let item = &self.cached_items[idx];
            let item = item.get();
            if (*item).is_none() {
                *item = Some(Item::PossiblyModified(self.list.get_at(idx)));
            }
            (*item).as_mut().unwrap().make_mut()
        }
    }
}

Playground

I apologize for the wall of code, but in reality the important stuff is marked under a SAFETY comment.

I am 90% sure that this is fine, but would like to make sure that it is.

Thanks!

Idiomatic way is to create zero cost and at the same time safe abstraction :slight_smile:
So the right answer depend on what C API you want to hide behind Rust API.

I'm wrapping the dart vm's native interface for native extensions. The list-related functions that the api provides are as follows:

extern "C" {
    pub fn Dart_IsList(object: Dart_Handle) -> bool;
    pub fn Dart_NewList(length: isize) -> Dart_Handle;
    pub fn Dart_NewListOf(element_type_id: Dart_CoreType_Id, length: isize) -> Dart_Handle;
    pub fn Dart_NewListOfType(element_type: Dart_Handle, length: isize) -> Dart_Handle;
    pub fn Dart_ListLength(list: Dart_Handle, length: *mut isize) -> Dart_Handle;
    pub fn Dart_ListGetAt(list: Dart_Handle, index: isize) -> Dart_Handle;
    pub fn Dart_ListGetRange(
        list: Dart_Handle,
        offset: isize,
        length: isize,
        result: *mut Dart_Handle,
    ) -> Dart_Handle;
    pub fn Dart_ListSetAt(list: Dart_Handle, index: isize, value: Dart_Handle) -> Dart_Handle;
}

And the ability to invoke a function (Which mind you is more than just methods and includes constructors, getters, setters, operators, and more).

I've wrapped them in a thin unidiomatic wrapper as follows:

// Has a better name in actual code
struct Handle {
    handle: ffi::Dart_Handle
}
impl Handle {
    pub fn is_list(&self) -> bool {
        unsafe {
            ffi::Dart_IsList(self.handle)
        }
    }
    pub fn new_list(length: usize) -> Result<Self, Error> {
        unsafe {
            Self::new(ffi::Dart_NewList(length as _)).get_error()
        }
    }
    pub fn new_list_of(length: usize, ty: ffi::Dart_CoreType_Id) -> Result<Self, Error> {
        unsafe {
            Self::new(ffi::Dart_NewListOf(ty, length as _)).get_error()
        }
    }
    pub fn new_list_of_self_as_type(&self, length: usize) -> Result<Self, Error> {
        unsafe {
            Self::new(ffi::Dart_NewListOfType(self.handle, length as _)).get_error()
        }
    }
    pub fn list_length(&self) -> Result<usize, Error> {
        unsafe {
            let mut result = MaybeUninit::<isize>::uninit();
            let error_handle = ffi::Dart_ListLength(self.handle, result.as_mut_ptr());
            Self::new(error_handle).get_error()?;
            Ok(result.assume_init() as usize)
        }
    }
    pub fn list_at(&self, index: usize) -> Result<Self, Error> {
        unsafe {
            Self::new(ffi::Dart_ListGetAt(self.handle, index as _)).get_error()
        }
    }
    pub fn list_get_range(&self, range: impl std::ops::RangeBounds<usize>) -> Result<Self, Error> {
        use std::ops::Bound::*;
        let start = match range.start_bound() {
            Included(x) => *x,
            Excluded(x) => *x + 1,
            Unbounded => 0
        };
        let end = match range.end_bound() {
            Included(x) => *x + 1,
            Excluded(x) => *x,
            Unbounded => self.list_length()?,
        };
        let len = end - start;
        unsafe {
            let mut result = MaybeUninit::<ffi::Dart_Handle>::uninit();
            let error_handle =
                ffi::Dart_ListGetRange(
                    self.handle,
                    start as isize,
                    len as isize,
                    result.as_mut_ptr()
                );
            Self::new(error_handle)
                .get_error()
                .and_then(|_| Self::new(result.assume_init()).get_error())
        }
    }
    pub fn list_set_at(&self, item: Self, index: usize) -> Result<(), Error> {
        unsafe {
            Self::new(ffi::Dart_ListSetAt(self.handle, index as _, item.handle))
                .get_error()
                .map(|_| ())
        }
    }
}

Which I am then trying to wrap once more in a List<T> struct which can more idiomatically manage this, instead of providing what is essentially a Any struct.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.