pw_log: Add core::fmt style format string support to Rust API

Change-Id: Ia48951608a931f74882eaa70b5579e72598b2d87
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/207331
Lint: Lint 🤖 <android-build-ayeaye@system.gserviceaccount.com>
Reviewed-by: Taylor Cramer <cramertj@google.com>
Pigweed-Auto-Submit: Erik Gilling <konkers@google.com>
Commit-Queue: Auto-Submit <auto-submit@pigweed-service-accounts.iam.gserviceaccount.com>
diff --git a/pw_log/rust/backend_tests.rs b/pw_log/rust/backend_tests.rs
index c7e9cd0..4539666 100644
--- a/pw_log/rust/backend_tests.rs
+++ b/pw_log/rust/backend_tests.rs
@@ -14,12 +14,16 @@
 #[cfg(test)]
 mod tests {
     use crate::run_with_capture;
-    use pw_log_backend::pw_logf_backend;
+    use pw_log_backend::{pw_log_backend, pw_logf_backend};
     use pw_log_backend_api::LogLevel;
 
     #[test]
     fn no_argument_log_line_prints_to_stdout() {
         assert_eq!(
+            run_with_capture(|| pw_log_backend!(LogLevel::Info, "test")),
+            "[INF] test\n"
+        );
+        assert_eq!(
             run_with_capture(|| pw_logf_backend!(LogLevel::Info, "test")),
             "[INF] test\n"
         );
@@ -59,6 +63,11 @@
     #[test]
     fn untyped_i32_argument_prints_to_stdout() {
         assert_eq!(
+            run_with_capture(|| pw_log_backend!(LogLevel::Info, "test {}", -1 as i32)),
+            "[INF] test -1\n",
+        );
+
+        assert_eq!(
             run_with_capture(|| pw_logf_backend!(LogLevel::Info, "test %v", -1 as i32)),
             "[INF] test -1\n",
         );
@@ -66,6 +75,11 @@
     #[test]
     fn untyped_u32_argument_prints_to_stdout() {
         assert_eq!(
+            run_with_capture(|| pw_log_backend!(LogLevel::Info, "test {}", 1 as u32)),
+            "[INF] test 1\n",
+        );
+
+        assert_eq!(
             run_with_capture(|| pw_logf_backend!(LogLevel::Info, "test %v", 1 as u32)),
             "[INF] test 1\n",
         );
@@ -74,6 +88,11 @@
     #[test]
     fn untyped_str_argument_prints_to_stdout() {
         assert_eq!(
+            run_with_capture(|| pw_log_backend!(LogLevel::Info, "test {}", "Pigweed" as &str)),
+            "[INF] test Pigweed\n",
+        );
+
+        assert_eq!(
             run_with_capture(|| pw_logf_backend!(LogLevel::Info, "test %v", "Pigweed" as &str)),
             "[INF] test Pigweed\n",
         );
diff --git a/pw_log/rust/pw_log.rs b/pw_log/rust/pw_log.rs
index a1c58a8..924d91b 100644
--- a/pw_log/rust/pw_log.rs
+++ b/pw_log/rust/pw_log.rs
@@ -21,20 +21,24 @@
 //! This flexibility is accomplished using Pigweed's
 //! [facade pattern](https://pigweed.dev/docs/facades.html),
 //! which uses build-system redirection to forward log invocations to the
-//! configured backend implementaiton.
+//! configured backend implementation.
 //!
 //! ```
-//! use pw_log::{logf, LogLevel};
+//! use pw_log::{log, info, LogLevel};
 //!
-//! logf!(LogLevel::Info, "Thank you for signing up for Log Facts!");
-//! logf!(LogLevel::Info, "Log Fact: Logs can be either %s, %s, or %s sawn.",
-//!   "flat", "quarter", "rift");
+//! log!(LogLevel::Info, "Thank you for signing up for Log Facts!");
+//! info!("Log Fact: Logs can be either {}, {}, or {} sawn.",
+//!   "flat" as &str, "quarter" as &str, "rift" as &str);
 //! ```
 //!
-//! Today `printf` style format strings are supported with Rust
-//! [`core::fmt`]/`println!()` style strings planned
+//! Today `printf` style format strings are well supported with Rust
+//! [`core::fmt`]/`println!()` style strings partially supported
 //! ([b/311232607](https://issues.pigweed.dev/issues/311232607)).
 //!
+//! Currently, when using a `stable` toolchain, "untyped" arguments (i.e.
+//! `{}` style) need to be in the form of an as-cast.  Users with nightly
+//! toolchains can enable the `nightly_tait` feature to remove this restriction.
+//!
 //! TODO: <pwbug.dev/311266298> - Document `pw_log`'s backend API.
 //!
 //! TODO: <pwbug.dev/311232605> - Document how to configure facade backends.
@@ -47,13 +51,37 @@
 #[doc(hidden)]
 pub mod __private {
     pub use crate::*;
-    pub use pw_log_backend;
-    pub use pw_log_backend::pw_logf_backend;
+    // pub use pw_log_backend;
+    pub use pw_log_backend::{pw_log_backend, pw_logf_backend};
+}
+
+/// Emit a log message using `core::fmt` format string semantics.
+///
+/// `log` takes a [`LogLevel`], a `core::fmt` style format string, and necessary
+/// arguments to that string and emits a log message to the logging backend.
+///
+/// ```
+/// use pw_log::{log, LogLevel};
+///
+/// log!(LogLevel::Info, "Log fact: A {} log has a Janka hardness of {} lbf.",
+///      "Spruce Pine" as &str, 700 as i32);
+/// ```
+#[macro_export]
+macro_rules! log {
+  ($log_level:expr, $format_string:literal) => {{
+    use $crate::__private as __pw_log_crate;
+    $crate::__private::pw_log_backend!($log_level, $format_string)
+  }};
+
+  ($log_level:expr, $format_string:literal, $($args:expr),*) => {{
+    use $crate::__private as __pw_log_crate;
+    $crate::__private::pw_log_backend!($log_level, $format_string, $($args),*)
+  }};
 }
 
 /// Emit a log message using `printf` format string semantics.
 ///
-/// `logf` takes a [`LogLevel`], a `printf style format string, and necessary
+/// `logf` takes a [`LogLevel`], a `printf` style format string, and necessary
 /// arguments to that string and emits a log message to the logging backend.
 ///
 /// ```
@@ -83,10 +111,26 @@
   }}
 }
 
+/// Emit a debug level log message using `core:fmt` format string semantics.
+///
+/// ```
+/// use pw_log::debug;
+///
+/// debug!("Log Fact: The American toy Lincoln Logs were inspired by the {} in {}.",
+///     "Imperial Hotel" as &str, "Tokyo" as &str);
+/// ```
+#[macro_export]
+macro_rules! debug {
+  ($($args:expr),*) => {{
+    use $crate::__private as __pw_log_crate;
+    __pw_log_crate::log!(__pw_log_crate::LogLevel::Debug, $($args),*)
+  }};
+}
+
 /// Emit a debug level log message using `printf` format string semantics.
 ///
 /// ```
-/// use pw_log::{debugf, LogLevel};
+/// use pw_log::debugf;
 ///
 /// debugf!("Log Fact: The American toy Lincoln Logs were inspired by the %s in %s.",
 ///     "Imperial Hotel", "Tokyo");
@@ -107,10 +151,27 @@
   }}
 }
 
