Runtime-fmt, a runtime formatting crate
— Rust
runtime-fmt
is a small, nightly-only, safe Rust crate which provides
variants of the six standard formatting macros which validate their format
strings at runtime. It has feature parity with std::fmt
and accepts exactly
the same syntax for format strings and arguments, with the additional feature
of supporting format strings which are not known at compile time.
#[macro_use] extern crate runtime_fmt;
fn main() {
let format_string = "Hello, {}!";
rt_println!(format_string, "world").unwrap();
rt_println!("bogus {} reference").unwrap_err();
rt_println!("bogus }{ format string").unwrap_err();
}
Applications include internationalization, configuration, and modding. For example, designers might be allowed to adjust text with interspersed values in a video game, or command-line users could specify what shape the output should be.
The core implementation is under 300 SLOC, plus 550 or
so to embed libfmt_macros
.
Implementation
runtime-fmt
is safe. It achieves direct feature-parity with the standard
formatting macros by using the same code that rustc
uses to parse format
strings. Somewhat awkwardly, this code is included inline in the crate rather
than linked in, because it is distributed only dynamically, and most Rust users
don’t want to link libstd
dynamically.
On top of format string parsing, rustc
includes elaborate machinery for
converting the parsed format string into an AST for macro expansion. Because
runtime-fmt
does not need to work on the AST level, and can make assumptions
about the values returned by libfmt_macros
, this conversion is vastly
simplified.
All format specifiers are supported. Recognizing at runtime whether a given
argument supports a given format string is achieved through specialization,
implementing internal shadow traits of the standard formatting traits which
return None
when unavailable, and surfacing this as an error to users.
All errors are encapsulated in an enum Error
, returned within a Result
from
all the macros. Syntax errors are returned as-is, including descriptions
similar to those generated by rustc, and other types of error variants are
available, including std::io::Error
and std::fmt::Error
variants which may
be returned when using rt_write!
or rt_writeln!
.
The crate requires nightly for obvious reasons; access to the formatting machinery isn’t stable, and might never be. That said, it changes rarely, and the crate is unlikely to break often. The use of specialization described earlier also requires nightly.
Test Suite
This crate passes the main body of tests from rustc’s test suite, as
well as a small smattering of tests written specifically for runtime-fmt
. I
intend to continue to expand the test suite, both to include more tests from
rustc and to more rigorously ensure that format string and argument
combinations which ought to error do.
Future Expansion
Some performance is left on the table in the initial implementation.
Particularly, format strings are re-parsed each time the string is to be
formatted. I have an idea on how to fix this, revolving around specifying a
struct or tuple type in rt_format_args!
once during parsing, where the format
string is parsed and types are resolved and checked, and then supplying values
of that struct type which can be formatted quickly by the prebuilt formatter.
Most of std::fmt
is also available in core
. The library currently uses heap
allocation in many instances, but it may be possible to adapt it to use static
buffers or other trickery in those situations and thereby allow the library
to be used under #[no_std]
, just like the builtin formatting macros.
Addendum: thanks illegalprime
on IRC for pointing out that the snippet at
the top of the blog post didn’t demonstrate error handling.