rustc frustrations: trait coherence edition

gefjon

Phoebe Goldman

Posted on September 25, 2021

rustc frustrations: trait coherence edition

Problem: for some hobby-hacking OS dev I’m doing, I need (want) to implement drivers for a few different UART consoles. The first part of these drivers I build is for writing output from my little baby OS over the UART, which I can read from a real computer. This allows me to do some basic printf debugging for the rest of my system.

A naive good-enough implementation of writing a byte to a UART is: wait in a busy-loop until the UART is ready for a byte, then push the byte into it. To write a string, just do that for every byte in the string. To do this, you need to implement two operations which are specific to the UART chip you’re using:

  • can_write: test if it’s ready to receive a byte
  • unchecked_write_byte: push the byte into it

Then, you plug those two implementations into a generic write_byte, which in turn you plug into a generic write_str, and now you can write to your UART!

To that effect, the code I wrote was:

pub trait Console {
  /// write `byte` to `self` without first verifying that `self` is ready to recieve a
  /// byte.
  unsafe fn unchecked_write_byte(&mut self, byte: u8);
  fn can_write(&mut self) -> bool;
  fn blocking_write_byte(&mut self, byte: u8) {
    block_until(|| self.can_write(), 1);
    unsafe { self.unchecked_write_byte(byte); }
  }
}

impl<T> core::fmt::Write for T
where T: Console,
{
  fn write_str(&mut self, s: &str) -> fmt::Result {
    for b in s.bytes() {
      self.blocking_write_byte(b);
    }
    Ok(())
  }
}
Enter fullscreen mode Exit fullscreen mode

Which, of course, doesn't compile. Can you spot the error? If you've written any serious amount of Rust, I'm sure you can, but for non-Rustaceans, the problem is:

error[E0119]: conflicting implementations of trait `core::fmt::Write` for type `&mut _`
  --> src/console.rs:18:1
   |
18 | / impl<T> core::fmt::Write for T
19 | | where T: Console,
20 | | {
21 | |     fn write_str(&mut self, s: &str) -> fmt::Result {
...  |
26 | |     }
27 | | }
   | |_^
   |
   = note: conflicting implementation in crate `core`:
           - impl<W> Write for &mut W
             where W: Write, W: ?Sized;
   = note: downstream crates may implement trait `console::Console` for type `&mut _`

For more information about this error, try `rustc --explain E0119`
Enter fullscreen mode Exit fullscreen mode

The rule at play here is called "trait coherence," so if you want to read more, that's your search term. The short version is that the compiler requires that there must be at most one implementation of a trait for each type; not type can have two or more competing implementations of the same trait. This saves the compiler developers from having to answer some hard questions about which implementation they should choose when offered alternatives, and mostly seems like a reasonable requirement.

The problem, from where I'm standing, is that the rules which prevent you from defining multiple conflicting trait implementations are a bit overzealous. In my case, no type ever will have conflicting implementations of Write, since none of the handful of types for which I've implemented Console are mutable references and therefore none of them get the implementation from Core for &mut W: Write + ?Sized.

But, as the error message points out, "downstream crates may implement trait console::Console for type &mut _." I'm compiling a binary, not a library, so "downstream crates" is not a meaningful concept, but whatever. Let's try making Console pub(crate), so that we know for sure that no "downstream crate" will ever implement Console for &mut W: Write + ?Sized.

Of couese, changing pub trait Console to pub(crate) trait Console doesn't change anything. Same error, same meaningless note about downstream crates.

I asked in the Rust Discord about what I should do, and the responses I got basically just made me more frustrated. To be clear, the people who responded were very helpful, and offered what I believe to be the best advice possible. It's just that rustc doesn't permit a good solution to my problem.

As is, I got two plausible suggestions, which look to me like these two implementations:

Candidate 1: write an impl Write for each implementor of Console.

This would essentially look like:

