Skip to content

Consider opening up more internal info #969

@diondokter

Description

@diondokter

Hello!

I'm doing work for 'Big Client' BC and we're running into some trouble with defmt.

BC already has a long established internal logging and event system which they want to keep using. It turns out it's big and opinionated enough that adding defmt support to it is infeasible (or so they tell me at least). So currently BC can't use defmt.

But they do want to use defmt as they want to use open source embedded Rust crates and also open up a lot of their stuff. Embedded Rust has semi-standardized on defmt and so they're tied to using it.

So these goals are incompatible and I'm trying to find a way to harmonize this.
The existing logging tool already has support for custom C-based string interning logs. If we had a way to customize the defmt output slightly, we could make it compatible enough for our needs.

Currently defmt is barely customizable since it doesn't expose any of its internals. There does seem to be a way that likely works for us, can be implemented in defmt in a backward compatible manner and would likely be a tiny maintenance burden (if kept as a 'use at your own risk, it's not covered by semver system).


Opening up the formatting internals so we can adapt to our own wire format.

Consider the following Rust code:

defmt::timestamp!("{=u32:us}", 0xDEADBEEF);

fn main() {
    defmt::println!(
        "hello: {} - {}",
        5,
        Test {
            a: 1,
            b: "2",
            c: 3.0
        }
    );
}

#[derive(defmt::Format)]
struct Test {
    a: u32,
    b: &'static str,
    c: f64,
}

Internally this is (part of) the trace it follows:

@istr: BDC8                   // Will contain the "hello: {} - {}" string
-- write: [C8, BD]
@u32: 0xDEADBEEF              // Timestamp
-- write: [EF, BE, AD, DE]
@fmt                          // Formatting the integer
@istr: E9D4                   // The format string for integer (will be "{=i32}")
-- write: [D4, E9]
@i32: 0x5                     // The integer value
-- write: [05, 00, 00, 00]
@fmt                          // Formatting the struct
@istr: BDC9                   // The format string for the struct
-- write: [C9, BD]
@u32: 0x1                     // struct field a
-- write: [01, 00, 00, 00]
@str: "2"                     // struct field b
@usize: 1                     // size of the string
-- write: [01, 00, 00, 00]
-- write: [32]                // Raw string contents
@f64: 3                       // struct field c
-- write: [00, 00, 00, 00, 00, 00, 08, 40]

This is a treasure trove of information. If we had access to this and if it could be modified we could create our own global logger that logs things compatible with our existing logging tool.

So my proposal is this:

  • These functions must exist somehow somewhere
  • We already forward some of these functions to the logger impl (e.g. acquire, write, etc.)
  • We could forward most of these functions to the logger trait so the user can implement them
  • We can keep backward-compat by providing the current implementations as default implementations on the trait

So what would that look like? (snippet)

pub unsafe trait Logger {
    fn acquire();
    unsafe fn flush();
    unsafe fn release();
    unsafe fn write(bytes: &[u8]);

    unsafe fn istr(address: u16) {
        write(&address.to_le_bytes());
    }
    unsafe fn bool(b: &bool) {
        u8(&(*b as u8));
    }
    unsafe fn u8(b: &u8) {
        write(&b.to_le_bytes());
    }
    // And more functions
}

If we have something like this we can intercept all the info and handle it as we need it.
For example, we might intercept the string slice and instead of writing a length and bytes, write the bytes and then null terminate it.

There are some things that need some more thought. Some of these functions (like fmt) use generics which is probably not possible using extern "Rust". And some other functions like timestamp don't directly interact with the write function. That's all still very valuable information to have, so maybe there could be some empty 'event' functions.
For example, when the Logger timestamp function is called, you'd know the next value would be the timestamp (which could be given through the u32 or u64 function).


I know this exposes a lot of the internals and that might be uncomfortable. However, in our usecase we're fine with pinning the version of defmt. So if this is 'officially' exposed, but semver unstable the onus would be on us to maintain compatibility with new enough versions.

Another thing is that maybe this should be feature gated? That would allow for removing the defmt version tag or setting it to something like 0 so that the defmt decode can refuse to touch any customized wire format.

Let me know what you think of this idea.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions