Format string to buffer and get length

I just want to put a formatted string in a byte array. To use it I obviously need the length of the output, but I did not find how to do it properly. Everything I come up with is just stupid... like this:

use std::io::Write;

fn main() {
  let mut buf = [0u8; 65536];

  let s = "string";

  let mut remlen = buf.len();

  {
    let mut x = &mut buf[..];
    write!(x, "test {} 123", s).unwrap();
    remlen = x.len();
  }

  println!(
    "{}",
    std::str::from_utf8(&buf[..(buf.len() - remlen)]).unwrap()
  );
}

The last println is just to check. In reality I want to send the buffer over a socket.

std::string::String has a method called as_bytes. Does that not work for you?

Does std::io::Write::write_fmt work? That's what the write macro is calling under the hood.

1 Like

I'd like to avoid the Heap allocation if thats possible. Sorry I am really new to Rust, but this seems like something that should not be that hard. I mean the write! macro is there... I am probably just not able to use it correctly

You could create an empty ArrayVec and write to it:

use arrayvec::ArrayVec;
use std::io::Write;

fn main() {
  let mut buf: ArrayVec<[u8; 65536]> = ArrayVec::new();

  let s = "string";
  write!(buf, "test {} 123", s).unwrap();

  println!("{}", std::str::from_utf8(&buf).unwrap());
}
2 Likes

AFAIK formatting strings will always return a heap allocated string because it's unknown beforehand what the size of the arguments will be so it can't be stack allocated.

However if your socket implements std::io::Write, there should be no need to format the strings, just write each piece in the writer subsequently. For that you will need to transform each piece to &[u8]. For strings (heap allocated or not) that can be done with as_bytes.

1 Like

Formatting won't allocate at all on it's own. So using write!(socket, "hello {}", "bar") won't allocate. For details, see the core::fmt::Arguments type.

1 Like

Here's another option, which uses only the standard library:

use std::io::{Cursor, Write};

fn main() {
    let mut buf = [0u8; 65536];

    let mut cursor = Cursor::new(&mut buf[..]);
    let s = "string";
    write!(cursor, "test {} 123", s).unwrap();
    let len = cursor.position() as usize;

    println!("{}", std::str::from_utf8(&buf[..len]).unwrap());
}

Playground

5 Likes

I like the pure std version a lot. Thanks!

I am really surprised how elegant the Write trait can be used to compose different things. Writing directly to the socket is awesome, but unfortunately I am using tokio. I guess it does not work that easily there...

1 Like

Sockets from tokio normally implement AsyncWrite, which just takes a &[u8] as well. The user facing methods are in an extension trait AsyncWriteExt. You shouldn't have to make an array specifically and shouldn't have to worry about the exact size if all you want to do is send the bytes into the socket.

use tokio::net::TcpStream;
use tokio::prelude::*;
use std::error::Error;

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
    // Connect to a peer
    let mut stream = TcpStream::connect("127.0.0.1:8080").await?;

    let s = "string";

    // Write some data.
    stream.write(b"test ").await?;
    stream.write(s.as_bytes()).await?;
    stream.write(b" 123").await?;
    Ok(())
}

If you really want a formatted string, it can also take a buffer in through AsyncWriteExt::write_buf, which will take a std::io::Cursor.

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.