Question about implementation of print macros

Hello :crab:,

I have a question about the implementation of print macros in Rust.
It seems that the Stdout used by print! and println! macros is wrapped by a
RefCell, and locks are also used to provide thread safety in writing to Stdout.

/* From: /src/libstd/io/stdio.rs */

thread_local! {
    /// Stdout used by print! and println! macros
    static LOCAL_STDOUT: RefCell<Option<Box<dyn Write + Send>>> = {
        RefCell::new(None)
    }
}

/* From: src/libstd/io/stdio.rs */

#[stable(feature = "rust1", since = "1.0.0")]
impl Write for Stdout {
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
        self.lock().write(buf)
    }
    fn write_vectored(&mut self, bufs: &[IoSlice<'_>]) -> io::Result<usize> {
        self.lock().write_vectored(bufs)
    }
    fn flush(&mut self) -> io::Result<()> {
        self.lock().flush()
    }
    fn write_all(&mut self, buf: &[u8]) -> io::Result<()> {
        self.lock().write_all(buf)
    }
    fn write_fmt(&mut self, args: fmt::Arguments<'_>) -> io::Result<()> {
        self.lock().write_fmt(args)
    }
}

It also seems that eventually, writing to stdout in Rust invokes a call to libc::write as below.
/* From: src/libstd/sys/unix/fd.rs */

pub fn write(&self, buf: &[u8]) -> io::Result<usize> {
    let ret = cvt(unsafe {
        libc::write(self.fd, buf.as_ptr() as *const c_void, cmp::min(buf.len(), max_len()))
    })?;
    Ok(ret as usize)
}

I picked up from the web that the write() function in glibc is thread-safe.
In that case, (confining just to Unix/Linux) is it safe to say that print macros in Rust suffer from redundant synchronization?

  • synchronization in the Rust wrapping around invocation of libc::write
  • synchronization inside the implementation of libc::write)

Please correct me if I have misunderstood anything.. Thank you for reading.

It is thread-safe in glibc, but not necessrily so in any other libc implementation. I can't find this info right now, but it's entirely possible that musl, for example, doesn't synchronize internally. And anyway, if this thread-safety is not a part of libc interface guarantees (i.e. it's not required to be so), we can't rely on the implementation.

3 Likes

Each call to write is not guaranteed to write the full buffer. So writing a single line may end up requiring several calls to write (this is what write_all does). So by locking on the full write_all, the entire line is guaranteed to be output without being interleaved with other lines printed by other threads.

6 Likes

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