From 671fa05f5135ecab8f03d7f2dc96915d0c6e64c7 Mon Sep 17 00:00:00 2001 From: Jason Francis Date: Sun, 6 Nov 2022 00:59:49 -0500 Subject: [PATCH] glib: allow GString to store small inline strings --- glib/src/gstring.rs | 138 +++++++++++++++++++++++++++----------------- 1 file changed, 85 insertions(+), 53 deletions(-) diff --git a/glib/src/gstring.rs b/glib/src/gstring.rs index 0e1e768e878a..a46a0d4d91c6 100644 --- a/glib/src/gstring.rs +++ b/glib/src/gstring.rs @@ -448,11 +448,16 @@ impl ToOwned for GStr { #[inline] fn to_owned(&self) -> Self::Owned { - if self.is_empty() { - return GString::default(); - } - // Always copy with the GLib allocator let b = self.as_bytes_with_nul(); + if self.len() < INLINE_LEN { + let mut data = <[u8; INLINE_LEN]>::default(); + let b = self.as_bytes(); + unsafe { data.get_unchecked_mut(..b.len()) }.copy_from_slice(b); + return GString(Inner::Inline { + len: self.len() as u8, + data, + }); + } let inner = unsafe { let copy = ffi::g_strndup(b.as_ptr() as *const c_char, b.len()); Inner::Foreign { @@ -603,12 +608,17 @@ const INLINE_LEN: usize = /// The constructors beginning with `from_utf8` `and `from_string` can also be used to further /// control how interior nul-bytes are handled. pub struct GString(Inner); + enum Inner { - Native(Option>), + Native(Box), Foreign { ptr: ptr::NonNull, len: usize, }, + Inline { + len: u8, + data: [u8; INLINE_LEN], + }, } unsafe impl Send for GString {} @@ -621,7 +631,10 @@ impl GString { /// Does not allocate. #[inline] pub fn new() -> Self { - Self(Inner::Native(None)) + Self(Inner::Inline { + len: 0, + data: Default::default(), + }) } // rustdoc-stripper-ignore-next /// Formats an [`Arguments`](std::fmt::Arguments) into a [`GString`]. @@ -685,11 +698,11 @@ impl GString { #[inline] pub unsafe fn from_utf8_unchecked(mut v: Vec) -> Self { if v.is_empty() { - Self(Inner::Native(None)) + Self::new() } else { v.reserve_exact(1); v.push(0); - Self(Inner::Native(Some(String::from_utf8_unchecked(v).into()))) + Self(Inner::Native(String::from_utf8_unchecked(v).into())) } } // rustdoc-stripper-ignore-next @@ -705,9 +718,9 @@ impl GString { return Err(GStringNoTrailingNulError(s.into_bytes()).into()); } if s.len() == 1 { - Ok(Self(Inner::Native(None))) + Ok(Self::new()) } else { - Ok(Self(Inner::Native(Some(s.into())))) + Ok(Self(Inner::Native(s.into()))) } } // rustdoc-stripper-ignore-next @@ -742,9 +755,9 @@ impl GString { String::from_utf8_unchecked(v) }; if s.len() == 1 { - Self(Inner::Native(None)) + Self::new() } else { - Self(Inner::Native(Some(s.into()))) + Self(Inner::Native(s.into())) } } // rustdoc-stripper-ignore-next @@ -760,14 +773,14 @@ impl GString { return Err(GStringNoTrailingNulError(bytes).into()); }; if nul_pos == 0 { - Ok(Self(Inner::Native(None))) + Ok(Self::new()) } else { if let Err(e) = std::str::from_utf8(unsafe { bytes.get_unchecked(..nul_pos) }) { return Err(GStringUtf8Error(bytes, e).into()); } bytes.truncate(nul_pos + 1); let s = unsafe { String::from_utf8_unchecked(bytes) }; - Ok(Self(Inner::Native(Some(s.into())))) + Ok(Self(Inner::Native(s.into()))) } } // rustdoc-stripper-ignore-next @@ -791,11 +804,11 @@ impl GString { #[inline] pub fn from_string_unchecked(mut s: String) -> Self { if s.is_empty() { - Self(Inner::Native(None)) + Self::new() } else { s.reserve_exact(1); s.push('\0'); - Self(Inner::Native(Some(s.into()))) + Self(Inner::Native(s.into())) } } // rustdoc-stripper-ignore-next @@ -830,9 +843,9 @@ impl GString { pub fn as_str(&self) -> &str { unsafe { let (ptr, len) = match self.0 { - Inner::Native(None) => (ptr::null(), 0), - Inner::Native(Some(ref s)) => (s.as_ptr() as *const u8, s.len() - 1), + Inner::Native(ref s) => (s.as_ptr() as *const u8, s.len() - 1), Inner::Foreign { ptr, len } => (ptr.as_ptr() as *const u8, len), + Inner::Inline { len, ref data } => (data.as_ptr(), len as usize), }; if len == 0 { "" @@ -848,12 +861,12 @@ impl GString { #[inline] pub fn as_gstr(&self) -> &GStr { let bytes = match self.0 { - Inner::Native(None) => return <&GStr>::default(), - Inner::Native(Some(ref s)) => s.as_bytes(), + Inner::Native(ref s) => s.as_bytes(), Inner::Foreign { len, .. } if len == 0 => &[0], Inner::Foreign { ptr, len } => unsafe { slice::from_raw_parts(ptr.as_ptr() as *const _, len + 1) }, + Inner::Inline { len, ref data } => unsafe { data.get_unchecked(..len as usize + 1) }, }; unsafe { GStr::from_utf8_with_nul_unchecked(bytes) } } @@ -863,9 +876,9 @@ impl GString { #[inline] pub fn as_ptr(&self) -> *const c_char { match self.0 { - Inner::Native(None) => <&GStr>::default().as_ptr(), - Inner::Native(Some(ref s)) => s.as_ptr() as *const _, + Inner::Native(ref s) => s.as_ptr() as *const _, Inner::Foreign { ptr, .. } => ptr.as_ptr(), + Inner::Inline { ref data, .. } => data.as_ptr() as *const _, } } @@ -875,19 +888,19 @@ impl GString { /// The returned buffer is not guaranteed to contain a trailing nul-byte. pub fn into_bytes(mut self) -> Vec { match &mut self.0 { - Inner::Native(s) => match s.take() { - None => Vec::new(), - Some(s) => { - let mut s = String::from(s); - let _nul = s.pop(); - debug_assert_eq!(_nul, Some('\0')); - s.into_bytes() - } - }, + Inner::Native(s) => { + let mut s = String::from(mem::replace(s, "".into())); + let _nul = s.pop(); + debug_assert_eq!(_nul, Some('\0')); + s.into_bytes() + } Inner::Foreign { ptr, len } => { let bytes = unsafe { slice::from_raw_parts(ptr.as_ptr() as *const u8, *len - 1) }; bytes.to_owned() } + Inner::Inline { len, data } => { + unsafe { data.get_unchecked(..*len as usize) }.to_owned() + } } } @@ -895,14 +908,14 @@ impl GString { /// Consumes the `GString` and returns the underlying byte buffer, with trailing nul-byte. pub fn into_bytes_with_nul(mut self) -> Vec { match &mut self.0 { - Inner::Native(s) => match s.take() { - None => vec![0u8], - Some(s) => str::into_boxed_bytes(s).into(), - }, + Inner::Native(s) => str::into_boxed_bytes(mem::replace(s, "".into())).into(), Inner::Foreign { ptr, len } => { let bytes = unsafe { slice::from_raw_parts(ptr.as_ptr() as *const u8, *len) }; bytes.to_owned() } + Inner::Inline { len, data } => { + unsafe { data.get_unchecked(..*len as usize + 1) }.to_owned() + } } } } @@ -1034,12 +1047,14 @@ impl IntoGlibPtr<*mut c_char> for GString { /// Transform into a nul-terminated raw C string pointer. unsafe fn into_glib_ptr(self) -> *mut c_char { match self.0 { - Inner::Native(None) => ffi::g_malloc0(1) as *mut _, - Inner::Native(Some(ref s)) => ffi::g_strndup(s.as_ptr() as *const _, s.len()), + Inner::Native(ref s) => ffi::g_strndup(s.as_ptr() as *const _, s.len()), Inner::Foreign { ptr, .. } => { let _s = mem::ManuallyDrop::new(self); ptr.as_ptr() } + Inner::Inline { len, ref data } => { + ffi::g_strndup(data.as_ptr() as *const _, len as usize) + } } } } @@ -1287,22 +1302,22 @@ impl From for String { #[inline] fn from(mut s: GString) -> Self { match &mut s.0 { - Inner::Native(s) => match s.take() { - None => Self::default(), - Some(s) => { - // Moves the underlying string - let mut s = String::from(s); - let _nul = s.pop(); - debug_assert_eq!(_nul, Some('\0')); - s - } - }, + Inner::Native(s) => { + // Moves the underlying string + let mut s = String::from(mem::replace(s, "".into())); + let _nul = s.pop(); + debug_assert_eq!(_nul, Some('\0')); + s + } Inner::Foreign { len, .. } if *len == 0 => String::new(), Inner::Foreign { ptr, len } => unsafe { // Creates a copy let slice = slice::from_raw_parts(ptr.as_ptr() as *const u8, *len); std::str::from_utf8_unchecked(slice).into() }, + Inner::Inline { len, data } => unsafe { + std::str::from_utf8_unchecked(data.get_unchecked(..*len as usize)).to_owned() + }, } } } @@ -1359,12 +1374,12 @@ impl From for GString { GStr::check_interior_nuls(&s).unwrap(); } if s.is_empty() { - Self(Inner::Native(None)) + Self::new() } else { s.reserve_exact(1); s.push('\0'); // No check for valid UTF-8 here - Self(Inner::Native(Some(s.into()))) + Self(Inner::Native(s.into())) } } } @@ -1400,8 +1415,14 @@ impl From<&str> for GString { if cfg!(debug_assertions) { GStr::check_interior_nuls(s).unwrap(); } - if s.is_empty() { - return Self::default(); + if s.len() < INLINE_LEN { + let mut data = <[u8; INLINE_LEN]>::default(); + let b = s.as_bytes(); + unsafe { data.get_unchecked_mut(..b.len()) }.copy_from_slice(b); + return Self(Inner::Inline { + len: b.len() as u8, + data, + }); } // Allocates with the GLib allocator unsafe { @@ -1420,7 +1441,7 @@ impl TryFrom for GString { #[inline] fn try_from(value: CString) -> Result { if value.as_bytes().is_empty() { - Ok(Self(Inner::Native(None))) + Ok(Self::new()) } else { // Moves the content of the CString // Also check if it's valid UTF-8 @@ -1431,7 +1452,7 @@ impl TryFrom for GString { err, ) })?; - Ok(Self(Inner::Native(Some(s.into())))) + Ok(Self(Inner::Native(s.into()))) } } } @@ -2096,4 +2117,15 @@ mod tests { let s = gformat!("bla bla {} bla", 123); assert_eq!(s, "bla bla 123 bla"); } + + #[test] + fn layout() { + // ensure the inline variant is not wider than the other variants + enum NoInline { + _Native(Box), + _Foreign(ptr::NonNull, usize), + } + assert_eq!(mem::size_of::(), mem::size_of::()); + assert_eq!(mem::size_of::(), mem::size_of::()); + } }