rust: support formatting of foreign types

Introduce a `fmt!` macro which wraps all arguments in
`kernel::fmt::Adapter` and a `kernel::fmt::Display` trait. This enables
formatting of foreign types (like `core::ffi::CStr`) that do not
implement `core::fmt::Display` due to concerns around lossy conversions
which do not apply in the kernel.

Suggested-by: Alice Ryhl <aliceryhl@google.com>
Link: https://rust-for-linux.zulipchat.com/#narrow/channel/288089-General/topic/Custom.20formatting/with/516476467
Acked-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
Reviewed-by: Alice Ryhl <aliceryhl@google.com>
Acked-by: Danilo Krummrich <dakr@kernel.org>
Reviewed-by: Benno Lossin <lossin@kernel.org>
Signed-off-by: Tamir Duberstein <tamird@gmail.com>
Link: https://patch.msgid.link/20251018-cstr-core-v18-15-9378a54385f8@gmail.com
Signed-off-by: Miguel Ojeda <ojeda@kernel.org>
This commit is contained in:
Tamir Duberstein 2025-10-18 15:16:36 -04:00 committed by Miguel Ojeda
parent 1dcd763ba1
commit c5cf01ba8d
5 changed files with 207 additions and 3 deletions

View File

@ -4,4 +4,89 @@
//! //!
//! This module is intended to be used in place of `core::fmt` in kernel code. //! This module is intended to be used in place of `core::fmt` in kernel code.
pub use core::fmt::{Arguments, Debug, Display, Error, Formatter, Result, Write}; pub use core::fmt::{Arguments, Debug, Error, Formatter, Result, Write};
/// Internal adapter used to route allow implementations of formatting traits for foreign types.
///
/// It is inserted automatically by the [`fmt!`] macro and is not meant to be used directly.
///
/// [`fmt!`]: crate::prelude::fmt!
#[doc(hidden)]
pub struct Adapter<T>(pub T);
macro_rules! impl_fmt_adapter_forward {
($($trait:ident),* $(,)?) => {
$(
impl<T: $trait> $trait for Adapter<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
let Self(t) = self;
$trait::fmt(t, f)
}
}
)*
};
}
use core::fmt::{Binary, LowerExp, LowerHex, Octal, Pointer, UpperExp, UpperHex};
impl_fmt_adapter_forward!(Debug, LowerHex, UpperHex, Octal, Binary, Pointer, LowerExp, UpperExp);
/// A copy of [`core::fmt::Display`] that allows us to implement it for foreign types.
///
/// Types should implement this trait rather than [`core::fmt::Display`]. Together with the
/// [`Adapter`] type and [`fmt!`] macro, it allows for formatting foreign types (e.g. types from
/// core) which do not implement [`core::fmt::Display`] directly.
///
/// [`fmt!`]: crate::prelude::fmt!
pub trait Display {
/// Same as [`core::fmt::Display::fmt`].
fn fmt(&self, f: &mut Formatter<'_>) -> Result;
}
impl<T: ?Sized + Display> Display for &T {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
Display::fmt(*self, f)
}
}
impl<T: ?Sized + Display> core::fmt::Display for Adapter<&T> {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
let Self(t) = self;
Display::fmt(t, f)
}
}
macro_rules! impl_display_forward {
($(
$( { $($generics:tt)* } )? $ty:ty $( { where $($where:tt)* } )?
),* $(,)?) => {
$(
impl$($($generics)*)? Display for $ty $(where $($where)*)? {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
core::fmt::Display::fmt(self, f)
}
}
)*
};
}
impl_display_forward!(
bool,
char,
core::panic::PanicInfo<'_>,
Arguments<'_>,
i128,
i16,
i32,
i64,
i8,
isize,
str,
u128,
u16,
u32,
u64,
u8,
usize,
{<T: ?Sized>} crate::sync::Arc<T> {where crate::sync::Arc<T>: core::fmt::Display},
{<T: ?Sized>} crate::sync::UniqueArc<T> {where crate::sync::UniqueArc<T>: core::fmt::Display},
);

View File

@ -25,7 +25,7 @@
pub use crate::alloc::{flags::*, Box, KBox, KVBox, KVVec, KVec, VBox, VVec, Vec}; pub use crate::alloc::{flags::*, Box, KBox, KVBox, KVVec, KVec, VBox, VVec, Vec};
#[doc(no_inline)] #[doc(no_inline)]
pub use macros::{export, kunit_tests, module, vtable}; pub use macros::{export, fmt, kunit_tests, module, vtable};
pub use pin_init::{init, pin_data, pin_init, pinned_drop, InPlaceWrite, Init, PinInit, Zeroable}; pub use pin_init::{init, pin_data, pin_init, pinned_drop, InPlaceWrite, Init, PinInit, Zeroable};
@ -36,7 +36,6 @@
pub use super::dbg; pub use super::dbg;
pub use super::{dev_alert, dev_crit, dev_dbg, dev_emerg, dev_err, dev_info, dev_notice, dev_warn}; pub use super::{dev_alert, dev_crit, dev_dbg, dev_emerg, dev_err, dev_info, dev_notice, dev_warn};
pub use super::{pr_alert, pr_crit, pr_debug, pr_emerg, pr_err, pr_info, pr_notice, pr_warn}; pub use super::{pr_alert, pr_crit, pr_debug, pr_emerg, pr_err, pr_info, pr_notice, pr_warn};
pub use core::format_args as fmt;
pub use super::{try_init, try_pin_init}; pub use super::{try_init, try_pin_init};

94
rust/macros/fmt.rs Normal file
View File

