Skip to content

Commit

Permalink
glib: allow GString to store small inline strings
Browse files Browse the repository at this point in the history
  • Loading branch information
jf2048 committed Dec 12, 2022
1 parent 50e2d2b commit 671fa05
Showing 1 changed file with 85 additions and 53 deletions.
138 changes: 85 additions & 53 deletions glib/src/gstring.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<Box<str>>),
Native(Box<str>),
Foreign {
ptr: ptr::NonNull<c_char>,
len: usize,
},
Inline {
len: u8,
data: [u8; INLINE_LEN],
},
}

unsafe impl Send for GString {}
Expand All @@ -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`].
Expand Down Expand Up @@ -685,11 +698,11 @@ impl GString {
#[inline]
pub unsafe fn from_utf8_unchecked(mut v: Vec<u8>) -> 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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 {
""
Expand All @@ -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) }
}
Expand All @@ -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 _,
}
}

Expand All @@ -875,34 +888,34 @@ impl GString {
/// The returned buffer is not guaranteed to contain a trailing nul-byte.
pub fn into_bytes(mut self) -> Vec<u8> {
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()
}
}
}

// rustdoc-stripper-ignore-next
/// Consumes the `GString` and returns the underlying byte buffer, with trailing nul-byte.
pub fn into_bytes_with_nul(mut self) -> Vec<u8> {
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()
}
}
}
}
Expand Down Expand Up @@ -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)
}
}
}
}
Expand Down Expand Up @@ -1287,22 +1302,22 @@ impl From<GString> 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()
},
}
}
}
Expand Down Expand Up @@ -1359,12 +1374,12 @@ impl From<String> 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()))
}
}
}
Expand Down Expand Up @@ -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 {
Expand All @@ -1420,7 +1441,7 @@ impl TryFrom<CString> for GString {
#[inline]
fn try_from(value: CString) -> Result<Self, Self::Error> {
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
Expand All @@ -1431,7 +1452,7 @@ impl TryFrom<CString> for GString {
err,
)
})?;
Ok(Self(Inner::Native(Some(s.into()))))
Ok(Self(Inner::Native(s.into())))
}
}
}
Expand Down Expand Up @@ -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<str>),
_Foreign(ptr::NonNull<c_char>, usize),
}
assert_eq!(mem::size_of::<GString>(), mem::size_of::<NoInline>());
assert_eq!(mem::size_of::<GString>(), mem::size_of::<String>());
}
}

0 comments on commit 671fa05

Please sign in to comment.