knurling-rs / defmt

Efficient, deferred formatting for logging on embedded systems
https://defmt.ferrous-systems.com/
Apache License 2.0
820 stars 76 forks source link

Add WebAssembly support to defmt-print #738

Open ia0 opened 1 year ago

ia0 commented 1 year ago

It is currently not possible to do defmt-print -e foo.wasm because the decoder expect an ELF and fails with Unknown file magic.

It looks like under the hood defmt-decoder uses the object crate which seems to support WebAssembly through the wasm feature. Besides, it looks like the necessary data is already in the wasm file (I can see many .defmt.* custom sections with data looking like formatting strings). So it looks like what is missing is support in defmt-decoder and maybe defmt-print.

If such support would be added, it would be possible to use defmt when targeting WebAssembly.

What do you think?

Here's an example file to play with:

// src/lib.rs
#![no_std]

use core::sync::atomic::AtomicBool;
use core::sync::atomic::Ordering::SeqCst;

#[no_mangle]
pub extern "C" fn main() {
    defmt::panic!("search {} me", 4);
}

#[panic_handler]
fn panic(_: &core::panic::PanicInfo) -> ! {
    core::arch::wasm32::unreachable();
}

#[defmt::global_logger]
struct Logger;

static ACQUIRED: AtomicBool = AtomicBool::new(false);
static mut ENCODER: defmt::Encoder = defmt::Encoder::new();

unsafe impl defmt::Logger for Logger {
    fn acquire() {
        assert!(!ACQUIRED.swap(true, SeqCst));
        // SAFETY: We are in a critical section.
        let encoder = unsafe { &mut ENCODER };
        encoder.start_frame(write);
    }

    unsafe fn flush() {}

    unsafe fn release() {
        // SAFETY: We are in a critical section.
        let encoder = unsafe { &mut ENCODER };
        encoder.end_frame(write);

        assert!(ACQUIRED.swap(false, SeqCst));
    }

    unsafe fn write(bytes: &[u8]) {
        // SAFETY: We are in a critical section.
        let encoder = unsafe { &mut ENCODER };
        encoder.write(bytes, write);
    }
}

fn write(bytes: &[u8]) {
    extern "C" {
        fn print(x: u32);
    }

    for &byte in bytes {
        unsafe { print(byte as u32) };
    }
}
# Cargo.toml
[package]
name = "bar"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
defmt = "0.3.2"
% cargo build --release --target=wasm32-unknown-unknown

% wasm-dis target/wasm32-unknown-unknown/release/bar.wasm | grep 'search {} me'
 (export "{"package":"bar","tag":"defmt_error","data":"panicked at 'search {} me'","disambiguator":"15898833218959482489"}" (global $global$1))
 ;; custom section ".defmt.{"package":"bar","tag":"defmt_error","data":"panicked at 'search {} me'","disambiguator":"15898833218959482489"}", size 1

% wasm-interp target/wasm32-unknown-unknown/debug/bar.wasm --dummy-import-func --run-all-exports \
  | sed -n 's/^.*env\.print(i32:\(.*\)) =>/\1/p' \
  | while read x; do printf '%02x' $x; done | xxd -p -r \
  | defmt-print -e target/wasm32-unknown-unknown/debug/bar.wasm

Just remove the last line (the one with defmt-print) to see that the output is reasonable (it looks like normal defmt output just waiting to be decoded).

Urhengulas commented 1 year ago

Interesting thought. You can try to patch defmt-decoder to enable the wasm feature in object. Then just use this patched version with defmt-print and it might already work. Please report how that goes!

ia0 commented 1 year ago

Thanks, I'll try it when I'll get some time (might be in a few months) and report here.

LaniKossmannPC commented 1 year ago

adding WebAssembly support to defmt-print would be a great addition! It's exciting to see that the necessary data is already present in the wasm file, and that support in defmt-decoder and defmt-print is all that's needed to make it work.

ia0 commented 1 year ago

Quick update

I started working on this and realized that:

  1. The object crate doesn't populate the address of symbols in wasm. I created https://github.com/gimli-rs/object/issues/538 for this and have something working locally.
  2. The symbol address in the wasm module is (kind of) wrong. Wasm doesn't have a concept of ELF section, so all symbols are in a single bag. Also, I don't think there is a notion of linker script, so I'm not sure how to replicated what defmt.x is doing (i.e. force all symbols to be in a virtual section starting at address 1). However, when using the lower 16-bits of the symbol address, it looks like it's working.

If I get more time later (and https://github.com/gimli-rs/object/pull/539 gets merged), I'll try to send a draft PR to demonstrate a possible solution for this issue, such that we can discuss whether it's something we want.

EDIT: Note to myself: Make sure wasm-strip/wasm-opt removes those exports and globals.

ia0 commented 1 year ago

I wrote a longer update here: https://github.com/gimli-rs/object/pull/539#issuecomment-1520915958.

I don't think this will be easy. The main difficulty will be to find a way to generate unique identifiers without consuming space in the Wasm linear memory. One idea I had was to use multi-memory, allocate the symbols in that memory, and strip it. Essentially using a memory as a section. But the support for multi-memory in Rust is not there yet.

Not sure if we should close the issue and reopen when feasible (or a new idea is found), or keep it open for tracking purposes.

ia0 commented 1 year ago

As suggested by @bjorn3 (https://github.com/gimli-rs/object/pull/539#issuecomment-1521627995) we could just give up on optimizing data transfer and use hashes instead of increasing sequence for identifiers. This shouldn't impact binary size since the address is unknown until linking. Only optimization passes after the symbols get their address would be impacted.

Urhengulas commented 1 year ago

I think we can keep the issue open