Linker map shows `write_char` (and others) duplicated for embedded project

Hey Rustecean... Rusttastions... Rust...
Hey everyone,

I'm working on a little embedded project for the mipsel-sony-psx target, which compiles into a flat binary format. Since I only have 2MB of RAM available I occasionally check the memory map generated, to identify large functions and whatnot.

Today I noticed the following entry
<puddle_sdk[9d06d2269bc47416]::support::StdOut as core[7361b8d8ea116b67]::fmt::Write>::write_char being there twice. On closer look I noticed that the two functions have slightly different mangled names:

  • _RNvYNtNtCsdtQE6lUraTi_10puddle_sdk7support6StdOutNtNtCs9UaNIwQESFv_4core3fmt5Write10write_charB6_
  • _RNvYNtNtCsdtQE6lUraTi_10puddle_sdk7support6StdOutNtNtCs9UaNIwQESFv_4core3fmt5Write10write_charCskDZwcgprsr3_18puddle_integration.

With a disassembler I verified that both functions have the exact same instructions.

  • puddle-sdk is the name of my crate to handle all the low level PSX stuff
  • puddle-integration is a collection of integration tests.

puddle-integration is the executable I am compiling.

I noticed that the _RNvYNtNtCsdtQE6lUraTi_10puddle_sdk7support6StdOutNtNtCs9UaNIwQESFv_4core3fmt5Write10write_charCskDZwcgprsr3_18puddle_integration entry disappears when I do not use println in puddle-integration.

Am I implementing println inefficiently somehow?

In the puddle-sdk I implement it like this:

#[macro_export]
macro_rules! println {
    ($($args:tt)*) => {
        {
            use core::fmt::Write;
            let _ = write!($crate::support::StdOut::new(), "{}\n", format_args!($($args)*));
        }
    }
}

With StdOut implemented like this:

pub struct StdOut {}

impl StdOut {
    pub const fn new() -> StdOut {
        StdOut{}
    }
}

impl Default for StdOut {
    fn default() -> Self {
        Self::new()
    }
}

impl core::fmt::Write for StdOut {
    fn write_str(&mut self, s: &str) -> core::fmt::Result {
        use crate::psx::bios;

        // `%.*s` is printing the sub string with the specified length
        unsafe{bios::printf(c"%.*s".as_ptr(), s.len(), s.as_ptr());}
        Ok(())
    }
}

Any ideas on how to avoid duplication or why it happens?

write_char() is a provided method for fmt::Write, defined in the trait:

    fn write_char(&mut self, c: char) -> Result {
        self.write_str(c.encode_utf8(&mut [0; char::MAX_LEN_UTF8]))
    }

So, each implementation of Write gets its own write_char method with the appropriate self type. If the write_str function is not inlined into write_char, then these might produce identical machine code and be deduplicated at link time, but this is not guaranteed to happen.

(If the trait is never used with dynamic dispatch, and the particular method is never called, then it won't get compiled to machine code, but fmt::Write is always used with dynamic dispatch, so there always has to be a write_char in the vtable. In the future, final methods may allow keeping such methods out of vtables, but retrofitting it on Write would be a breaking change.)


Oops, I just noticed that your duplication has nothing to do with multiple Write implementations. Sorry! Maybe it helps someone else.