-
Notifications
You must be signed in to change notification settings - Fork 214
Description
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