How do you cast an i32 to a &str in rust without the std library?

I am writing a program for the RP2040 microcontroller and cannot use the std library. I have a function that takes in a &str and prints it on a display. How can I print an integer (i32) without using the std library?

All integer types implement core::fmt::Display, which seems like it would work for your use case.

defmt takes a different approach to printing values, delegating to the remote host running the terminal. It's more complicated to setup but removes all of the formatting machinery from the embedded binary, which can save some flash space (if that's important) and improves runtime performance for printing complex types.

It works by compressing the data going over the wire to a string identifier (which the terminal app resolves to the static format string) and a list of serialized (but unformatted) arguments.

It is a clever idea, but you might have enough CPU and memory bandwidth on RP2040 that you can do all of the printing on the device and send the entire string over serial IO. It worked out alright on one of my projects.

2 Likes

itoa crate is no_std.

3 Likes

The other answers are all correct, but the actual logic here is pretty easy to do too:

#![no_std]

#[test]
fn test() {
    assert_eq!(itoa(0).as_str(), "0");
    assert_eq!(itoa(1).as_str(), "1");
    assert_eq!(itoa(-1).as_str(), "-1");
    assert_eq!(itoa(-9).as_str(), "-9");
    assert_eq!(itoa(-10).as_str(), "-10");
    assert_eq!(itoa(1234567).as_str(), "1234567");
    assert_eq!(itoa(i32::MAX).as_str(), "2147483647");
    assert_eq!(itoa(i32::MIN + 1).as_str(), "-2147483647");
    assert_eq!(itoa(i32::MIN).as_str(), "-2147483648");
}

// Can't return str without a ref to a lifetime somewhere else,
// so instead return a buffer of the maximum i32 length
pub struct ItoaResult {
    // i32 represents values from -2147483648 to 2147483647
    buf: [u8; 11],
    len: usize,
}

impl ItoaResult {
    pub fn new() -> Self {
        Self { buf: [0u8; 11], len: 0 }
    }

    fn push(&mut self, c: u8) {
        self.len += 1;
        self.buf[11usize.checked_sub(self.len).unwrap()] = c;
    }
    
    pub fn as_str(&self) -> &str {
        core::str::from_utf8(&self.buf[11usize.checked_sub(self.len).unwrap()..]).unwrap()
    }
}

pub fn itoa(mut value: i32) -> ItoaResult {
    let mut result = ItoaResult::new();

    let neg = value < 0;

    loop {
        result.push(b'0' + u8::try_from((value % 10).abs()).unwrap());
        value /= 10;
        // check at end of loop, so 0 prints to "0"
        if value == 0 {
            break;
        }
    }
    
    if neg {
        result.push(b'-');
    }
    
    result
}

The main trouble is handling getting positive digits from i32::MIN (which you can't negate), which it turns out is simplest to handle in the laziest way, simply do the default % implementation and .abs() the result!

There's probably a nicer way to do the push(), but really it's just a paranoid self.buf[11 - self.len] = c;, which isn't that scary looking.

2 Likes

you can also just use i32::unsigned_abs which never overflows.

1 Like

Damn, I actually looked specifically for that and must have missed it next to the unchecked_* methods. I checked the x86_64 assembly generated by both (with all the unchecked methods used to get the cleanest output), and it indeed looks quite a bit nicer, adding 3 instructions before the loop and removing 6 inside the loop (going from 26 -> 20 instructions).

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.