@ -0,0 +1,94 @@
// SPDX-License-Identifier: GPL-2.0
use proc_macro::{Ident, TokenStream, TokenTree};
use std::collections::BTreeSet;
/// Please see [`crate::fmt`] for documentation.
pub(crate) fn fmt(input: TokenStream) -> TokenStream {
let mut input = input.into_iter();
let first_opt = input.next();
let first_owned_str;
let mut names = BTreeSet::new();
let first_span = {
let Some((mut first_str, first_span)) = (match first_opt.as_ref() {
Some(TokenTree::Literal(first_lit)) => {
first_owned_str = first_lit.to_string();
Some(first_owned_str.as_str()).and_then(|first| {
let first = first.strip_prefix('"')?;
let first = first.strip_suffix('"')?;
Some((first, first_lit.span()))
})
}
_ => None,
}) else {
return first_opt.into_iter().chain(input).collect();
};
// Parse `identifier`s from the format string.
//
// See https://doc.rust-lang.org/std/fmt/index.html#syntax.
while let Some((_, rest)) = first_str.split_once('{') {
first_str = rest;
if let Some(rest) = first_str.strip_prefix('{') {
first_str = rest;
continue;
}
if let Some((name, rest)) = first_str.split_once('}') {
first_str = rest;
let name = name.split_once(':').map_or(name, |(name, _)| name);
if !name.is_empty() && !name.chars().all(|c| c.is_ascii_digit()) {
names.insert(name);
}
}
}
first_span
};
let adapter = quote_spanned!(first_span => ::kernel::fmt::Adapter);
let mut args = TokenStream::from_iter(first_opt);
{
let mut flush = |args: &mut TokenStream, current: &mut TokenStream| {
let current = std::mem::take(current);
if !current.is_empty() {
let (lhs, rhs) = (|| {
let mut current = current.into_iter();
let mut acc = TokenStream::new();
while let Some(tt) = current.next() {
// Split on `=` only once to handle cases like `a = b = c`.
if matches!(&tt, TokenTree::Punct(p) if p.as_char() == '=') {
names.remove(acc.to_string().as_str());
// Include the `=` itself to keep the handling below uniform.
acc.extend([tt]);
return (Some(acc), current.collect::<TokenStream>());
}
acc.extend([tt]);
}
(None, acc)
})();
args.extend(quote_spanned!(first_span => #lhs #adapter(&#rhs)));
}
};
let mut current = TokenStream::new();
for tt in input {
match &tt {
TokenTree::Punct(p) if p.as_char() == ',' => {
flush(&mut args, &mut current);
&mut args
}
_ => &mut current,
}
.extend([tt]);
}
flush(&mut args, &mut current);
}
for name in names {
let name = Ident::new(name, first_span);
args.extend(quote_spanned!(first_span => , #name = #adapter(&#name)));
}
quote_spanned!(first_span => ::core::format_args!(#args))
}

View File

@ -15,6 +15,7 @@
mod quote; mod quote;
mod concat_idents; mod concat_idents;
mod export; mod export;
mod fmt;
mod helpers; mod helpers;
mod kunit; mod kunit;
mod module; mod module;
@ -201,6 +202,24 @@ pub fn export(attr: TokenStream, ts: TokenStream) -> TokenStream {
export::export(attr, ts) export::export(attr, ts)
} }
/// Like [`core::format_args!`], but automatically wraps arguments in [`kernel::fmt::Adapter`].
///
/// This macro allows generating `fmt::Arguments` while ensuring that each argument is wrapped with
/// `::kernel::fmt::Adapter`, which customizes formatting behavior for kernel logging.
///
/// Named arguments used in the format string (e.g. `{foo}`) are detected and resolved from local
/// bindings. All positional and named arguments are automatically wrapped.
///
/// This macro is an implementation detail of other kernel logging macros like [`pr_info!`] and
/// should not typically be used directly.
///
/// [`kernel::fmt::Adapter`]: ../kernel/fmt/struct.Adapter.html
/// [`pr_info!`]: ../kernel/macro.pr_info.html
#[proc_macro]
pub fn fmt(input: TokenStream) -> TokenStream {
fmt::fmt(input)
}
/// Concatenate two identifiers. /// Concatenate two identifiers.
/// ///
/// This is useful in macros that need to declare or reference items with names /// This is useful in macros that need to declare or reference items with names

View File

@ -48,6 +48,7 @@ macro_rules! quote_spanned {
($span:expr => $($tt:tt)*) => {{ ($span:expr => $($tt:tt)*) => {{
let mut tokens = ::proc_macro::TokenStream::new(); let mut tokens = ::proc_macro::TokenStream::new();
{ {
#[allow(unused_variables)]
let span = $span; let span = $span;
quote_spanned!(@proc tokens span $($tt)*); quote_spanned!(@proc tokens span $($tt)*);
} }
@ -146,6 +147,12 @@ macro_rules! quote_spanned {
)]); )]);
quote_spanned!(@proc $v $span $($tt)*); quote_spanned!(@proc $v $span $($tt)*);
}; };
(@proc $v:ident $span:ident & $($tt:tt)*) => {
$v.extend([::proc_macro::TokenTree::Punct(
::proc_macro::Punct::new('&', ::proc_macro::Spacing::Alone),
)]);
quote_spanned!(@proc $v $span $($tt)*);
};
(@proc $v:ident $span:ident _ $($tt:tt)*) => { (@proc $v:ident $span:ident _ $($tt:tt)*) => {
$v.extend([::proc_macro::TokenTree::Ident( $v.extend([::proc_macro::TokenTree::Ident(
::proc_macro::Ident::new("_", $span), ::proc_macro::Ident::new("_", $span),