DEV Community

nikhilraojl
nikhilraojl

Posted on

Taking an input and printing stuff in rust

In python taking input from the user is as simple as using the built-in input function, assigning it to a variable and using it as we need. Below is a simple program which takes in a name from a user and prints out a greeting

name = input("Please enter your name: ")
print(f"Hello {name}")

# Output:
# Enter your name: John Smith
# Hello John Smith
Enter fullscreen mode Exit fullscreen mode

In rust, and in many low level languages, to capture a user input we first have to read from stdin and store it in a buffer which can then be used as needed. Let's see how this looks all put together

A small note on stdin, stdout, stderr

In a nutshell

  • stdin -> Standard Input, your process reads information from you
  • stdout -> Standard Output, your process writes normal output
  • stderr -> Standard Output, your process writes debug output for more detailed information read the wikipedia page on this topic StandardStreams

Let's get back to our rust program

fn main() {
    // Prints to the standard output, with a newline.
    println!("Please enter your name: ");

    // create an empty buffer before hand
    let mut name_buf = String::new();
    // create a `Stdin` object
    let stdin_handle = std::io::stdin();
    // read a line from stdin and store it in buffer `name_buf`
    stdin_handle.read_line(&mut name_buf).unwrap();

    println!("hello {}", name_buf);
}

// Output:
// Enter your name:
// John Smith
// hello John Smith
Enter fullscreen mode Exit fullscreen mode

NOTE: I am going to use unwrap instead of handling errors to have less things to worry about

If we look closely there is a small difference in the two programs. In python the prompt and name input both happen on same line but in our rust program the input happens on a new line

RUST
Enter your name:
John Smith  <- input on a new line
hello John Smith

PYTHON
Enter your name: John Smith <- input the same line
Hello John Smith
Enter fullscreen mode Exit fullscreen mode

What if we want the similar behaviour in rust? We some how need to avoid printing a new line after the prompt. For this we can use print! macro for this. Reading the print! docs ...

Prints to the standard output.

Equivalent to the println! macro except that a newline is not printed at the end of the message.

Let's try this out

fn main() {
    // Prints to the standard output, without a newline.
    print!("Greetings, please enter your name: ");

    let mut name_buf = String::new();
    let stdin_handle = std::io::stdin();
    stdin_handle.read_line(&mut name_buf).unwrap();

    println!("hello {}", name_buf);
}

// Output:
// John Smith
// Enter your name: hello John Smith
Enter fullscreen mode Exit fullscreen mode

This output was not what we expected. But before understanding what's going on let's try something that fixes this and work our way backwards. Let's try std::io::stdout().flush().unwrap() from one of the examples in the docs

// flush method comes from `Write` trait. We need this in scope
use std::io::Write;

fn main() {
    print!("Enter your name: ");
    // immediately "push"(flush) buffer to stdout
    std::io::stdout().flush().unwrap();
    let mut buf = String::new();
    std::io::stdin().read_line(&mut buf).unwrap();
    println!("hello {buf}");
}

// Output:
// Enter your name: John Smith
// hello John Smith
Enter fullscreen mode Exit fullscreen mode

Yay, we go the output we wanted. The reason for earlier "bad" output was that stdout is usually line buffered i.e once there is a new line character in the buffer, the buffer will then immediately be flushed/printed to stdout. Taking output from first print! program above analyzing

-----PROGRAM STARTS-----

John Smith
^--- input prompt in buffer is not flushed until we type name and hit enter(newline) key

Enter your name: hello John Smith
^--- prompt and  ^--- greeting are flushed to stdout together causing "bad" output

-----PROGRAM ENDS-----
Enter fullscreen mode Exit fullscreen mode

To avoid such issues we can use std::io::stdio().flush() as needed to push data to stdout immediately.

One more difference between print! & println!

Another difference between print! and println! is that the print! doesn't accept empty args but you can call println! without any args

// Program 1
fn main() {
    println!();
    println!("Hello World");
}

// Output:
//                <- empty line
// Hello World

// Program 2
fn main() {
    print!(); // <- this will fail to compile
    print!("hello World");
}

// Output:
// error: requires at least a format string argument
Enter fullscreen mode Exit fullscreen mode

To find out we can look at source of print! & println! macros

macro_rules! print {
    ($($arg:tt)*) => {{
        $crate::io::_print($crate::format_args!($($arg)*));
    }};
}

macro_rules! println {
    () => {
        $crate::print!("\n")
    };
    ($($arg:tt)*) => {{
        $crate::io::_print($crate::format_args_nl!($($arg)*));
    }};
}

Enter fullscreen mode Exit fullscreen mode

Looking at the source we find that if there are no args passed to println! we simply print a new line using, print! macro. But such is not the case for print! macro, we have to pass in some arguments or it won't compile.

Hopefully you understand a couple of differences between print! and println!. Now diving a little deeper

Diving deeper

If stdout is line buffered then it is a good guess that there must be a size limit to that buffer. Let's try to find its true and try to find the limit

Note: I am doing this on a 64 bit windows machine with 1.72 rustc. Findings may be different on different machines. This may not be the right way, I am just having fun tinkering and sharing what I found

