| // Part of the Crubit project, under the Apache License v2.0 with LLVM |
| // Exceptions. See /LICENSE for license information. |
| // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
| |
| extern crate alloc; |
| extern crate std; |
| |
| use crate::crubit_cc_std_internal::conversion_function_helpers; |
| use crate::lossy_utf8::{debug_bytes, LossyUtf8Display}; |
| use crate::std::string_view; |
| use alloc::string::String; |
| use alloc::vec::Vec; |
| use bridge_rust::{transmute_abi, CrubitAbi, Decoder, Encoder}; |
| use core::clone::Clone; |
| use core::cmp::Eq; |
| use core::cmp::PartialEq; |
| use core::ffi::c_void; |
| use core::mem::{ManuallyDrop, MaybeUninit}; |
| use core::ops::Deref; |
| use core::ptr::NonNull; |
| use ctor::{Ctor, CtorNew, FnCtor, Infallible, PinnedDrop, RvalueReference}; |
| use std::fmt::Display; |
| |
| #[cfg(unix)] |
| use std::os::unix::ffi::OsStrExt; |
| |
| /// An owned C++ string. The pointer is guaranteed to be a non-null C++ |
| /// allocated pointer to std::string. |
| /// |
| /// We make sure that conversion between `string` and `std::string` is O(1), but |
| /// conversion between `string` and other types like `&str` or `String` can be |
| /// O(n). |
| // TODO: Make it mutable?. |
| // TODO: add a dependency on `crubit_annotate` to use that directly rather than doc attributes. |
| #[doc = "CRUBIT_ANNOTATE: cpp_type=std::string"] |
| #[doc = "CRUBIT_ANNOTATE: include_path=<string>"] |
| #[doc = "CRUBIT_ANNOTATE: cpp_to_rust_converter=cpp_string_to_rust_string"] |
| #[doc = "CRUBIT_ANNOTATE: rust_to_cpp_converter=rust_string_to_cpp_string"] |
| #[allow(non_snake_case)] |
| #[repr(C)] |
| pub struct string_wrapper { |
| owned_cpp_string: NonNull<c_void>, |
| } |
| |
| // SAFETY: the string is owned, and mutation is only allowed via `Pin<&mut Self>`. |
| unsafe impl Send for string_wrapper {} |
| |
| // SAFETY: the string is owned, and mutation is only allowed via `Pin<&mut Self>`. |
| unsafe impl Sync for string_wrapper {} |
| |
| impl string_wrapper { |
| pub fn as_slice(&self) -> &[u8] { |
| self.as_ref() |
| } |
| |
| pub fn to_vec(&self) -> Vec<u8> { |
| self.as_slice().into() |
| } |
| |
| pub fn to_str(&self) -> Result<&str, core::str::Utf8Error> { |
| str::from_utf8(self.as_slice()) |
| } |
| |
| pub fn to_string(&self) -> Result<String, core::str::Utf8Error> { |
| self.to_str().map(String::from) |
| } |
| |
| #[cfg(unix)] |
| pub fn as_os_str(&self) -> &std::ffi::OsStr { |
| std::ffi::OsStr::from_bytes(&self) |
| } |
| |
| #[cfg(unix)] |
| pub fn to_os_string(&self) -> std::ffi::OsString { |
| self.as_os_str().into() |
| } |
| |
| /// Returns a `*const c_void` pointing to the underlying C++ |
| /// `std::string` object. |
| /// |
| /// The caller must ensure that the `string` outlives the pointer. |
| /// |
| /// This method guarantees that for the purpose of the aliasing |
| /// model, this method does not materialize a reference to the |
| /// underlying data the string owns, and thus the returned |
| /// pointer will remain valid when mixed with other calls to |
| /// `as_void_ptr` and `as_mut_void_ptr`. |
| pub fn as_void_ptr(&self) -> *const c_void { |
| self.owned_cpp_string.as_ptr() as *const _ |
| } |
| |
| /// Returns a `*mut c_void` pointing to the underlying C++ |
| /// `std::string` object. |
| /// |
| /// The caller must ensure that the `string` outlives the pointer. |
| /// |
| /// This method guarantees that for the purpose of the aliasing |
| /// model, this method does not materialize a reference to the |
| /// underlying data the string owns, and thus the returned |
| /// pointer will remain valid when mixed with other calls to |
| /// `as_void_ptr` and `as_mut_void_ptr`. |
| /// |
| /// However, note that writing to the pointer will invalidate references to |
| /// the underlying data. For example, the behavior is undefined if the |
| /// underlying string is mutated through this pointer during the lifetime |
| /// of a slice as returned by `as_slice`. |
| pub fn as_mut_void_ptr(&mut self) -> *mut c_void { |
| self.owned_cpp_string.as_ptr() |
| } |
| |
| /// Returns an object that implements `Display` for safely printing strings that may contain |
| /// non-Unicode data. The Display implementation replaces invalid UTF-8 with the Unicode |
| /// replacement character (�), and is potentially lossy. |
| pub fn display(&self) -> impl Display + use<'_> { |
| LossyUtf8Display(self.as_slice()) |
| } |
| } |
| |
| impl PartialEq for string_wrapper { |
| fn eq(&self, other: &Self) -> bool { |
| // SAFETY: `owned_cpp_string` is guaranteed to be a non-null C++ allocated |
| // pointer to std::string. |
| unsafe { |
| conversion_function_helpers::StringEqual( |
| self.owned_cpp_string.as_ptr(), |
| other.owned_cpp_string.as_ptr(), |
| ) |
| } |
| } |
| } |
| |
| impl Eq for string_wrapper {} |
| |
| impl Default for string_wrapper { |
| fn default() -> Self { |
| "".into() |
| } |
| } |
| |
| impl Clone for string_wrapper { |
| fn clone(&self) -> Self { |
| // SAFETY: `owned_cpp_string` is guaranteed to be a non-null C++ allocated |
| // pointer to std::string. |
| let raw_string = unsafe { |
| conversion_function_helpers::StringCopyOwnedPtr(self.owned_cpp_string.as_ptr()) |
| }; |
| if let Some(ptr) = NonNull::new(raw_string) { |
| Self { owned_cpp_string: ptr } |
| } else { |
| panic!("Failed to copy string"); |
| } |
| } |
| } |
| |
| impl Drop for string_wrapper { |
| fn drop(&mut self) { |
| unsafe { |
| // SAFETY: `owned_cpp_string` is guaranteed to be a non-null C++ allocated |
| // pointer to std::string. |
| conversion_function_helpers::StringDelete(self.owned_cpp_string.as_ptr()); |
| } |
| } |
| } |
| |
| impl From<String> for string_wrapper { |
| fn from(s: String) -> Self { |
| s.as_bytes().into() |
| } |
| } |
| |
| impl From<&String> for string_wrapper { |
| fn from(s: &String) -> Self { |
| s.as_bytes().into() |
| } |
| } |
| |
| impl From<&Vec<u8>> for string_wrapper { |
| fn from(s: &Vec<u8>) -> Self { |
| s.as_slice().into() |
| } |
| } |
| |
| impl From<&str> for string_wrapper { |
| fn from(s: &str) -> Self { |
| s.as_bytes().into() |
| } |
| } |
| |
| impl From<&[u8]> for string_wrapper { |
| fn from(s: &[u8]) -> Self { |
| // SAFETY: Rust slice returns a valid pointer to a buffer of bytes. |
| let raw_string = unsafe { |
| conversion_function_helpers::StringCreateFromBuffer(s.as_ptr() as _, s.len()) |
| }; |
| if let Some(ptr) = NonNull::new(raw_string) { |
| Self { owned_cpp_string: ptr } |
| } else { |
| panic!("Failed to create string"); |
| } |
| } |
| } |
| |
| impl Deref for string_wrapper { |
| type Target = [u8]; |
| fn deref(&self) -> &Self::Target { |
| let ptr = self.owned_cpp_string.as_ptr(); |
| // SAFETY: |
| // |
| // * `owned_cpp_string` is guaranteed to be a non-null C++ allocated pointer to std::string |
| // so `ptr` is non-null. |
| // * `StringGetData` returns the pointer of the C++ `std::string::data()`, which |
| // is guaranteed to be non-null and point to a continuous memory region. Every |
| // byte in [ptr, ptr + len)) is initialized. (See https://en.cppreference.com/w/cpp/string/basic_string/data) |
| // * The data is guaranteed to be not mutated because we don't ever mutate data() except |
| // when accessed via &mut self, which is blocked by Rust borrow checker. |
| // * `len` is guaranteed to be less than `isize::MAX` because C++ implementations guarantee |
| // in practice that the object won't go past the end of the address space. |
| unsafe { |
| let len = conversion_function_helpers::StringGetSize(ptr); |
| core::slice::from_raw_parts(conversion_function_helpers::StringGetData(ptr) as _, len) |
| } |
| } |
| } |
| |
| impl core::convert::AsRef<[u8]> for string_wrapper { |
| fn as_ref(&self) -> &[u8] { |
| &*self |
| } |
| } |
| |
| impl core::fmt::Debug for string_wrapper { |
| // TODO(b/351976622): Make a pretty Debug like std::string(b"\xffhello\xde") or |
| // similar. |
| fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { |
| write!(f, "cc_std::string_wrapper({:?})", self.as_slice()) |
| } |
| } |
| |
| // Allow converting a cc_std::std::string_wrapper reference to a "real" C++ string pointer. |
| |
| type StringSymbol = forward_declare::symbol!( |
| "std :: basic_string < char , std :: char_traits < char >, std :: allocator < char >>" |
| ); |
| |
| impl<'a, Crate> forward_declare::CppCoerce<*const forward_declare::Incomplete<StringSymbol, Crate>> |
| for &'a string_wrapper |
| { |
| fn cpp_coerce(self) -> *const forward_declare::Incomplete<StringSymbol, Crate> { |
| self.owned_cpp_string.as_ptr() as *const _ as *const _ |
| } |
| } |
| |
| impl<'a, Crate> forward_declare::CppCoerce<*mut forward_declare::Incomplete<StringSymbol, Crate>> |
| for &'a mut string_wrapper |
| { |
| fn cpp_coerce(self) -> *mut forward_declare::Incomplete<StringSymbol, Crate> { |
| self.owned_cpp_string.as_ptr() as *mut _ |
| } |
| } |
| |
| impl<'a> forward_declare::CppCoerce<*const string> for &'a string_wrapper { |
| fn cpp_coerce(self) -> *const string { |
| self.owned_cpp_string.as_ptr() as *const string |
| } |
| } |
| |
| impl<'a> forward_declare::CppCoerce<*mut string> for &'a mut string_wrapper { |
| fn cpp_coerce(self) -> *mut string { |
| self.owned_cpp_string.as_ptr() as *mut string |
| } |
| } |
| |
| impl<'a> forward_declare::CppCoerce<&'a string> for &'a string_wrapper { |
| fn cpp_coerce(self) -> &'a string { |
| unsafe { &*(self.owned_cpp_string.as_ptr() as *const string) } |
| } |
| } |
| |
| impl<'a> forward_declare::CppCoerce<core::pin::Pin<&'a mut string>> for &'a mut string_wrapper { |
| fn cpp_coerce(self) -> core::pin::Pin<&'a mut string> { |
| unsafe { |
| core::pin::Pin::new_unchecked(&mut *(self.owned_cpp_string.as_ptr() as *mut string)) |
| } |
| } |
| } |
| |
| /// The Crubit ABI for C++ `std::string`. It is specified as one pointer. |
| /// |
| /// This pointer should point to a heap allocated `std::string` object, where the pointer is the |
| /// owner of the allocation. Alternatively, the pointer can be null if and only if the allocation |
| /// failed, in which case it's okay to panic. |
| #[derive(Clone, Default)] |
| pub struct BoxedCppStringAbi; |
| |
| unsafe impl CrubitAbi for BoxedCppStringAbi { |
| type Value = string_wrapper; |
| |
| const SIZE: usize = core::mem::size_of::<*mut c_void>(); |
| |
| fn encode(self, value: Self::Value, encoder: &mut Encoder) { |
| transmute_abi().encode(ManuallyDrop::new(value).as_mut_void_ptr(), encoder); |
| } |
| |
| unsafe fn decode(self, decoder: &mut Decoder) -> Self::Value { |
| // SAFETY: the caller guarantees that the buffer contains an allocated or null pointer to a |
| // C++ `std::string` object. |
| let ptr: *mut c_void = unsafe { transmute_abi().decode(decoder) }; |
| |
| Self::Value { |
| owned_cpp_string: NonNull::new(ptr).expect("Boxing a std::string shouldn't fail"), |
| } |
| } |
| } |
| |
| // Void pointer converters are needed for cc_bindings_from_rs. |
| |
| #[unsafe(no_mangle)] |
| pub unsafe extern "C" fn rust_string_to_cpp_string(input: *const c_void, output: *mut c_void) { |
| // SAFETY: |
| // * `input` is a valid `string_wrapper`. |
| // * `input.owned_cpp_string` is guaranteed to be a non-null C++ allocated pointer to |
| // std::string. |
| // * `output` is a valid C++ `std::string`. |
| unsafe { |
| let input = &*(input as *const string_wrapper); |
| conversion_function_helpers::StringCreateInPlace(output, input.owned_cpp_string.as_ptr()); |
| } |
| } |
| |
| #[unsafe(no_mangle)] |
| pub unsafe extern "C" fn cpp_string_to_rust_string(input: *mut c_void, output: *mut c_void) { |
| // SAFETY: `input` is a valid `std::string` so it can be safely moved. |
| let owned_cpp_string = unsafe { conversion_function_helpers::StringMoveOwnedPtr(input) }; |
| if let Some(ptr) = NonNull::new(owned_cpp_string) { |
| // SAFETY: `output` is a valid buffer for a `string_wrapper`. |
| unsafe { |
| let output = &mut *(output as *mut MaybeUninit<string_wrapper>); |
| output.as_mut_ptr().write(string_wrapper { owned_cpp_string: ptr }); |
| } |
| } else { |
| panic!("Failed to create owned string"); |
| } |
| } |
| |
| /// A layout-compatible `std::string` wrapper. |
| /// |
| /// # Differences from Rust String types |
| /// |
| /// * **Rust `String`**: |
| /// - **Movability**: Rust's `String` is trivially relocatable (can be moved with `memcpy`). |
| /// `string` (wrapping C++ `std::string`) is **not** trivially relocatable. On some platforms |
| /// (e.g., `libstdc++` with Short String Optimization), `std::string` may contain |
| /// self-references (pointers pointing to its own internal buffer). Moving it in memory without |
| /// running its C++ move constructor would invalidate these pointers. Thus, in Rust, it is |
| /// `!Unpin` and must be pinned. |
| /// - **Layout**: Rust's `String` has a fixed layout of 3 fields (pointer, capacity, length). |
| /// `string` has a platform-dependent layout that matches C++ `std::string`. |
| /// |
| /// * **Rust `Vec<u8>`**: |
| /// - **Movability and Layout**: Like `String`, `Vec<u8>` is trivially relocatable and has a fixed |
| /// Rust layout, unlike `string`. |
| /// - **Intent**: `Vec<u8>` is a general-purpose byte container, whereas `string` represents a C++ |
| /// string, typically used for textual data (though C++ `std::string` can contain arbitrary |
| /// bytes including nulls). |
| /// |
| /// * **Byte String types (e.g., `bstr::BStr`)**: |
| /// - **Movability and Layout**: These are slice types or wrapper types that are trivially |
| /// relocatable, unlike `string`. |
| /// - `string` is most similar to these in that it does not enforce UTF-8 validation, but it |
| /// retains the C++ layout and non-movability constraints. |
| /// |
| /// # Examples |
| /// |
| /// ``` |
| /// use cpp_std::string; |
| /// use ctor::{emplace, CtorNew}; |
| /// |
| /// // Create a string using emplace! and CtorNew: |
| /// let my_string = emplace!(string::ctor_new("Hello, C++!")); |
| /// |
| /// // Access it as a slice: |
| /// assert_eq!(my_string.as_slice(), b"Hello, C++!"); |
| /// |
| /// // Convert to Rust str (if valid UTF-8): |
| /// assert_eq!(my_string.to_str().unwrap(), "Hello, C++!"); |
| /// ``` |
| #[crubit_annotate::cpp_layout_equivalent(cpp_type = "::std::string", include_path = "<string>")] |
| #[cfg_attr(target_pointer_width = "64", repr(C, align(8)))] |
| #[cfg_attr(target_pointer_width = "32", repr(C, align(4)))] |
| #[allow(non_camel_case_types)] |
| #[ctor::recursively_pinned(PinnedDrop)] |
| pub struct string { |
| _opaque: core::cfg_select! { |
| any(target_arch = "x86_64", target_arch = "aarch64", target_arch = "riscv64") => { |
| [u8; 24] |
| } |
| any(target_arch = "arm", target_arch = "x86") => [u8; 12], |
| _ => { |
| compile_error!("string is only supported on x86_64, aarch64, riscv64, arm, and x86") |
| } |
| }, |
| _pinned: core::marker::PhantomPinned, |
| } |
| |
| // SAFETY: the string is owned, and mutation is only allowed via `Pin<&mut Self>`. |
| unsafe impl Send for string {} |
| |
| // SAFETY: the string is owned, and mutation is only allowed via `Pin<&mut Self>`. |
| unsafe impl Sync for string {} |
| |
| impl PinnedDrop for string { |
| unsafe fn pinned_drop(self: core::pin::Pin<&mut Self>) { |
| // SAFETY: `self` is a valid, pinned reference to a `string`. |
| unsafe { |
| conversion_function_helpers::StringDestroyInPlace( |
| self.get_unchecked_mut() as *mut _ as *mut c_void |
| ); |
| } |
| } |
| } |
| |
| impl CtorNew<()> for string { |
| type CtorType = impl Ctor<Output = Self, Error = Infallible>; |
| type Error = Infallible; |
| |
| fn ctor_new(_args: ()) -> Self::CtorType { |
| unsafe { |
| // SAFETY: `dest` is a valid, pinned reference to a `string`. |
| FnCtor::new(|dest| { |
| conversion_function_helpers::StringCreateDefaultInPlace(dest as *mut c_void); |
| }) |
| } |
| } |
| } |
| |
| impl<'a> CtorNew<&'a [u8]> for string { |
| type CtorType = impl Ctor<Output = Self, Error = Infallible> + use<'a>; |
| type Error = Infallible; |
| |
| fn ctor_new(args: &'a [u8]) -> Self::CtorType { |
| // SAFETY: `dest` is a valid, pinned reference to a `string`. |
| unsafe { |
| FnCtor::new(move |dest| { |
| conversion_function_helpers::StringCreateFromBufferInPlace( |
| dest as *mut c_void, |
| args.as_ptr() as _, |
| args.len(), |
| ); |
| }) |
| } |
| } |
| } |
| |
| impl<'a> CtorNew<&'a str> for string { |
| type CtorType = impl Ctor<Output = Self, Error = Infallible> + use<'a>; |
| type Error = Infallible; |
| |
| fn ctor_new(args: &'a str) -> Self::CtorType { |
| Self::ctor_new(args.as_bytes()) |
| } |
| } |
| |
| impl<'a> CtorNew<&'a string> for string { |
| type CtorType = impl Ctor<Output = Self, Error = Infallible> + use<'a>; |
| type Error = Infallible; |
| |
| fn ctor_new(args: &'a string) -> Self::CtorType { |
| // SAFETY: `dest` is a valid, pinned reference to a `string`. |
| unsafe { |
| FnCtor::new(move |dest| { |
| conversion_function_helpers::StringCreateCopyInPlace( |
| dest as *mut c_void, |
| args as *const string as *const c_void, |
| ); |
| }) |
| } |
| } |
| } |
| |
| impl<'a> CtorNew<RvalueReference<'a, string>> for string { |
| type CtorType = impl Ctor<Output = Self, Error = Infallible> + use<'a>; |
| type Error = Infallible; |
| |
| fn ctor_new(args: RvalueReference<'a, string>) -> Self::CtorType { |
| // SAFETY: `dest` is a valid, pinned reference to a `string`. |
| unsafe { |
| FnCtor::new(move |dest| { |
| let src = args.0.get_unchecked_mut() as *mut string; |
| conversion_function_helpers::StringCreateInPlace( |
| dest as *mut c_void, |
| src as *mut c_void, |
| ); |
| }) |
| } |
| } |
| } |
| |
| impl string { |
| /// Returns a shared slice of the string's bytes. |
| pub fn as_slice(&self) -> &[u8] { |
| self |
| } |
| |
| /// Returns a mutable slice of the string's bytes. |
| /// |
| /// Since `string` is `!Unpin`, this requires pinning. |
| pub fn as_mut_slice(self: core::pin::Pin<&mut Self>) -> &mut [u8] { |
| // SAFETY: `self` is a valid, pinned reference to a `string`. |
| unsafe { |
| let ptr = self.get_unchecked_mut() as *mut string as *mut c_void; |
| let len = conversion_function_helpers::StringGetSize(ptr); |
| core::slice::from_raw_parts_mut( |
| conversion_function_helpers::StringGetMutData(ptr) as _, |
| len, |
| ) |
| } |
| } |
| |
| /// Returns a raw pointer to the C++ `std::string` object. |
| pub fn as_ptr(&self) -> *const Self { |
| self as *const Self |
| } |
| |
| /// Returns a raw mutable pointer to the C++ `std::string` object. |
| pub fn as_mut_ptr(self: core::pin::Pin<&mut Self>) -> *mut Self { |
| // SAFETY: `self` is a valid, pinned reference to a `string`. |
| unsafe { self.get_unchecked_mut() as *mut Self } |
| } |
| |
| /// Returns a copy of the string's bytes as a `Vec<u8>`. |
| pub fn to_vec(&self) -> Vec<u8> { |
| self.as_slice().into() |
| } |
| |
| /// Yields a `&str` slice if the string contains valid UTF-8. |
| pub fn to_str(&self) -> Result<&str, core::str::Utf8Error> { |
| core::str::from_utf8(self.as_slice()) |
| } |
| |
| /// Yields a Rust `String` if the string contains valid UTF-8. |
| pub fn to_string(&self) -> Result<String, core::str::Utf8Error> { |
| self.to_str().map(String::from) |
| } |
| |
| /// Returns an object that implements `Display` for safely printing the string, |
| /// replacing invalid UTF-8 with the Unicode replacement character. |
| pub fn display(&self) -> impl Display + '_ { |
| LossyUtf8Display(self.as_slice()) |
| } |
| } |
| |
| impl Deref for string { |
| type Target = [u8]; |
| fn deref(&self) -> &Self::Target { |
| // SAFETY: `self` is a valid, pinned reference to a `string`. |
| unsafe { |
| let ptr = self as *const string as *const c_void; |
| let len = conversion_function_helpers::StringGetSize(ptr); |
| core::slice::from_raw_parts(conversion_function_helpers::StringGetData(ptr) as _, len) |
| } |
| } |
| } |
| |
| impl core::convert::AsRef<[u8]> for string { |
| fn as_ref(&self) -> &[u8] { |
| &*self |
| } |
| } |
| |
| impl PartialEq for string { |
| fn eq(&self, other: &Self) -> bool { |
| self.as_slice() == other.as_slice() |
| } |
| } |
| |
| impl Eq for string {} |
| |
| impl PartialOrd for string { |
| fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> { |
| Some(self.cmp(other)) |
| } |
| } |
| |
| impl Ord for string { |
| fn cmp(&self, other: &Self) -> core::cmp::Ordering { |
| self.as_slice().cmp(other.as_slice()) |
| } |
| } |
| |
| impl core::hash::Hash for string { |
| fn hash<H: core::hash::Hasher>(&self, state: &mut H) { |
| self.as_slice().hash(state); |
| } |
| } |
| |
| // PartialEq impls for comparing to string_view, str, and [u8] |
| impl<'a> PartialEq<string_view<'a>> for string { |
| fn eq(&self, other: &string_view<'a>) -> bool { |
| self.as_slice() == other.as_bytes() |
| } |
| } |
| |
| impl PartialEq<str> for string { |
| fn eq(&self, other: &str) -> bool { |
| self.as_slice() == other.as_bytes() |
| } |
| } |
| |
| impl<'a> PartialEq<&'a str> for string { |
| fn eq(&self, other: &&'a str) -> bool { |
| self.as_slice() == other.as_bytes() |
| } |
| } |
| |
| impl PartialEq<[u8]> for string { |
| fn eq(&self, other: &[u8]) -> bool { |
| self.as_slice() == other |
| } |
| } |
| |
| impl<'a> PartialEq<&'a [u8]> for string { |
| fn eq(&self, other: &&'a [u8]) -> bool { |
| self.as_slice() == *other |
| } |
| } |
| |
| impl core::fmt::Debug for string { |
| fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { |
| debug_bytes(self.as_slice(), f) |
| } |
| } |