21

How could I implement the following example without using std?

let text = format!("example {:.1} test {:x} words {}", num1, num2, num3);

text has type &str and num1, num2 and num3 have any numeric type.

I've tried using numtoa and itoa/dtoa for displaying numbers but numtoa does not support floats and itoa does not support no_std. I feel like displaying a number in a string is fairly common and that I'm probably missing something obvious.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Ferdia McKeogh
  • 421
  • 1
  • 5
  • 9

5 Answers5

28

In addition to Shepmaster's answer you can also format strings without an allocator.

In core::fmt::Write you only need to implement write_str and then you get write_fmt for free.

With format_args!(...) (same syntax as format!) you can prepare a core::fmt::Arguments value, which can be passed to core::fmt::write.

See Playground:

#![crate_type = "dylib"]
#![no_std]

pub mod write_to {
    use core::cmp::min;
    use core::fmt;

    pub struct WriteTo<'a> {
        buffer: &'a mut [u8],
        // on write error (i.e. not enough space in buffer) this grows beyond
        // `buffer.len()`.
        used: usize,
    }

    impl<'a> WriteTo<'a> {
        pub fn new(buffer: &'a mut [u8]) -> Self {
            WriteTo { buffer, used: 0 }
        }

        pub fn as_str(self) -> Option<&'a str> {
            if self.used <= self.buffer.len() {
                // only successful concats of str - must be a valid str.
                use core::str::from_utf8_unchecked;
                Some(unsafe { from_utf8_unchecked(&self.buffer[..self.used]) })
            } else {
                None
            }
        }
    }

    impl<'a> fmt::Write for WriteTo<'a> {
        fn write_str(&mut self, s: &str) -> fmt::Result {
            if self.used > self.buffer.len() {
                return Err(fmt::Error);
            }
            let remaining_buf = &mut self.buffer[self.used..];
            let raw_s = s.as_bytes();
            let write_num = min(raw_s.len(), remaining_buf.len());
            remaining_buf[..write_num].copy_from_slice(&raw_s[..write_num]);
            self.used += raw_s.len();
            if write_num < raw_s.len() {
                Err(fmt::Error)
            } else {
                Ok(())
            }
        }
    }

    pub fn show<'a>(buffer: &'a mut [u8], args: fmt::Arguments) -> Result<&'a str, fmt::Error> {
        let mut w = WriteTo::new(buffer);
        fmt::write(&mut w, args)?;
        w.as_str().ok_or(fmt::Error)
    }
}

pub fn test() {
    let mut buf = [0u8; 64];
    let _s: &str = write_to::show(
        &mut buf,
        format_args!("write some stuff {:?}: {}", "foo", 42),
    ).unwrap();
}
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Stefan
  • 5,304
  • 2
  • 25
  • 44
  • 1
    Why not `let mut w = write_to::WriteTo::new(&mut buf); write!(&mut w, "café").unwrap();` ? – Shepmaster May 06 '18 at 18:28
  • 1
    @Shepmaster you'll need another step to extract the result and probably a `use core::fmt::Write;` ([`write!`](https://doc.rust-lang.org/core/macro.write.html) needs a `write_fmt` method in scope)... but yes, you can use it that way too. Maybe a `format_buf!(&mut buf, "...", ...)` macro would be a more comfortable way - but I think that exercise can be left to the reader :) – Stefan May 06 '18 at 18:38
15

In general, you don't. format! allocates a String, and a no_std environment doesn't have an allocator.

If you do have an allocator, you can use the alloc crate. This crate contains the format! macro.

#![crate_type = "dylib"]
#![no_std]

#[macro_use]
extern crate alloc;

fn thing() {
    let text = format!("example {:.1} test {:x} words {}", 1, 2, 3);
}

See also:

