Skip to content

Allow custom implementations of DebugWriter #4382

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

ZhekaS
Copy link

@ZhekaS ZhekaS commented Mar 25, 2025

Pull Request Overview

Fixes #4302

Add a trait for debug writer, so the downstream code can implement it's own, not depending on UART backend.

Testing Strategy

Tested on QEMU and local simulated RiscV platform with both UART based and non-UART implentations.
Tested debug buffer overflow on buffered implementation

TODO or Help Wanted

Would be nice to test with some implementation using console_ordered as it has some "intrusive" calls to the debug api

Documentation Updated

Inline documentation added in debug.rs

Formatting

  • Ran make prepush.

Sorry, something went wrong.

Copy link
Contributor

@bradjc bradjc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we want to do something like this, but I get the sense that what is in debug.rs right now is more based on what worked rather than something meant to be extensible.

I think we should create the trait, but having extract() as a member function doesn't make much sense. Probably flush() should be there instead.

The existing struct should be called DebugWriterRingBuffer or something.

@@ -303,22 +303,36 @@ pub struct DebugWriter {
count: Cell<usize>,
}

pub trait DebugWriterTrait: Write + IoWrite {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pub trait DebugWriterTrait: Write + IoWrite {
pub trait DebugWriter: Write + IoWrite {

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the draft implementation is a somewhat dirty WA intended to keep all the existing implementation as is while also allowing custom one. Ideally the empty trait functions should not be there as they are only required by specific implementations. If we are in agreement we want this, I can look into cleaner implementation when I have some free cycles...

Eugene Shamis added 2 commits April 24, 2025 16:41
@ZhekaS ZhekaS marked this pull request as ready for review April 24, 2025 21:15
Copy link
Member

@alevy alevy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically looks good to me! Seems like a good candidate for SeggerRTT, no @bradjc ?

Comment on lines +334 to +338
/// Available length of the internal buffer
fn available_len(&self) -> usize {
// Default implementation is to return maximum (for synchronous/unbuffered implementations)
usize::MAX
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove this default method implementation. It is very easy to miss overriding default methods, and as such we should only use them when they're always a sensible, correct implementation w.r.t. the abstract interface's guarantees.

In this case, many implementations may not be synchronous and unbuffered, and they should be forced to implement this method themselves faithfully to their implementation.

Comment on lines +339 to +343
/// Publish bytes from the internal buffer to the output
fn publish_bytes(&self) -> usize {
// Default implementation is to do nothing
0
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does the return value indicate? It is legal for an implementation to "publish" bytes before this method is called? Should this really be called "flush"?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should create the trait, but having extract() as a member function doesn't make much sense. Probably flush() should be there instead.

I only now saw @bradjc's comment, which I'm echoing above.

Comment on lines +344 to +348
/// Extract the internal buffer
fn extract(&self) -> Option<&mut RingBuffer<'static, u8>> {
// Default implementation has no internal buffer
None
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like the wrong place for this method. It concerns implementation details of the underlying debug writer implementation (such as it, perhaps, internally using a RingBuffer). Such a method should instead be implemented on the proper type, not this generic trait.

It doesn't seem to be used, so I'd ask to remove this method.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should create the trait, but having extract() as a member function doesn't make much sense. Probably flush() should be there instead.

Same here, agree with @bradjc.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This extract() is somewhat problematic. Adding flush() instead needs it to take another writer as a parameter to flush into, which is in turn has to be Write + IoWrite. But then it make DebugWriter "not dyn compatible" and I am not sure how to fix it.

How about splitting the trait into two like this instead:

pub trait DebugWriter {
    /// Write bytes to output
    /// The `overflow` slice is used as a message to be appended to the end of the available
    /// buffer if it becomes full.
    fn write(&self, bytes: &[u8], overflow: &[u8]) -> usize;

    /// Return a reference to self if it implements [`BufferedDebugWriter`].
    fn as_buffered(&self) -> Option<&dyn BufferedDebugWriter>;
}

pub trait BufferedDebugWriter: DebugWriter {
    /// Available length of the internal buffer
    fn available_len(&self) -> usize;

    /// Publish bytes from the internal buffer to the output
    fn publish_bytes(&self) -> usize;

    /// Extract the internal buffer
    fn extract(&self) -> Option<&RingBuffer<'static, u8>>;
}

Then buffered writers can implement both traits, while unbuffered can only implement DebugWriter. The wrapper will take care if handling the as_buffered() as in

impl DebugWriterWrapper {
    // .....
    fn extract(&self) -> Option<&RingBuffer<'static, u8>> {
        self.dw
            .and_then(|dw| dw.as_buffered())
            .and_then(|dw| dw.extract())
    }
    // ...
}

}

impl DebugWriter {
impl UartDebugWriter {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
impl UartDebugWriter {
impl RingBufferDebugWriter {

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add a DebugWriter with a non-UART backend
5 participants