pub trait Console {
  /// write `byte` to `self` without first verifying that `self` is ready to recieve a
  /// byte.
  unsafe fn unchecked_write_byte(&mut self, byte: u8);
  fn can_write(&mut self) -> bool;
  fn blocking_write_byte(&mut self, byte: u8) {
    block_until(|| self.can_write(), 1);
    unsafe { self.unchecked_write_byte(byte); }
  }
  fn write_str(&mut self, s: &str) -> fmt::Result {
    for b in s.bytes() {
      self.blocking_write_byte(b);
    }
    Ok(())
  }
}

impl Console for Pl011 {
  unsafe fn unchecked_write_byte(&mut self, byte: u8) {
    // this part is boring & irrelevant
  }
  fn can_write(&mut self) -> bool {
    // same with this
  }
}

impl fmt::Write for Pl011 {
  fn write_str(&mut self, s: &str) -> fmt::Result {
    Console::write_str(self, s)
  }
}

impl Console for Pc16550d {
  unsafe fn unchecked_write_byte(&mut self, byte: u8) {
    // this part is boring & irrelevant
  }
  fn can_write(&mut self) -> bool {
    // same with this
  }
}

impl fmt::Write for Pc16550d {
  fn write_str(&mut self, s: &str) -> fmt::Result {
    Console::write_str(self, s)
  }
}
Enter fullscreen mode Exit fullscreen mode

Notice that the two fmt::Write implementations have the same text other than the name of the implementing type. Not my favorite...

Candidate 2: use a generic wrapper struct to implement Write.

pub trait Console {
  /// write `byte` to `self` without first verifying that `self` is ready to recieve a
  /// byte.
  unsafe fn unchecked_write_byte(&mut self, byte: u8);
  fn can_write(&mut self) -> bool;
  fn blocking_write_byte(&mut self, byte: u8) {
    block_until(|| self.can_write(), 1);
    unsafe { self.unchecked_write_byte(byte); }
  }
}

pub struct ConsoleWriter<T>(T);

impl<T> fmt::Write for ConsoleWriter<T>
where T: Console,
{
  fn write_str(&mut self, s: &str) -> fmt::Result {
    let c = &mut self.0;
    for b in s.bytes() {
      c.blocking_write_byte(b);
    }
    Ok(())
  }
}
Enter fullscreen mode Exit fullscreen mode

That part alone isn't so bad, but now I also have to wrap my Console instances in a ConsoleWriter struct, so

use spin::Mutex;
use crate::driver::uart::Pl011;

pub static CONSOLE: Mutex<Pl011> = Mutex::new(
  unsafe { Pl011::new(0x3F20_1000usize as *mut u8) }
);
Enter fullscreen mode Exit fullscreen mode

becomes

use spin::Mutex;
use crate::driver::uart::Pl011;
use crate::console::ConsoleWriter;

pub static CONSOLE: Mutex<ConsoleWriter<Pl011>> = Mutex::new(
  ConsoleWriter(unsafe { Pl011::new(0x3F20_1000usize as *mut u8) })
);
Enter fullscreen mode Exit fullscreen mode

Again, I don't love this.

My solution: just don't implement Write.

After writing each of these solutions and considering their merits and flaws, I realized that, hey, who the hell even needs core traits? If interacting with the standard library (or the little part of it I can use on a bare-metal target) is going to make me miserable, I just won't do it.

Will this make my code more brittle? Yes. Will it be harder to read, since people joining the project will have to familiarize themselves with my own custom set of traits instead of the standard traits they already know? Probably. Will I lose the ability to interoperate with libraries that use Write to implement generic functionality, and end up reinventing the wheel instead? Almost certainly. Do I care? Not at all.

💖 💪 🙅 🚩
gefjon
Phoebe Goldman

Posted on September 25, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Add context to errors
rust Add context to errors

November 16, 2023

Rust is not panacea
rust Rust is not panacea

February 24, 2021