fn main() {
    let array = [1; 500];
    print!("{array:?}");
    let mut buf = String::new();
    std::io::stdin().read_line(&mut buf).unwrap();
    println!("hello {buf}");
}

// Output:
// [1, 1, 1, 1, 1 ..(repeated around 336 times) JohnSmith
                                            //  ^ input
// ...(remaining) 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]hello JohnSmith
                                            // ^ output
Enter fullscreen mode Exit fullscreen mode

When we try to print out an array of 1s of length 500, we see some of them are printed before taking input and some after. We can then make a guess, the first portion of 1s might have been written out as it exceeded the buffer limit and remaining output stays in buffer until flushed later. We can try to test this hypothesis by increasing size of array from 500 -> 700. You can try this for yourself, you should see that the first output portion of array is doubled.

// Output:
// [1, 1, 1, 1, 1 ..(repeated around 660 times) JohnSmith
                                            //  ^ input
// ...(remaining) 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]hello JohnSmith
                                            // ^ output
Enter fullscreen mode Exit fullscreen mode

Getting a little side tracked on formatter

There was a small thing that was irking me in the above example. Why only 341 1's are printed. It must be that the debug format adding brackets, spaces and commas adding to buffer size. Okay, now on to finding the size of the debug printed array

#![feature(fmt_internals)]

use std::fmt::{Formatter, Debug};

fn main() {
    let arr_341 =  [1; 341];
    let mut fmt_buf = String::new();
    let mut fmtr = Formatter::new(&mut fmt_buf);
    arr_341.fmt(&mut fmtr).unwrap();
    println!("BUF SIZE: {}", std::mem::size_of_val(&*fmt_buf));
}

// Output:
// BUF SIZE: 1023
Enter fullscreen mode Exit fullscreen mode

There is a little more going on here than some of the earlier examples. Going from top to down

  • #![feature(fmt_internals)] -> this is a flag that allows the use of constructing Formatter object as fmt_internals is a unstable feature and shouldn't be used in normal code. Also this requires rust nightly
  • fmt_buf -> is an owned string
  • Formatter::new() -> constructs a new formatter object with the passed in buffer
  • std::mem::size_of_val() -> gives us the size of an object through shared reference
  • &*fmt_buf -> we have to dereference first because &fmt_buf is a &String fat pointer with size 24 bytes(address, length, capacity) and we want the size of what the pointer is pointing to and not the size of pointer itself. *fmt_buf is a str object and we can pass a refernce(&*fmt_buf) to this object to size_of_val function to get the correct size

Now if I increase the length of array from 341 to 342, first the size of the debug format buffer becomes 1026 and intitally 1024 are printed out and then the remaining 2 bytes. Try the below program to verify it yourself

#![feature(fmt_internals)]

use std::{fmt::{Formatter, Debug}, time::Duration};

fn main() {
    let arr_342 =  [1; 342];
    let mut fmt_buf = String::new();
    let mut fmtr = Formatter::new(&mut fmt_buf);
    arr_342.fmt(&mut fmtr).unwrap();
    print!("{arr_342:?}");

    std::thread::sleep(Duration::from_millis(5000));
    println!();
    println!("BUF {}", std::mem::size_of_val(&*fmt_buf));
}

// Output:
// [1, 1, 1, ...(repeated until 1024 bytes) |sleep 5sec| 1]
//                                                       ^^The last 2 bytes  are printed after the 5sec sleep.
Enter fullscreen mode Exit fullscreen mode

Sucker punch on our expectation

The theory so far is that the usual buffer size while writing to stdout is 1024 bytes and anything less will not be flushed immediately. Testing on an array supports this theory. Now on to try on different data structures. Let's try a String with the size of 1000 bytes or something less than our guessed size limit for output buffer

use std::{thread::sleep, time::Duration};

fn main() {
    let aa_string = "a".repeat(1000);
    print!("{aa_string}");


    sleep(Duration::from_millis(3000));

    println!();
    println!("STR SIZE: {}", std::mem::size_of_val(&*aa_string));
}

// Output:
// |Sleep for 3sec|aa... (repeated 1000 times)
// STR SIZE: 1000
Enter fullscreen mode Exit fullscreen mode

As expected, the program sleeps first as the buffer is not exceeding the limit. Now let's try it with something that exceeds the 1024 bytes

// same as prev fuc except this line
    let aa_string = "a".repeat(1224);
//

// Output:
// aa... (repeated 1225 times)|Sleep for 3sec|
// STR SIZE: 1225
Enter fullscreen mode Exit fullscreen mode

But this is not following our array example. According to our theory we should get the first 1024 bytes, sleep for 3 seconds, get remaining 200 bytes, then STR SIZE output. This example shows there is something more than just fixed buffer size for outputting stuff to stdout.

Unfortunately, this is as far as I got for now. I will dig into this topic more and will write another blog if I find something. In the mean time if any of you people reading this have more information kindly point me to those resources. I will be very grateful.

Whew, that's a long story on print! & println! . I had fun learning and experimenting about these things and hopefully you found some of it interesting. Thanks for reading and until next one

Top comments (0)