jhpratt
  • 6,841
  • 16
  • 40
  • 50
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
  • I'm targeting an STM32F103, will japaric's [`alloc-cortex-m`](https://github.com/japaric/alloc-cortex-m) work? Do I have any safer alternatives? Thanks so much! – Ferdia McKeogh May 06 '18 at 14:14
  • 1
    @FerdiaMcKeogh I'm not sure what you mean by "safer" in this context, but it seems like a reasonable choice. – Shepmaster May 06 '18 at 15:00
  • Sorry, I meant safer in terms of a stable/widely accepted method. `alloc-cortex-m` is working perfectly! Thanks! – Ferdia McKeogh May 06 '18 at 15:05
3

You can also combine the usage of numtoa and arrayvec crates. Example:

#![no_std]

use numtoa::NumToA;
use arrayvec::ArrayString;

fn main() -> ! {
    let mut num_buffer = [0u8; 20];
    let mut text = ArrayString::<[_; 100]>::new();

    let num1 = 123;
    let num2 = 456;
    let num3 = 789;

    // text.clear(); (on subsequent usages)
    text.push_str("example ");
    text.push_str(num1.numtoa_str(10, &mut num_buffer));

    text.push_str(" test ");
    text.push_str(num2.numtoa_str(10, &mut num_buffer));

    text.push_str(" words ");
    text.push_str(num3.numtoa_str(10, &mut num_buffer));
}

Note that push_str can panic. Check out the api for try_ -methods

And Cargo.toml

 [dependencies]
 arrayvec = { version = "0.5", default-features = false }
 numtoa = "0.2"
Mika Vatanen
  • 3,782
  • 1
  • 28
  • 33
3

Write a formatter!

use core::fmt::{self, Write};
use core::str;

fn main() {
    // For LCD 160 / 8 = 20 chars
    let mut buf = [0u8; 20];
    let mut buf = ByteMutWriter::new(&mut buf[..]);
  
    buf.clear();
    write!(&mut buf, "Hello {}!", "Rust").unwrap();
    
    // buf.as_str()
}


pub struct ByteMutWriter<'a> {
    buf: &'a mut [u8],
    cursor: usize,
}

impl<'a> ByteMutWriter<'a> {
    pub fn new(buf: &'a mut [u8]) -> Self {
        ByteMutWriter { buf, cursor: 0 }
    }

    pub fn as_str(&self) -> &str {
        str::from_utf8(&self.buf[0..self.cursor]).unwrap()
    }

    #[inline]
    pub fn capacity(&self) -> usize {
        self.buf.len()
    }

    pub fn clear(&mut self) {
        self.cursor = 0;
    }

    pub fn len(&self) -> usize {
        self.cursor
    }

    pub fn empty(&self) -> bool {
        self.cursor == 0
    }

    pub fn full(&self) -> bool {
        self.capacity() == self.cursor
    }
}

impl fmt::Write for ByteMutWriter<'_> {
    fn write_str(&mut self, s: &str) -> fmt::Result {
        let cap = self.capacity();
        for (i, &b) in self.buf[self.cursor..cap]
            .iter_mut()
            .zip(s.as_bytes().iter())
        {
            *i = b;
        }
        self.cursor = usize::min(cap, self.cursor + s.as_bytes().len());
        Ok(())
    }
}

Andelf
  • 603
  • 6
  • 10
2

I build a small crate based on Shepmaster's post. There is also a macro included that allows easy use. All this works without a heap allocator and is compatible with no_std.

use arrform::{arrform, ArrForm};
 
let af = arrform!(64, "write some stuff {}: {:.2}", "foo", 42.3456);
assert_eq!("write some stuff foo: 42.35", af.as_str());

This macro first reserves a buffer on the stack. Then it uses the struct ArrForm to format text and numbers. It returns an instance of ArrForm that allows easy access to the contained text. The macro panics if the buffer is chosen too small.

see https://github.com/Simsys/arrform or https://crates.io/crates/arrform.

Coder
  • 1,415
  • 2
  • 23
  • 49
Simsys
  • 62
  • 4