+/// Emit an info level log message using `core:fmt` format string semantics.
+///
+/// ```
+/// use pw_log::info;
+///
+/// info!(
+///     "Log Fact: The American president Abraham Lincoln (born {}) once lived in a log cabin.",
+///     1809 as u32);
+/// ```
+#[macro_export]
+macro_rules! info {
+  ($($args:expr),*) => {{
+    use $crate::__private as __pw_log_crate;
+    __pw_log_crate::log!(__pw_log_crate::LogLevel::Info, $($args),*)
+  }};
+}
+
 /// Emit an info level log message using `printf` format string semantics.
 ///
 /// ```
-/// use pw_log::{infof, LogLevel};
+/// use pw_log::infof;
 ///
 /// infof!(
 ///     "Log Fact: The American president Abraham Lincoln (born %x) once lived in a log cabin.",
@@ -132,10 +193,27 @@
   }}
 }
 
+/// Emit a warn level log message using `core::fmt` format string semantics.
+///
+/// ```
+/// use pw_log::warn;
+///
+/// warn!(
+///     "Log Fact: Made from a log, an {} year old dugout canoe is the oldest discovered boat in {}.",
+///     8000 as i32, "Africa" as &str);
+/// ```
+#[macro_export]
+macro_rules! warn {
+  ($($args:expr),*) => {{
+    use $crate::__private as __pw_log_crate;
+    __pw_log_crate::log!(__pw_log_crate::LogLevel::Warn, $($args),*)
+  }};
+}
+
 /// Emit a warn level log message using `printf` format string semantics.
 ///
 /// ```
-/// use pw_log::{warnf, LogLevel};
+/// use pw_log::warnf;
 ///
 /// warnf!(
 ///     "Log Fact: Made from a log, an %d year old dugout canoe is the oldest discovered boat in %s.",
@@ -157,10 +235,27 @@
   }}
 }
 
+/// Emit an error level log message using `core::fmt` format string semantics.
+///
+/// ```
+/// use pw_log::error;
+///
+/// error!(
+///     "Log Fact: Before saws were invented, the {} was used prepare logs for use.",
+///     "adze" as &str);
+/// ```
+#[macro_export]
+macro_rules! error {
+  ($($args:expr),*) => {{
+    use $crate::__private as __pw_log_crate;
+    __pw_log_crate::log!(__pw_log_crate::LogLevel::Error, $($args),*)
+  }};
+}
+
 /// Emit an error level log message using `printf` format string semantics.
 ///
 /// ```
-/// use pw_log::{errorf, LogLevel};
+/// use pw_log::errorf;
 ///
 /// errorf!(
 ///     "Log Fact: Before saws were invented, the %s was used prepare logs for use.",
@@ -182,6 +277,23 @@
   }}
 }
 
+/// Emit a critical level log message using `core::fmt` format string semantics.
+///
+/// ```
+/// use pw_log::critical;
+///
+/// critical!(
+///     "Log Fact: Until the {}th century, all ships' masts were made from a single log.",
+///     19 as u32);
+/// ```
+#[macro_export]
+macro_rules! critical {
+  ($($args:expr),*) => {{
+    use $crate::__private as __pw_log_crate;
+    __pw_log_crate::log!(__pw_log_crate::LogLevel::Critical, $($args),*)
+  }};
+}
+
 /// Emit a critical level log message using `printf` format string semantics.
 ///
 /// ```
@@ -207,6 +319,23 @@
   }}
 }
 
+/// Emit a fatal level log message using `core::fmt` format string semantics.
+///
+/// *Note*: `fatal` only emits a log message and does not cause a `panic!()`
+///
+/// ```
+/// use pw_log::fatal;
+///
+/// fatal!("Log Fact: All out of log facts! Timber!");
+/// ```
+#[macro_export]
+macro_rules! fatal {
+  ($($args:expr),*) => {{
+    use $crate::__private as __pw_log_crate;
+    __pw_log_crate::log!(__pw_log_crate::LogLevel::Fatal, $($args),*)
+  }};
+}
+
 /// Emit a fatal level log message using `printf` format string semantics.
 ///
 /// *Note*: `fatalf` only emits a log message and does not cause a `panic!()`
diff --git a/pw_log/rust/pw_log_backend_printf/lib.rs b/pw_log/rust/pw_log_backend_printf/lib.rs
index f67abdb..704003e 100644
--- a/pw_log/rust/pw_log_backend_printf/lib.rs
+++ b/pw_log/rust/pw_log_backend_printf/lib.rs
@@ -29,7 +29,6 @@
 //! TODO: <pwbug.dev/311232605> - Document how to configure facade backends.
 
 #![deny(missing_docs)]
-pub use pw_log_backend_printf_macro::_pw_logf_backend;
 
 pub mod varargs;
 
@@ -39,7 +38,7 @@
     use core::ffi::{c_int, c_uchar};
 
     pub use pw_bytes::concat_static_strs;
-    pub use pw_log_backend_printf_macro::_pw_logf_backend;
+    pub use pw_log_backend_printf_macro::{_pw_log_backend, _pw_logf_backend};
 
     use pw_log_backend_api::LogLevel;
 
@@ -123,10 +122,19 @@
 
 /// Implements the `pw_log` backend api.
 #[macro_export]
+macro_rules! pw_log_backend {
+    ($log_level:expr, $format_string:literal $(, $args:expr)* $(,)?) => {{
+        use $crate::__private as __pw_log_backend_crate;
+        $crate::__private::_pw_log_backend!($log_level, $format_string,  $($args),*)
+    }};
+}
+
+/// Implements the `pw_log` backend api.
+#[macro_export]
 macro_rules! pw_logf_backend {
     ($log_level:expr, $format_string:literal $(, $args:expr)* $(,)?) => {{
         use $crate::__private as __pw_log_backend_crate;
-        $crate::_pw_logf_backend!($log_level, $format_string,  $($args),*)
+        $crate::__private::_pw_logf_backend!($log_level, $format_string,  $($args),*)
     }};
 }
 
