Skip to content

Converting to VarULE #1179

@sffc

Description

@sffc

Part of #1082

Converting From VarULE

From VarULE is fairly easy. From works fine when the target type allocates memory. If we want to support cases where the target type borrows from the VarULE, FromVarULE can work (playground):

trait FromVarULE<'a, U: ?Sized>: 'a {
    fn from_var_ule(ule: &'a U) -> Self;
}

impl FromVarULE<'_, str> for String {
    fn from_var_ule(ule: &str) -> Self {
        ule.to_string()
    }
}

impl<'a> FromVarULE<'a, str> for Cow<'a, str> {
    fn from_var_ule(ule: &'a str) -> Self {
        Cow::Borrowed(ule)
    }
}

Converting To VarULE

The other direction is more complicated because VarULE is unsized.

Box-based solutions

A simple trait would be something like the following, but we should not consider it because it requires allocation (playground):

trait IntoBoxedVarULE<U: ?Sized> {
    fn into_var_ule(&self) -> Box<U>;
}

impl IntoBoxedVarULE<str> for String {
    fn into_var_ule(&self) -> Box<str> {
        self.clone().into_boxed_str()
    }
}

impl IntoBoxedVarULE<str> for Cow<'_, str> {
    fn into_var_ule(&self) -> Box<str> {
        self.to_string().into_boxed_str()
    }
}

An interesting option would be a Box-based solution that uses a custom allocator such that it does not need to actually allocate memory. A trait definition such as the following would be cool, but I can't get this to compile because MaybeUninit requires a Sized type (which seems silly to me, since the main reason you allocate in Rust is for unsized things):

// error[E0277]: the size for values of type `U` cannot be known at compilation time
trait IntoAllocBoxedVarULE<U: ?Sized> {
    fn into_var_ule<A: Allocator>(
        &self,
        allocf: impl FnOnce(usize) -> Box<MaybeUninit<U>, A>
    ) -> Box<U, A>;
}

A workaround would be to pass an uninitialized buffer into the function. This results in a safe trait (I think), although implementing it requires unsafe code and unstable features (playground):

trait IntoAllocBufferVarULE<U: ?Sized> {
    fn into_var_ule<A: Allocator>(
        &self,
        allocf: impl FnOnce(usize) -> Box<[MaybeUninit<u8>], A>
    ) -> Box<U, A>;
}

Buffer-based solutions

@Manishearth proposed the following in #1173:

pub unsafe trait EncodeAsVarULE {
    type VarULE: VarULE + ?Sized;
    fn encode_var_ule<R>(&self, cb: impl FnOnce(&[&[u8]]) -> R) -> R;
}

unsafe impl EncodeAsVarULE for String {
    type VarULE = str;
    fn encode_var_ule<R>(&self, cb: impl FnOnce(&[&[u8]]) -> R) -> R {
        cb(&[self.as_bytes()])
    }
}

The advantage of this type of approach, which I'll call a "buffer-based approach" since it returns a [u8] instead of a U, is that it is easy to reason about and doesn't require unstable features or additional memory allocations. The disadvantage is that it requires the trait to be unsafe.

An issue with the above trait is that it requires creating the &[&[u8]], which is easy if the outer slice has only a single element, but may require allocating if multiple slices are required (such as a Vec<String>). An alternative would be something such as:

trait AppendableBuffer {
    fn push_bytes(&mut self, bytes: &[u8]);
}

unsafe trait AppendAsVarULE<U: ?Sized> {
    fn encode_var_ule<R, A: AppendableBuffer>(
        &self,
        appendable: &mut A
    ) -> R;
}

The above solution could also use std::io::Write, but that trait requires the std feature.

We could also pre-allocate the whole buffer:

unsafe trait WriteToBufferAsVarULE<U: ?Sized> {
    fn encode_var_ule<'a, E>(
        &self,
        get_buffer: impl FnOnce(usize) -> &'a mut [u8]
    ) -> Result<&'a U, E>;
}

An advantage of WriteToBufferAsVarULE is that the safety constraint is a bit simpler: the only requirement is that the pointer returned by encode_var_ule is equal in in location, size, and alignment to the pointer returned by get_buffer. Validating that the buffer is a valid VarULE is done internally in the function.

CC @zbraniecki

Metadata

Metadata

Assignees

Labels

C-data-infraComponent: provider, datagen, fallback, adaptersS-smallSize: One afternoon (small bug fix or enhancement)T-coreType: Required functionality

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions