Phoebe Goldman
Posted on September 25, 2021
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(())
}
}
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`
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)
}
}
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(())
}
}
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) }
);
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) })
);
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.
Posted on September 25, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.