diff --git a/pw_log/rust/pw_log_backend_printf_macro.rs b/pw_log/rust/pw_log_backend_printf_macro.rs
index 992bf9d..75f58cc 100644
--- a/pw_log/rust/pw_log_backend_printf_macro.rs
+++ b/pw_log/rust/pw_log_backend_printf_macro.rs
@@ -21,27 +21,27 @@
 };
 
 use pw_format::macros::{
-    generate_printf, Arg, FormatAndArgsFlavor, PrintfFormatMacroGenerator,
-    PrintfFormatStringFragment, PrintfFormatStringParser, Result,
+    generate_printf, Arg, CoreFmtFormatStringParser, FormatAndArgsFlavor, FormatStringParser,
+    PrintfFormatMacroGenerator, PrintfFormatStringFragment, PrintfFormatStringParser, Result,
 };
 
 type TokenStream2 = proc_macro2::TokenStream;
 
-// Arguments to `pw_logf_backend`.  A log level followed by a [`pw_format`]
+// Arguments to `pw_log[f]_backend`.  A log level followed by a [`pw_format`]
 // format string.
 #[derive(Debug)]
-struct PwLogfArgs {
+struct PwLogArgs<T: FormatStringParser> {
     log_level: Expr,
-    format_and_args: FormatAndArgsFlavor<PrintfFormatStringParser>,
+    format_and_args: FormatAndArgsFlavor<T>,
 }
 
-impl Parse for PwLogfArgs {
+impl<T: FormatStringParser> Parse for PwLogArgs<T> {
     fn parse(input: ParseStream) -> syn::parse::Result<Self> {
         let log_level: Expr = input.parse()?;
         input.parse::<Token![,]>()?;
         let format_and_args: FormatAndArgsFlavor<_> = input.parse()?;
 
-        Ok(PwLogfArgs {
+        Ok(PwLogArgs {
             log_level,
             format_and_args,
         })
@@ -148,8 +148,20 @@
 }
 
 #[proc_macro]
+pub fn _pw_log_backend(tokens: TokenStream) -> TokenStream {
+    let input = parse_macro_input!(tokens as PwLogArgs<CoreFmtFormatStringParser>);
+
+    let generator = LogfGenerator::new(&input.log_level);
+
+    match generate_printf(generator, input.format_and_args.into()) {
+        Ok(token_stream) => token_stream.into(),
+        Err(e) => e.to_compile_error().into(),
+    }
+}
+
+#[proc_macro]
 pub fn _pw_logf_backend(tokens: TokenStream) -> TokenStream {
-    let input = parse_macro_input!(tokens as PwLogfArgs);
+    let input = parse_macro_input!(tokens as PwLogArgs<PrintfFormatStringParser>);
 
     let generator = LogfGenerator::new(&input.log_level);
 
diff --git a/pw_log/rust/pw_log_backend_println.rs b/pw_log/rust/pw_log_backend_println.rs
index 3f4ecdc..f8a6704 100644
--- a/pw_log/rust/pw_log_backend_println.rs
+++ b/pw_log/rust/pw_log_backend_println.rs
@@ -19,11 +19,13 @@
 //! *Note*: This modules requires `std`.
 //!
 //! TODO: <pwbug.dev/311232605> - Document how to configure facade backends.
-pub use pw_log_backend_println_macro::_pw_logf_backend;
 
 #[doc(hidden)]
 pub mod __private {
+    pub use pw_log_backend_println_macro::{_pw_log_backend, _pw_logf_backend};
+
     use pw_log_backend_api::LogLevel;
+
     pub const fn log_level_tag(level: LogLevel) -> &'static str {
         match level {
             LogLevel::Debug => "DBG",
@@ -38,9 +40,17 @@
 
 // Implement the `pw_log` backend API.
 #[macro_export]
+macro_rules! pw_log_backend {
+  ($log_level:expr, $format_string:literal $(, $args:expr)* $(,)?) => {{
+    use $crate::__private as __pw_log_backend_crate;
+    $crate::__private::_pw_log_backend!($log_level, $format_string, $($args),*);
+  }};
+}
+
+#[macro_export]
 macro_rules! pw_logf_backend {
   ($log_level:expr, $format_string:literal $(, $args:expr)* $(,)?) => {{
     use $crate::__private as __pw_log_backend_crate;
-    $crate::_pw_logf_backend!($log_level, $format_string, $($args),*);
+    $crate::__private::_pw_logf_backend!($log_level, $format_string, $($args),*);
   }};
 }
diff --git a/pw_log/rust/pw_log_backend_println_macro.rs b/pw_log/rust/pw_log_backend_println_macro.rs
index 48a7163..35e43a5 100644
--- a/pw_log/rust/pw_log_backend_println_macro.rs
+++ b/pw_log/rust/pw_log_backend_println_macro.rs
@@ -21,27 +21,27 @@
 };
 
 use pw_format::macros::{
-    generate_core_fmt, Arg, CoreFmtFormatMacroGenerator, FormatAndArgsFlavor,
-    PrintfFormatStringParser, Result,
+    generate_core_fmt, Arg, CoreFmtFormatMacroGenerator, CoreFmtFormatStringParser,
+    FormatAndArgsFlavor, FormatStringParser, PrintfFormatStringParser, Result,
 };
 
 type TokenStream2 = proc_macro2::TokenStream;
 
-// Arguments to `pw_logf_backend`.  A log level followed by a [`pw_format`]
+// Arguments to `pw_log[f]_backend`.  A log level followed by a [`pw_format`]
 // format string.
 #[derive(Debug)]
-struct PwLogfArgs {
+struct PwLogArgs<T: FormatStringParser> {
     log_level: Expr,
-    format_and_args: FormatAndArgsFlavor<PrintfFormatStringParser>,
+    format_and_args: FormatAndArgsFlavor<T>,
 }
 
-impl Parse for PwLogfArgs {
+impl<T: FormatStringParser> Parse for PwLogArgs<T> {
     fn parse(input: ParseStream) -> syn::parse::Result<Self> {
         let log_level: Expr = input.parse()?;
         input.parse::<Token![,]>()?;
         let format_and_args: FormatAndArgsFlavor<_> = input.parse()?;
 
-        Ok(PwLogfArgs {
+        Ok(PwLogArgs {
             log_level,
             format_and_args,
         })
@@ -106,8 +106,20 @@
 }
 
 #[proc_macro]
+pub fn _pw_log_backend(tokens: TokenStream) -> TokenStream {
+    let input = parse_macro_input!(tokens as PwLogArgs<CoreFmtFormatStringParser>);
+
+    let generator = LogfGenerator::new(&input.log_level);
+
+    match generate_core_fmt(generator, input.format_and_args.into()) {
+        Ok(token_stream) => token_stream.into(),
+        Err(e) => e.to_compile_error().into(),
+    }
+}
+
+#[proc_macro]
 pub fn _pw_logf_backend(tokens: TokenStream) -> TokenStream {
-    let input = parse_macro_input!(tokens as PwLogfArgs);
+    let input = parse_macro_input!(tokens as PwLogArgs<PrintfFormatStringParser>);
 
     let generator = LogfGenerator::new(&input.log_level);
 
diff --git a/pw_rust/examples/tokenized_logging/main.rs b/pw_rust/examples/tokenized_logging/main.rs
index 74f6ea6..70de7b2 100644
--- a/pw_rust/examples/tokenized_logging/main.rs
+++ b/pw_rust/examples/tokenized_logging/main.rs
@@ -30,17 +30,27 @@
 // Semihosting support which is well supported for QEMU targets.
 use cortex_m_semihosting::{debug, hprintln};
 
-use pw_log::{infof, warnf};
+use pw_log::{critical, infof, warnf};
 
 #[entry]
 fn main() -> ! {
     // Plain text printout without `pw_log`
     hprintln!("Hello, Pigweed!");
 
-    // `pw_log` messages
+    // printf style `pw_log` messages
     infof!("Bare string");
     warnf!("Integer value %d", 42);
-    infof!("generic arguments %v %v", 42 as u32, -42 as i32);
+
+    // core::fmt style `pw_log` messages
+    //
+    // Note the support for this is in progress with the following notes:
+    // * only `u32`, `i32`, and '&str' arguments are supported right now.
+    // * arguments must be annotated with `as <type>` to work with `stable`
+    //   toolchains.
+    //
+    // Both of these will be addressed in the future.
+    critical!("Generic rusty arguments {} {}", 42 as u32, -42 as i32);
+
     debug::exit(debug::EXIT_SUCCESS);
     loop {}
 }
diff --git a/pw_rust/examples/tokenized_logging/pw_log_backend.rs b/pw_rust/examples/tokenized_logging/pw_log_backend.rs
index 96a5c0a..d3c84c0 100644
--- a/pw_rust/examples/tokenized_logging/pw_log_backend.rs
+++ b/pw_rust/examples/tokenized_logging/pw_log_backend.rs
@@ -26,7 +26,7 @@
     use pw_tokenizer::MessageWriter;
 
     // Re-export for use by the `pw_logf_backend!` macro.
-    pub use pw_tokenizer::tokenize_to_writer;
+    pub use pw_tokenizer::{tokenize_core_fmt_to_writer, tokenize_printf_to_writer};
 
     const ENCODE_BUFFER_SIZE: usize = 32;
 
@@ -80,16 +80,29 @@
 }
 
 // Implement the `pw_log` backend API.
+//
+// Since we're logging to a shared/ambient resource we can use
+// tokenize_*_to_writer!` instead of `tokenize_*_to_buffer!` and avoid the
+// overhead of initializing any intermediate buffers or objects.
+//
+// Uses `pw_format` special `PW_FMT_CONCAT` operator to prepend a place to
+// print the log level.
+#[macro_export]
+macro_rules! pw_log_backend {
+  ($log_level:expr, $format_string:literal $(, $args:expr)* $(,)?) => {{
+    let _ = $crate::__private::tokenize_core_fmt_to_writer!(
+      $crate::__private::LogMessageWriter,
+      "[{}] " PW_FMT_CONCAT $format_string,
+      $crate::__private::log_level_tag($log_level) as &str,
+      $($args),*);
+  }};
+}
+
 #[macro_export]
 macro_rules! pw_logf_backend {
   ($log_level:expr, $format_string:literal $(, $args:expr)* $(,)?) => {{
-    // Since we're logging to a shared/ambient resource we can use
-    // tokenize_to_writer!` instead of `tokenize_to_buffer!` and avoid the
-    // overhead of initializing any intermediate buffers or objects.
-    let _ = $crate::__private::tokenize_to_writer!(
+    let _ = $crate::__private::tokenize_printf_to_writer!(
       $crate::__private::LogMessageWriter,
-      // Use `pw_format` special `PW_FMT_CONCAT` operator to prepend a place to
-      // print the log level.
       "[%s] " PW_FMT_CONCAT $format_string,
       $crate::__private::log_level_tag($log_level),
       $($args),*);
diff --git a/pw_tokenizer/rust/pw_tokenizer/lib.rs b/pw_tokenizer/rust/pw_tokenizer/lib.rs
index 23c0133..faee9a2 100644
--- a/pw_tokenizer/rust/pw_tokenizer/lib.rs
+++ b/pw_tokenizer/rust/pw_tokenizer/lib.rs
@@ -23,21 +23,35 @@
 //! For a more in depth explanation of the systems design and motivations,
 //! see [Pigweed's pw_tokenizer module documentation](https://pigweed.dev/pw_tokenizer/).
 //!
-//! # Example
+//! # Examples
+//!
+//! Pigweed's tokenization database uses `printf` style strings internally so
+//! those are supported directly.
 //!
 //! ```
-//! use pw_tokenizer::tokenize_to_buffer;
+//! use pw_tokenizer::tokenize_printf_to_buffer;
 //!
-//! # fn doctest() -> pw_status::Result<()> {
 //! let mut buffer = [0u8; 1024];
-//! let len = tokenize_to_buffer!(&mut buffer, "The answer is %d", 42)?;
+//! let len = tokenize_printf_to_buffer!(&mut buffer, "The answer is %d", 42)?;
 //!
 //! // 4 bytes used to encode the token and one to encode the value 42.  This
 //! // is a **3.5x** reduction in size compared to the raw string!
 //! assert_eq!(len, 5);
-//! # Ok(())
-//! # }
-//! # doctest().unwrap();
+//! # Ok::<(), pw_status::Error>(())
+//! ```
+//!
+//! We also support Rust's `core::fmt` style syntax.  These format strings are
+//! converted to `printf` style at compile time to maintain compatibly with the
+//! rest of the Pigweed tokenizer ecosystem.  The below example produces the
+//! same token and output as the above one.
+//!
+//! ```
+//! use pw_tokenizer::tokenize_core_fmt_to_buffer;
+//!
+//! let mut buffer = [0u8; 1024];
+//! let len = tokenize_core_fmt_to_buffer!(&mut buffer, "The answer is {}", 42 as i32)?;
+//! assert_eq!(len, 5);
+//! # Ok::<(), pw_status::Error>(())
 //! ```
 
 #![cfg_attr(not(feature = "std"), no_std)]
@@ -63,7 +77,10 @@
     pub use pw_status::Result;
     pub use pw_stream::{Cursor, Seek, WriteInteger, WriteVarint};
     pub use pw_tokenizer_core::hash_string;
-    pub use pw_tokenizer_macro::{_token, _tokenize_to_buffer, _tokenize_to_writer};
+    pub use pw_tokenizer_macro::{
+        _token, _tokenize_core_fmt_to_buffer, _tokenize_core_fmt_to_writer,
+        _tokenize_printf_to_buffer, _tokenize_printf_to_writer,
+    };
 }
 
 /// Return the [`u32`] token for the specified string and add it to the token
@@ -101,8 +118,9 @@
     }};
 }
 
-/// Tokenize a format string and arguments to an [`AsMut<u8>`] buffer and add
-/// the format string's token to the token database.
+/// Tokenize a `core::fmt` style format string and arguments to an [`AsMut<u8>`]
+/// buffer.  The format string is converted in to a `printf` and added token to
+/// the token database.
 ///
 /// See [`token`] for an explanation on how strings are tokenized and entries
 /// are added to the token database.
@@ -120,18 +138,18 @@
 /// # Example
 ///
 /// ```
-/// use pw_tokenizer::tokenize_to_buffer;
+/// use pw_tokenizer::tokenize_core_fmt_to_buffer;
 ///
 /// // Tokenize a format string and argument into a buffer.
 /// let mut buffer = [0u8; 1024];
-/// let len = tokenize_to_buffer!(&mut buffer, "The answer is %d", 42)?;
+/// let len = tokenize_core_fmt_to_buffer!(&mut buffer, "The answer is {}", 42 as i32)?;
 ///
 /// // 4 bytes used to encode the token and one to encode the value 42.
 /// assert_eq!(len, 5);
 ///
 /// // The format string can be composed of multiple strings literals using
 /// // the custom`PW_FMT_CONCAT` operator.
-/// let len = tokenize_to_buffer!(&mut buffer, "Hello " PW_FMT_CONCAT "Pigweed")?;
+/// let len = tokenize_core_fmt_to_buffer!(&mut buffer, "Hello " PW_FMT_CONCAT "Pigweed")?;
 ///
 /// // Only a single 4 byte token is emitted after concatenation of the string
 /// // literals above.
@@ -139,10 +157,63 @@
 /// # Ok::<(), pw_status::Error>(())
 /// ```
 #[macro_export]
-macro_rules! tokenize_to_buffer {
+macro_rules! tokenize_core_fmt_to_buffer {
     ($buffer:expr, $($format_string:literal)PW_FMT_CONCAT+ $(, $args:expr)* $(,)?) => {{
       use $crate::__private as __pw_tokenizer_crate;
-      __pw_tokenizer_crate::_tokenize_to_buffer!($buffer, $($format_string)PW_FMT_CONCAT+, $($args),*)
+      __pw_tokenizer_crate::_tokenize_core_fmt_to_buffer!($buffer, $($format_string)PW_FMT_CONCAT+, $($args),*)
+    }};
+}
+
+/// Tokenize a printf format string and arguments to an [`AsMut<u8>`] buffer
+/// and add the format string's token to the token database.
+///
+/// See [`token`] for an explanation on how strings are tokenized and entries
+/// are added to the token database.  The token's domain is set to "".
+///
+/// Returns a [`pw_status::Result<usize>`] the number of bytes written to the buffer.
+///
+/// `tokenize_to_buffer!` supports concatenation of format strings as described
+/// in [`pw_format::macros::FormatAndArgs`].
+///
+/// # Errors
+/// - [`pw_status::Error::OutOfRange`] - Buffer is not large enough to fit
+///   tokenized data.
+/// - [`pw_status::Error::InvalidArgument`] - Invalid buffer was provided.
+///
+/// # Example
+///
+/// ```
+/// use pw_tokenizer::tokenize_printf_to_buffer;
+///
+/// // Tokenize a format string and argument into a buffer.
+/// let mut buffer = [0u8; 1024];
+/// let len = tokenize_printf_to_buffer!(&mut buffer, "The answer is %d", 42)?;
+///
+/// // 4 bytes used to encode the token and one to encode the value 42.
+/// assert_eq!(len, 5);
+///
+/// // The format string can be composed of multiple strings literals using
+/// // the custom`PW_FMT_CONCAT` operator.
+/// let len = tokenize_printf_to_buffer!(&mut buffer, "Hello " PW_FMT_CONCAT "Pigweed")?;
+///
+/// // Only a single 4 byte token is emitted after concatenation of the string
+/// // literals above.
+/// assert_eq!(len, 4);
+/// # Ok::<(), pw_status::Error>(())
+/// ```
+#[macro_export]
+macro_rules! tokenize_printf_to_buffer {
+    ($buffer:expr, $($format_string:literal)PW_FMT_CONCAT+ $(, $args:expr)* $(,)?) => {{
+      use $crate::__private as __pw_tokenizer_crate;
+      __pw_tokenizer_crate::_tokenize_printf_to_buffer!($buffer, $($format_string)PW_FMT_CONCAT+, $($args),*)
+    }};
+}
+
+/// Deprecated alias for [`tokenize_printf_to_buffer!`].
+#[macro_export]
+macro_rules! tokenize_to_buffer {
+    ($buffer:expr, $($format_string:literal)PW_FMT_CONCAT+ $(, $args:expr)* $(,)?) => {{
+      $crate::tokenize_printf_to_buffer!($buffer, $($format_string)PW_FMT_CONCAT+, $($args),*)
     }};
 }
 
@@ -155,7 +226,7 @@
 /// UART, or a shared buffer.
 ///
 /// See [`token`] for an explanation on how strings are tokenized and entries
-/// are added to the token database.
+/// are added to the token database.  The token's domain is set to "".
 ///
 /// Returns a [`pw_status::Result<()>`].
 ///
@@ -222,13 +293,30 @@
 /// # Ok::<(), pw_status::Error>(())
 /// ```
 #[macro_export]
-macro_rules! tokenize_to_writer {
+macro_rules! tokenize_core_fmt_to_writer {
     ($ty:ty, $($format_string:literal)PW_FMT_CONCAT+ $(, $args:expr)* $(,)?) => {{
       use $crate::__private as __pw_tokenizer_crate;
-      __pw_tokenizer_crate::_tokenize_to_writer!($ty, $($format_string)PW_FMT_CONCAT+, $($args),*)
+      __pw_tokenizer_crate::_tokenize_core_fmt_to_writer!($ty, $($format_string)PW_FMT_CONCAT+, $($args),*)
     }};
 }
 
+/// DOCME
+#[macro_export]
+macro_rules! tokenize_printf_to_writer {
+    ($ty:ty, $($format_string:literal)PW_FMT_CONCAT+ $(, $args:expr)* $(,)?) => {{
+      use $crate::__private as __pw_tokenizer_crate;
+      __pw_tokenizer_crate::_tokenize_printf_to_writer!($ty, $($format_string)PW_FMT_CONCAT+, $($args),*)
+    }};
+}
+
+/// Deprecated alias for [`tokenize_printf_to_writer!`].
+#[macro_export]
+macro_rules! tokenize_to_writer {
+  ($ty:ty, $($format_string:literal)PW_FMT_CONCAT+ $(, $args:expr)* $(,)?) => {{
+    $crate::tokenize_printf_to_writer!($ty, $($format_string)PW_FMT_CONCAT+, $($args),*)
+  }};
+}
+
 /// A trait used by [`tokenize_to_writer!`] to output tokenized messages.
 ///
 /// For more details on how this type is used, see the [`tokenize_to_writer!`]
@@ -265,18 +353,30 @@
     fn test_token() {}
 
     macro_rules! tokenize_to_buffer_test {
-      ($expected_data:expr, $buffer_len:expr, $fmt:expr $(, $args:expr)* $(,)?) => {{
-        let mut buffer = [0u8; $buffer_len];
-        let len = tokenize_to_buffer!(&mut buffer, $fmt, $($args),*).unwrap();
-        assert_eq!(
-            &buffer[..len],
-            $expected_data,
-        );
+      ($expected_data:expr, $buffer_len:expr, $printf_fmt:literal, $core_fmt:literal $(, $args:expr)* $(,)?) => {{
+        if $printf_fmt != "" {
+          let mut buffer = [0u8; $buffer_len];
+          let len = tokenize_printf_to_buffer!(&mut buffer, $printf_fmt, $($args),*).unwrap();
+          assert_eq!(
+              &buffer[..len],
+              $expected_data,
+              "printf style input does not produce expected output",
+          );
+        }
+        if $core_fmt != "" {
+           let mut buffer = [0u8; $buffer_len];
+           let len = tokenize_core_fmt_to_buffer!(&mut buffer, $core_fmt, $($args),*).unwrap();
+           assert_eq!(
+               &buffer[..len],
+               $expected_data,
+              "core::fmt style input does not produce expected output",
+           );
+        }
       }}
     }
 
     macro_rules! tokenize_to_writer_test {
-      ($expected_data:expr, $buffer_len:expr, $fmt:expr $(, $args:expr)* $(,)?) => {{
+      ($expected_data:expr, $buffer_len:expr, $printf_fmt:literal, $core_fmt:literal $(, $args:expr)* $(,)?) => {{
         // The `MessageWriter` API is used in places like logging where it
         // accesses an shared/ambient resource (like stdio or an UART).  To test
         // it in a hermetic way we declare test specific `MessageWriter` that
@@ -315,20 +415,34 @@
           }
         }
 
-        tokenize_to_writer!(TestMessageWriter, $fmt, $($args),*).unwrap();
-        TEST_OUTPUT.with(|output| {
-            assert_eq!(
-                *output.borrow(),
-                Some($expected_data.to_vec()),
-            )
-        });
+        if $printf_fmt != "" {
+          TEST_OUTPUT.with(|output| *output.borrow_mut() = None);
+          tokenize_printf_to_writer!(TestMessageWriter, $printf_fmt, $($args),*).unwrap();
+          TEST_OUTPUT.with(|output| {
+              assert_eq!(
+                  *output.borrow(),
+                  Some($expected_data.to_vec()),
+              )
+          });
+        }
+
+        if $core_fmt != "" {
+          TEST_OUTPUT.with(|output| *output.borrow_mut() = None);
+          tokenize_core_fmt_to_writer!(TestMessageWriter, $core_fmt, $($args),*).unwrap();
+          TEST_OUTPUT.with(|output| {
+              assert_eq!(
+                  *output.borrow(),
+                  Some($expected_data.to_vec()),
+              )
+          });
+        }
       }}
     }
 
     macro_rules! tokenize_test {
-        ($expected_data:expr, $buffer_len:expr, $fmt:expr $(, $args:expr)* $(,)?) => {{
-            tokenize_to_buffer_test!($expected_data, $buffer_len, $fmt, $($args),*);
-            tokenize_to_writer_test!($expected_data, $buffer_len, $fmt, $($args),*);
+        ($expected_data:expr, $buffer_len:expr, $printf_fmt:literal, $core_fmt:literal $(, $args:expr)* $(,)?) => {{
+            tokenize_to_buffer_test!($expected_data, $buffer_len, $printf_fmt, $core_fmt, $($args),*);
+            tokenize_to_writer_test!($expected_data, $buffer_len, $printf_fmt, $core_fmt, $($args),*);
         }};
     }
 
@@ -337,30 +451,38 @@
         tokenize_test!(
             &[0xe0, 0x92, 0xe0, 0xa], // expected buffer
             64,                       // buffer size
-            "Hello Pigweed",
+            "Hello Pigweed",          // printf style
+            "Hello Pigweed",          // core::fmt style
         );
     }
+
     #[test]
     fn test_decimal_format() {
+        // "as casts" are used for the integer arguments below.  They are only
+        // need for the core::fmt style arguments but are added so that we can
+        // check that the printf and core::fmt style equivalents encode the same.
         tokenize_test!(
             &[0x52, 0x1c, 0xb0, 0x4c, 0x2], // expected buffer
             64,                             // buffer size
-            "The answer is %d!",
-            1
+            "The answer is %d!",            // printf style
+            "The answer is {}!",            // core::fmt style
+            1 as i32
         );
 
         tokenize_test!(
             &[0x36, 0xd0, 0xfb, 0x69, 0x1], // expected buffer
             64,                             // buffer size
-            "No! The answer is %d!",
-            -1
+            "No! The answer is %d!",        // printf style
+            "No! The answer is {}!",        // core::fmt style
+            -1 as i32
         );
 
         tokenize_test!(
-            &[0xa4, 0xad, 0x50, 0x54, 0x0], // expected buffer
-            64,                             // buffer size
-            "I think you'll find that the answer is %d!",
-            0
+            &[0xa4, 0xad, 0x50, 0x54, 0x0],               // expected buffer
+            64,                                           // buffer size
+            "I think you'll find that the answer is %d!", // printf style
+            "I think you'll find that the answer is {}!", // core::fmt style
+            0 as i32
         );
     }
 
@@ -370,7 +492,8 @@
         tokenize_test!(
             &[0x52, 0x1c, 0xb0, 0x4c, 0x2], // expected buffer
             64,                             // buffer size
-            "The answer is %d!",
+            "The answer is %d!",            // printf style
+            "",                             // no equivalent core::fmt style
             1
         );
 
@@ -379,35 +502,40 @@
         tokenize_test!(
             &[0x52, 0x1c, 0xb0, 0x4c, 0x2], // expected buffer
             64,                             // buffer size
-            "The answer is %i!",
+            "The answer is %i!",            // printf style
+            "",                             // no equivalent core::fmt style
             1
         );
 
         tokenize_test!(
             &[0x5d, 0x70, 0x12, 0xb4, 0x2], // expected buffer
             64,                             // buffer size
-            "The answer is %o!",
+            "The answer is %o!",            // printf style
+            "",                             // no equivalent core::fmt style
             1u32
         );
 
         tokenize_test!(
             &[0x63, 0x58, 0x5f, 0x8f, 0x2], // expected buffer
             64,                             // buffer size
-            "The answer is %u!",
+            "The answer is %u!",            // printf style
+            "",                             // no equivalent core::fmt style
             1u32
         );
 
         tokenize_test!(
             &[0x66, 0xcc, 0x05, 0x7d, 0x2], // expected buffer
             64,                             // buffer size
-            "The answer is %x!",
+            "The answer is %x!",            // printf style
+            "",                             // no equivalent core::fmt style
             1u32
         );
 
         tokenize_test!(
             &[0x46, 0x4c, 0x16, 0x96, 0x2], // expected buffer
             64,                             // buffer size
-            "The answer is %X!",
+            "The answer is %X!",            // printf style
+            "",                             // no equivalent core::fmt style
             1u32
         );
     }
@@ -417,7 +545,8 @@
         tokenize_test!(
             b"\x25\xf6\x2e\x66\x07Pigweed", // expected buffer
             64,                             // buffer size
-            "Hello: %s!",
+            "Hello: %s!",                   // printf style
+            "",                             // no equivalent core::fmt style
             "Pigweed"
         );
     }
@@ -427,7 +556,8 @@
         tokenize_test!(
             b"\x25\xf6\x2e\x66\x83Pig", // expected buffer
             8,                          // buffer size
-            "Hello: %s!",
+            "Hello: %s!",               // printf style
+            "",                         // no equivalent core::fmt style
             "Pigweed"
         );
     }
@@ -437,22 +567,66 @@
         tokenize_test!(
             &[0x2e, 0x52, 0xac, 0xe4, 0x50], // expected buffer
             64,                              // buffer size
-            "Hello: %cigweed",
+            "Hello: %cigweed",               // printf style
+            "",                              // no equivalent core::fmt style
             "P".as_bytes()[0]
         );
     }
 
     #[test]
-    fn tokenizer_supports_concatenated_format_strings() {
+    fn test_untyped_format() {
+        tokenize_test!(
+            &[0x63, 0x58, 0x5f, 0x8f, 0x2], // expected buffer
+            64,                             // buffer size
+            "The answer is %u!",            // printf style
+            "The answer is {}!",            // core::fmt style
+            1 as u32
+        );
+
+        tokenize_test!(
+            &[0x36, 0xd0, 0xfb, 0x69, 0x1], // expected buffer
+            64,                             // buffer size
+            "No! The answer is %v!",        // printf style
+            "No! The answer is {}!",        // core::fmt style
+            -1 as i32
+        );
+
+        tokenize_test!(
+            b"\x25\xf6\x2e\x66\x07Pigweed", // expected buffer
+            64,                             // buffer size
+            "Hello: %v!",                   // printf style
+            "Hello: {}!",                   // core::fmt style
+            "Pigweed" as &str
+        );
+    }
+
+    #[test]
+    fn tokenizer_supports_concatenated_printf_format_strings() {
         // Since the no argument and some arguments cases are handled differently
         // by `tokenize_to_buffer!` we need to test both.
         let mut buffer = [0u8; 64];
-        let len = tokenize_to_buffer!(&mut buffer, "Hello" PW_FMT_CONCAT " Pigweed").unwrap();
+        let len =
+            tokenize_printf_to_buffer!(&mut buffer, "Hello" PW_FMT_CONCAT " Pigweed").unwrap();
         assert_eq!(&buffer[..len], &[0xe0, 0x92, 0xe0, 0xa]);
 
-        let len = tokenize_to_buffer!(&mut buffer, "Hello: " PW_FMT_CONCAT "%cigweed",
+        let len = tokenize_printf_to_buffer!(&mut buffer, "Hello: " PW_FMT_CONCAT "%cigweed",
           "P".as_bytes()[0])
         .unwrap();
         assert_eq!(&buffer[..len], &[0x2e, 0x52, 0xac, 0xe4, 0x50]);
     }
+
+    #[test]
+    fn tokenizer_supports_concatenated_core_fmt_format_strings() {
+        // Since the no argument and some arguments cases are handled differently
+        // by `tokenize_to_buffer!` we need to test both.
+        let mut buffer = [0u8; 64];
+        let len =
+            tokenize_core_fmt_to_buffer!(&mut buffer, "Hello" PW_FMT_CONCAT " Pigweed").unwrap();
+        assert_eq!(&buffer[..len], &[0xe0, 0x92, 0xe0, 0xa]);
+
+        let len = tokenize_core_fmt_to_buffer!(&mut buffer, "The answer is " PW_FMT_CONCAT "{}!",
+          1 as i32)
+        .unwrap();
+        assert_eq!(&buffer[..len], &[0x52, 0x1c, 0xb0, 0x4c, 0x2]);
+    }
 }
diff --git a/pw_tokenizer/rust/pw_tokenizer_macro.rs b/pw_tokenizer/rust/pw_tokenizer_macro.rs
index 9503dda..4b4db40 100644
--- a/pw_tokenizer/rust/pw_tokenizer_macro.rs
+++ b/pw_tokenizer/rust/pw_tokenizer_macro.rs
@@ -26,8 +26,8 @@
 };
 
 use pw_format::macros::{
-    generate_printf, Arg, FormatAndArgsFlavor, PrintfFormatMacroGenerator,
-    PrintfFormatStringFragment, PrintfFormatStringParser, Result,
+    generate_printf, Arg, CoreFmtFormatStringParser, FormatAndArgsFlavor, FormatStringParser,
+    PrintfFormatMacroGenerator, PrintfFormatStringFragment, PrintfFormatStringParser, Result,
 };
 use pw_tokenizer_core::TOKENIZER_ENTRY_MAGIC;
 
@@ -102,12 +102,12 @@
 // Args to tokenize to buffer that are parsed according to the pattern:
 //   ($buffer:expr, $format_string:literal, $($args:expr),*)
 #[derive(Debug)]
-struct TokenizeToBufferArgs {
+struct TokenizeToBufferArgs<T: FormatStringParser + core::fmt::Debug> {
     buffer: Expr,
-    format_and_args: FormatAndArgsFlavor<PrintfFormatStringParser>,
+    format_and_args: FormatAndArgsFlavor<T>,
 }
 
-impl Parse for TokenizeToBufferArgs {
+impl<T: FormatStringParser + core::fmt::Debug> Parse for TokenizeToBufferArgs<T> {
     fn parse(input: ParseStream) -> syn::parse::Result<Self> {
         let buffer: Expr = input.parse()?;
         input.parse::<Token![,]>()?;
@@ -205,20 +205,40 @@
 
     fn untyped_conversion(&mut self, expression: Arg) -> Result<()> {
         self.encoding_fragments.push(quote! {
-          __pw_tokenizer_crate::internal::Encoder::encode(#expression, &mut cursor)?;
+          Argument::from(#expression)
         });
         Ok(())
     }
 }
 
-// Generates code to marshal a tokenized string and arguments into a buffer.
-// See [`pw_tokenizer::tokenize_to_buffer`] for details on behavior.
-//
-// Internally the [`AsMut<u8>`] is wrapped in a [`pw_stream::Cursor`] to
-// fill the buffer incrementally.
+/// Generates code to marshal a tokenized core::fmt format string and arguments
+/// into a buffer.  See [`pw_tokenizer::tokenize_core_fmt_to_buffer`] for details
+/// on behavior.
+///
+/// Internally the [`AsMut<u8>`] is wrapped in a [`pw_stream::Cursor`] to
+/// fill the buffer incrementally.
 #[proc_macro]
-pub fn _tokenize_to_buffer(tokens: TokenStream) -> TokenStream {
-    let input = parse_macro_input!(tokens as TokenizeToBufferArgs);
+pub fn _tokenize_core_fmt_to_buffer(tokens: TokenStream) -> TokenStream {
+    let input = parse_macro_input!(tokens as TokenizeToBufferArgs<CoreFmtFormatStringParser>);
+
+    // Hard codes domain to "".
+    let generator = TokenizeToBufferGenerator::new("", &input.buffer);
+
+    match generate_printf(generator, input.format_and_args.into()) {
+        Ok(token_stream) => token_stream.into(),
+        Err(e) => e.to_compile_error().into(),
+    }
+}
+
+/// Generates code to marshal a tokenized printf format string and arguments
+/// into a buffer.  See [`pw_tokenizer::tokenize_printf_to_buffer`] for details
+/// on behavior.
+///
+/// Internally the [`AsMut<u8>`] is wrapped in a [`pw_stream::Cursor`] to
+/// fill the buffer incrementally.
+#[proc_macro]
+pub fn _tokenize_printf_to_buffer(tokens: TokenStream) -> TokenStream {
+    let input = parse_macro_input!(tokens as TokenizeToBufferArgs<PrintfFormatStringParser>);
 
     // Hard codes domain to "".
     let generator = TokenizeToBufferGenerator::new("", &input.buffer);
@@ -232,12 +252,12 @@
 // Args to tokenize to buffer that are parsed according to the pattern:
 //   ($ty:ty, $format_string:literal, $($args:expr),*)
 #[derive(Debug)]
-struct TokenizeToWriterArgs {
+struct TokenizeToWriterArgs<T: FormatStringParser> {
     ty: Type,
-    format_and_args: FormatAndArgsFlavor<PrintfFormatStringParser>,
+    format_and_args: FormatAndArgsFlavor<T>,
 }
 
-impl Parse for TokenizeToWriterArgs {
+impl<T: FormatStringParser> Parse for TokenizeToWriterArgs<T> {
     fn parse(input: ParseStream) -> syn::parse::Result<Self> {
         let ty: Type = input.parse()?;
         input.parse::<Token![,]>()?;
@@ -340,9 +360,28 @@
     }
 }
 
+/// Generates code to marshal a tokenized core::fmt format string and arguments
+/// into a [`pw_stream::Write`].  See [`pw_tokenizer::tokenize_core_fmt_to_writer`]
+/// for details on behavior.
 #[proc_macro]
-pub fn _tokenize_to_writer(tokens: TokenStream) -> TokenStream {
-    let input = parse_macro_input!(tokens as TokenizeToWriterArgs);
+pub fn _tokenize_core_fmt_to_writer(tokens: TokenStream) -> TokenStream {
+    let input = parse_macro_input!(tokens as TokenizeToWriterArgs<CoreFmtFormatStringParser>);
+
+    // Hard codes domain to "".
+    let generator = TokenizeToWriterGenerator::new("", &input.ty);
+
+    match generate_printf(generator, input.format_and_args.into()) {
+        Ok(token_stream) => token_stream.into(),
+        Err(e) => e.to_compile_error().into(),
+    }
+}
+
+/// Generates code to marshal a tokenized printf format string and arguments
+/// into a [`pw_stream::Write`].  See [`pw_tokenizer::tokenize_printf_to_writer`]
+/// for details on behavior.
+#[proc_macro]
+pub fn _tokenize_printf_to_writer(tokens: TokenStream) -> TokenStream {
+    let input = parse_macro_input!(tokens as TokenizeToWriterArgs<PrintfFormatStringParser>);
 
     // Hard codes domain to "".
     let generator = TokenizeToWriterGenerator::new("", &input.ty);
diff --git a/pw_toolchain/rust/defs.bzl b/pw_toolchain/rust/defs.bzl
index dff8881..11fdb4e 100644
--- a/pw_toolchain/rust/defs.bzl
+++ b/pw_toolchain/rust/defs.bzl
@@ -65,7 +65,7 @@
 CHANNELS = [
     {
         "name": "nightly",
-        "extra_rustc_flags": ["-Dwarnings"],
+        "extra_rustc_flags": ["-Dwarnings", "-Zmacro-backtrace"],
         "target_settings": ["@rules_rust//rust/toolchain/channel:nightly"],
     },
     {