kiv_apple
kiv_apple

Reputation: 671

How to convert from String to Rc<str>?

Let's say I would like to generate a string using write! (it might be called conditionally, inside the loop etc, it's why I prefer write! over format!) and then use it in multiple places (so I will need some sort of Rc, otherwise I can't easily share it between structures). I don't need to change the string after generation, so I think that Rc<str> is a good choice (I can avoid indirection that will cause Rc<String> that will store a pointer to pointer).

The problem is a conversion between String (valid argument for write!) and Rc<str>:

use std::fmt::Write;

pub fn generate() -> Result<Rc<str>, Box<dyn Error>> {
    let mut out = String::new();
    writeln!(out, "Hello {} {}", 1, 2)?; // Just an example
    Ok(Rc::from(out.into_boxed_str()))
}

As I understand Rc::from performs copying of all character data into a new memory block (because Box and Rc have different layouts).

Is it possible somehow utilise the knowledge that we will create Rc later, so we can avoid copying? Maybe there are possibility to create someting like String (at least something that accepted by write!), but with a layout compatible with Rc?

Upvotes: 1

Views: 777

Answers (2)

cafce25
cafce25

Reputation: 27248

With an unknown number of bytes up front

Your best bet is to just use the From<String> implementation of Rc<str>, the String handles growing and copying while you're still appending and then when you know the amount of memory you need you copy it once over into the Rc<str> getting rid of any over allocation.

use std::error::Error;
use std::fmt::Write;
use std::rc::Rc;

pub fn generate() -> Result<Rc<str>, Box<dyn Error>> {
    let mut out = String::new();
    writeln!(out, "Hello {} {}", 1, 2)?; // Just an example
    Ok(Rc::from(out))
}

With a known number of bytes

You can directly allocate an Rc with enough storage to store the whole str and copy the data directly into it. To avoid initializing you can use MaybeUninit.

This currently requires nightly since there is no way to create an Rc with uninitialized storage on stable (that I know of).

#![feature(new_uninit, read_buf)]
use std::io::BorrowedBuf;
use std::rc::Rc;
use std::io::Write;
fn main() {
    let a = "hello";
    let b = "world";
    let len = a.len() + b.len() + 2;
    let mut rc = Rc::<[u8]>::new_uninit_slice(len);
    let mut r = BorrowedBuf::from(Rc::get_mut(&mut rc).unwrap());
    write!(r.unfilled(), "{a} {b}!").unwrap();
    assert_eq!(len, r.len());
    // SAFETY:
    // * we wrote all bytes in the `Rc`
    // * it's guaranteed valid utf-8 because we only used `write!` to do so
    // * layout is the same between `Rc<[u8]>` and `Rc<str>` https://rust-lang.github.io/unsafe-code-guidelines/layout/structs-and-tuples.html#single-field-structs
    let rc: Rc<str> = unsafe { std::mem::transmute(rc) };
    dbg!(rc);
}

Upvotes: 1

Chayim Friedman
Chayim Friedman

Reputation: 70910

If you have a known length, you can do it even on stable, at the cost of zeroing the array (and unsafe code). This is not guaranteed, but currently works without allocating because std specializes Rc's FromIterator for TrustedLen iterators, and Take<Repeat> is TrustedLen (adapted the code from @cafce25's answer):

use std::error::Error;
use std::fmt::Write;
use std::rc::Rc;

pub fn generate() -> Result<Rc<str>, Box<dyn Error>> {
    let mut rc = StrRcBuidler::new(6);
    writeln!(rc, "Hello")?;
    Ok(rc.build().unwrap())
}

#[derive(Debug)]
struct StrRcBuidler {
    rc: Rc<[u8]>,
    pos: usize,
}

impl StrRcBuidler {
    fn new(len: usize) -> Self {
        Self {
            rc: std::iter::repeat(0).take(len).collect(),
            pos: 0,
        }
    }
    fn build(self) -> Result<Rc<str>, Self> {
        if self.pos < self.rc.len() {
            return Err(self);
        }
        // SAFETY:
        //  - `[u8]` has the same layout as `str`.
        //  - Everything is UTF-8 because we only allow writes with `fmt::Write`.
        //  - We checked above that all bytes are initialized.
        Ok(unsafe { Rc::from_raw(Rc::into_raw(self.rc) as *const str) })
    }
}

impl Write for StrRcBuidler {
    fn write_str(&mut self, i: &str) -> Result<(), std::fmt::Error> {
        let i = i.as_bytes();
        let data = Rc::get_mut(&mut self.rc).unwrap()[self.pos..]
            .get_mut(..i.len())
            .ok_or(std::fmt::Error)?;
        data.copy_from_slice(i);
        self.pos += i.len();
        Ok(())
    }
}

Technically, you can use this also with only an upper limit on the length (just omit the if self.pos < self.rc.len() check), but this will pad the string with zeroes.

Upvotes: 0

Related Questions