4 releases (stable)
Uses new Rust 2024
new 1.1.1 | Mar 21, 2025 |
---|---|
1.1.0 | Mar 19, 2025 |
1.0.0 | Mar 19, 2025 |
0.1.0 | Mar 7, 2025 |
#410 in Rust patterns
670 downloads per month
Used in smithy-cargo-macros
42KB
208 lines
Click here to read the docs!
lib.rs
:
🦀 Crabtime
Crabtime offers a novel way to write Rust macros, inspired by
Zig's comptime. It provides even more flexibility and power than procedural
macros, while remaining easier and more natural to read and write than
macro_rules!
.
🆚 Comparison to Proc Macros and macro_rules!
Below is a comparison of key aspects of Rust's macro systems:
Input/Output
Crabtime | Proc Macro | macro_rules! |
|
---|---|---|---|
Input as Token Stream | ✅ | ✅ | ❌ |
Input as Macro Fragments | ✅ | ❌ | ✅ |
Input as Rust Code (String) | ✅ | ❌ | ❌ |
Output as Token Stream | ✅ | ✅ | ❌ |
Output as Macro Fragments Template | ✅ | ❌ | ✅ |
Output as Rust Code (String) | ✅ | ❌ | ❌ |
Functionalities
Crabtime | Proc Macro | macro_rules! |
|
---|---|---|---|
Advanced transformations | ✅ | ✅ | ❌ |
Space-aware interpolation | ✅ | ❌ | ❌ |
Can define fn-like macros | ✅ | ✅ | ✅ |
Can define derive macros | 🚧 | ✅ | ❌ |
Can define attribute macros | 🚧 | ✅ | ❌ |
Reusable across modules and crates | ✅ | ✅ | ✅ |
Comfort of life
Crabtime | Proc Macro | macro_rules! |
|
---|---|---|---|
Full expansion in IDEs[^supported_ides] | ✅ | ✅ | ✅ |
Full type hints in IDEs[^supported_ides] | ✅ | ✅ | ❌ |
Works with rustfmt | ✅ | ✅ | ❌ |
Easy to define (inline, the same crate) | ✅ | ❌ | ✅ |
Easy to read | ✅ | ❌ | ⚠️ |
Hygienic | ❌ | ❌ | ✅ |
🎯 One-shot evaluation
The simplest and least exciting use of Crabtime is straightforward compile-time code
evaluation. To evaluate an expression and paste its output as new code, just use
crabtime::eval
, as shown below:
const MY_NUM: usize = crabtime::eval! {
(std::f32::consts::PI.sqrt() * 10.0).round() as usize
};
🤩 Function-like macros
Use the crabtime::function
attribute to define a new function-like macro.
Crabtime will remove the annotated function and replace it with a macro definition of the same
name. You can then call the macro to compile and execute the function at build time, and use
its output as the generated Rust code. You can also use the standard #[macro_export]
attribute to export your macro. Let's start with a simple example, and let's refine it down
the line. Let's generate the following Rust code:
enum Position1 { X }
enum Position2 { X, Y }
enum Position3 { X, Y, Z }
enum Position4 { X, Y, Z, W }
We can do it in this, not very exciting way:
// Replaces the function definition with a `gen_positions1!` macro.
#[crabtime::function]
#[macro_export] // <- This is how you export it!
fn gen_positions1() -> &str {
"
enum Position1 { X }
enum Position2 { X, Y }
enum Position3 { X, Y, Z }
enum Position4 { X, Y, Z, W }
"
}
// Compiles and evaluates the gen_positions1 function at build-time and
// uses its output as the new code source.
gen_positions1!();
🤩 Attribute and derive macros
Currently, generating attribute macros and derive macros is not supported, but there are several ways to achieve it. If you want to help, ping us on GitHub.
📤 Output
There are several ways to generate the output code from a Crabtime macro. We recommend you to
use either crabtime::output!
or crabtime::quote!
macros, as they allow for the most
concise, easy-to-understand, and maintainable implementations. Supported input types are
described later, for now just ignore them.
Generating output by using crabtime::output!
The simplest and most recommended way to generate macro output is by using the
crabtime::output!
macro. It allows for space-aware variable interpolation. It's like the
format!
macro, but with inversed rules regarding curly braces – it preserves single braces
and uses double braces for interpolation. Please note that it preserves spaces, so
Position {{ix}}
and Position{{ix}}
mean different things, and the latter will generate
Position1
, Position2
, etc.
#[crabtime::function]
fn gen_positions2(components: Vec<String>) {
for dim in 1 ..= components.len() {
let cons = components[0..dim].join(",");
crabtime::output! {
enum Position{{dim}} {
{{cons}}
}
}
}
}
gen_positions2!(["X", "Y", "Z", "W"]);
Generating output by using crabtime::quote!
The crabtime::quote!
macro is just like crabtime::output!
, but instead of outputting the
code immediately, it returns it (as a String
), so you can store it in a variable and re-use
it across different subsequent calls to crabtime::quote!
or crabtime::output!
.
#[crabtime::function]
fn gen_positions3(components: Vec<String>) -> String {
let structs = (1 ..= components.len()).map(|dim| {
let cons = components[0..dim].join(",");
crabtime::quote! {
enum Position{{dim}} {
{{cons}}
}
}
}).collect::<Vec<String>>();
structs.join("\n")
}
gen_positions3!(["X", "Y", "Z", "W"]);
Generating output by returning a string or number
You can simply return a string or number from the function. It will be used as the generated macro code.
#[crabtime::function]
fn gen_positions4(components: Vec<String>) -> String {
(1 ..= components.len()).map(|dim| {
let cons = components[0..dim].join(",");
format!("enum Position{dim} {{ {cons} }}")
}).collect::<Vec<_>>().join("\n")
}
gen_positions4!(["X", "Y", "Z", "W"]);
Generating output by using crabtime::output_str!
Alternatively, you can use the crabtime::output_str!
macro to immediately write strings to
the code output buffer:
#[crabtime::function]
fn gen_positions5(components: Vec<String>) {
for dim in 1 ..= components.len() {
let cons = components[0..dim].join(",");
crabtime::output_str!("enum Position{dim} {{ {cons} }}")
}
}
gen_positions5!(["X", "Y", "Z", "W"]);
Generating output by returning a TokenStream
Finally, you can output TokenStream from the macro. Please note that for
brevity the below example uses inline dependency injection,
which is described later. In real code you should use your Cargo.toml
's
[build-dependencies]
section to include the necessary dependencies instead.
#[crabtime::function]
fn gen_positions6() -> proc_macro2::TokenStream {
// Inline dependencies used for brevity.
// You should use [build-dependencies] section in your Cargo.toml instead.
#![dependency(proc-macro2 = "1")]
#![dependency(syn = "2")]
#![dependency(quote = "1")]
use proc_macro2::Span;
use quote::quote;
let components = ["X", "Y", "Z", "W"];
let defs = (1 ..= components.len()).map(|dim| {
let cons = components[0..dim].iter().map(|t|
syn::Ident::new(t, Span::call_site())
);
let ident = syn::Ident::new(&format!("Position{dim}"), Span::call_site());
quote! {
enum #ident {
#(#cons),*
}
}
}).collect::<Vec<_>>();
quote! {
#(#defs)*
}
}
gen_positions6!();
📥 Input
Similarly to generating output, there are several ways to parametrize macros and provide them with input at their call site. We recommend you use the pattern parametrization, as it's the simplest and easiest to maintain.
Input by using supported arguments
Currently, you can use any combination of the following types as arguments to your macro and
they will be automatically translated to patterns: Vec<...>
, &str
, String
, and numbers.
If the expected argument is a string, you can pass either a string literal or an identifier,
which will automatically be converted to a string.
#[crabtime::function]
fn gen_positions7(name: String, components: Vec<String>) {
for dim in 1 ..= components.len() {
let cons = components[0..dim].join(",");
crabtime::output! {
enum {{name}}{{dim}} {
{{cons}}
}
}
}
}
gen_positions7!(Position, ["X", "Y", "Z", "W"]);
gen_positions7!(Color, ["R", "G", "B"]);
Input by using patterns
In case you want even more control, you can use the same patterns as
macro_rules!
by using a special pattern!
macro, and you can expand any pattern
using the expand!
macro:
expand!
macro simply passes its input along. It is used
only to make the code within the function a valid Rust code block. Thus, you do not need to use
it if you want to expand variables within other macros, like stringify!
.
// Please note that we need to type the pattern argument as `_` to make the
// code a valid Rust code.
#[crabtime::function]
fn gen_positions8(pattern!($name:ident, $components:tt): _) {
let components = expand!($components);
for dim in 1 ..= components.len() {
let cons = components[0..dim].join(",");
// We don't need to use `expand!` here.
let name = stringify!($name);
crabtime::output! {
enum {{name}}{{dim}} {
{{cons}}
}
}
}
}
gen_positions8!(Position, ["X", "Y", "Z", "W"]);
gen_positions8!(Color, ["R", "G", "B"]);
Input by using TokenStream
Alternatively, you can consume the provided input as a TokenStream:
#[crabtime::function]
fn gen_positions9(name: TokenStream) {
#![dependency(proc-macro2 = "1")]
let components = ["X", "Y", "Z", "W"];
let name_str = name.to_string();
for dim in 1 ..= components.len() {
let cons = components[0..dim].join(",");
crabtime::output! {
enum {{name_str}}{{dim}} {
{{cons}}
}
}
}
}
gen_positions9!(Position);
🚀 Performance
The lifecycle of a Crabtime macro is similar to that of a procedural macro. It is compiled as a
separate crate and then invoked to transform input tokens into output tokens. On the unstable
Rust channel, Crabtime and procedural macros have the same performance. On the stable channel,
Crabtime requires slightly more time than a procedural macro after you change your macro
definition. In other words, Crabtime’s performance is similar to procedural macros. It has
higher compilation overhead than macro_rules!
but processes tokens and complex
transformations faster.
Proc Macro |
Crabtime |
macro_rules! |
|
---|---|---|---|
First evaluation (incl. compilation) | ⚠️ Relatively slow | ⚠️ Relatively slow | ✅ Fast |
Next evaluation (on call-site change) | ✅ Fast | ✅ Fast on nightly ⚠️ Relatively slow on stable |
❌ Slow for complex transformations |
Cost after changing module code without changing macro-call site code | ✅ Zero | ✅ Zero | ✅ Zero |
Cache
When a Crabtime macro is called, it creates a new Rust project, compiles it, evaluates it, and
interprets the results as the generated Rust code. When you call the macro again (for example,
after changing the macro’s parameters or calling the same macro in a different place), Crabtime
can reuse the previously generated project. This feature is called “caching.” It is enabled by
default on the nightly channel and can be enabled on the stable channel by providing a module
attribute, for example:
#[crabtime::function(cache_key=my_key)]
#[module(my_crate::my_module)]
fn my_macro() {
// ...
}
The cache is always written to
<project_dir>/target/debug/build/crabtime/<module>/<macro_name>
. The defaults are presented
below:
Rust Unstable | Rust Stable | |
---|---|---|
Cache enabled | ✅ | ❌ by default, ✅ when module used. |
module default |
path to def-site module | none |
Please note that caching will be automatically enabled on the stable channel as soon as the proc_macro_span feature is stabilized. That feature allows Crabtime to read the path of the file where the macro was used, so it can build a unique cache key.
Performance Stats
Crabtime also generates runtime and performance statistics to help you understand how much time
was spent evaluating your macros, where projects were generated, and which options were used.
If you expand any usage of #[crabtime::function]
(for example, in your IDE), you will see
compilation stats like:
# Compilation Stats
Start: 13:17:09 (825)
Duration: 0.35 s
Cached: true
Output Dir: /Users/crabtime_user/my_project/target/debug/build/crabtime/macro_path
Macro Options: MacroOptions {
cache: true,
content_base_name: false,
}
Please note that you can be presented with the Cached: true
result even after the first
macro evaluation if your IDE or build system evaluated it earlier in the background.
🪲 Logging & Debugging
There are several ways to log from your Crabtime macros. Because
proc_macro::Diagnostic is
currently a nightly-only feature, Crabtime prints nicer warnings and errors if you are using
nightly Rust channel. They look just like warnings and errors from the Rust compiler.
Otherwise, your warnings and errors will be printed to the console with a [WARNING]
or
[ERROR]
prefix.
Method | Behavior on stable | Behavior on nightly |
---|---|---|
println! |
Debug log in console | Debug log in console |
crabtime::warning! |
Debug log in console | Warning in console |
crabtime::error! |
Debug log in console | Error in console |
Stdout Protocol
Please note that Crabtime uses stdout for all communication between the code generation process
and the host process. Depending on the prefix of each stdout line, it is interpreted according
to the following table. In particular, instead of using the methods shown above, you can
generate code from your macros by printing it to stdout (like
println!("[OUTPUT] struct T {}")
), but it's highly discouraged.
Prefix | Meaning |
---|---|
(none) | Debug log message (informational output). |
[OUTPUT] |
A line of generated Rust code to be included in the final macro output. |
[WARNING] |
A compilation warning. |
[ERROR] |
A compilation error. |
Stdout Protocol Utilities
Although you are not supposed to generate the Stdout Protocol messages manually, we believe that it is better to expose the underlying utilities so that in rare cases, you can use them to reduce the risk of malformed output. These functions allow you to transform multi-line strings by adding the appropriate prefixes:
mod crabtime {
fn prefix_lines_with(prefix: &str, input: &str) -> String {
// Adds the given prefix to each line of the input string.
# panic!()
}
fn prefix_lines_with_output(input: &str) -> String {
// Adds `[OUTPUT]` to each line of the input string.
# panic!()
}
fn prefix_lines_with_warning(input: &str) -> String {
// Adds `[WARNING]` to each line of the input string.
# panic!()
}
fn prefix_lines_with_error(input: &str) -> String {
// Adds `[ERROR]` to each line of the input string.
# panic!()
}
}
These macros allow you to directly print prefixed lines to stdout
, following the protocol:
mod crabtime {
macro_rules! output_str {
// Outputs code by printing a line prefixed with `[OUTPUT]`.
# () => {};
}
macro_rules! warning {
// On the nightly channel prints a compilation warning.
// On the stable channel prints a log prefixed with `[WARNING]`.
# () => {};
}
macro_rules! error {
// On the nightly channel prints a compilation error.
// On the stable channel prints a log prefixed with `[ERROR]`.
# () => {};
}
}
⚙️ Macro Cargo Configuration
Every Crabtime macro is a separate Cargo project with its own configuration and dependencies. If you use nightly, Crabtime automatically uses your Cargo.toml configuration. On stable, due to lack of proc_macro_span stabilization, Crabtime cannot discover your Cargo.toml automatically. You must provide cargo configuration in your macro blocks, for example:
#[crabtime::function]
fn my_macro() {
// Do this only on Rust stable channel. On the unstable channel
// use your Cargo.toml's [build-dependencies] section instead.
#![edition(2024)]
#![resolver(3)]
#![dependency(anyhow = "1.0")]
type Result<T> = anyhow::Result<T>;
// ...
}
Crabtime recognizes these Cargo configuration attributes. The attributes below override any configuration discovered in your Cargo.toml, even on nightly:
Supported Cargo Configuration Attributes
Attribute | Default |
---|---|
#![edition(...)] |
2024 |
#![resolver(...)] |
3 |
#![dependency(...)] |
[] |
📚 Attributes
You can provide any set of global attributes (#![...]
) on top of your Crabtime macro
definition for them to be applied to the given generated Crabtime crate.
🗺️ Paths
Crabtime macros provide access to several path variables, allowing you to traverse your
project's folder structure during macro evaluation. All paths are accessible within the
crabtime::
namespace.
Path | Availability | Description |
---|---|---|
WORKSPACE_PATH |
Stable & Nightly | Path to the root of your project. This is where the top-most Cargo.toml resides, whether it's a single-crate project or a Cargo workspace. |
CRATE_CONFIG_PATH |
Nightly only | Path to the Cargo.toml file of the current crate. |
CALL_SITE_FILE_PATH |
Nightly only | Path to the file where the macro was invoked. |
#[crabtime::function]
fn check_paths() {
println!("Workspace path: {}", crabtime::WORKSPACE_PATH);
}
check_paths!();
📖 How It Works Under The Hood
The content of a function annotated with crabtime::function
is pasted into the main
function of a temporary Rust project. This project is created, compiled, executed, and (if
caching is disabled) removed at build time, and its stdout
becomes the generated Rust code.
The generated main
function looks something like this:
const SOURCE_CODE: &str = "..."; // Your code as a string.
mod crabtime {
// Various utils described in this documentation.
# pub fn push_as_str(str: &mut String, result: &()) {}
# pub fn prefix_lines_with_output(input: &str) -> String { String::new() }
}
fn main() {
let mut __output_buffer__ = String::new();
let result = {
// Your code.
};
crabtime::push_as_str(&mut __output_buffer__, &result);
println!("{}", crabtime::prefix_lines_with_output(&__output_buffer__));
}
The output!
macro is essentially a shortcut for writing to output buffer using format!
, so
this:
#[crabtime::function]
fn my_macro_expansion1(components: Vec<String>) {
for dim in 1 ..= components.len() {
let cons = components[0..dim].join(",");
crabtime::output! {
enum Position{{dim}} {
{{cons}}
}
}
}
}
my_macro_expansion1!(["X", "Y", "Z", "W"]);
Is equivalent to:
#[crabtime::function]
fn my_macro_expansion2(pattern!([$($components_arg:expr),*$(,)?]): _) {
let components: Vec<String> = expand!(
[$(crabtime::stringify_if_needed!($components_arg).to_string()),*]
).into_iter().collect();
for dim in 1 ..= components.len() {
let cons = components[0..dim].join(",");
crabtime::output_str! {"
enum Position{dim} {{
{cons}
}}
"}
}
}
my_macro_expansion2!(["X", "Y", "Z", "W"]);
And that, in turn, is just the same as:
#[crabtime::function]
fn my_macro_expansion3() {
let components = ["X", "Y", "Z", "W"];
for dim in 1 ..= components.len() {
let cons = components[0..dim].join(",");
__output_buffer__.push_str(
&format!("enum Position{dim} {{ {cons} }}\n")
);
}
}
my_macro_expansion3!();
Which, ultimately, is equivalent to:
#[crabtime::function]
fn my_macro_expansion4() {
let components = ["X", "Y", "Z", "W"];
for dim in 1 ..= components.len() {
let cons = components[0..dim].join(",");
println!("[OUTPUT] enum Position{dim} {{");
println!("[OUTPUT] {cons}");
println!("[OUTPUT] }}");
}
}
my_macro_expansion4!();
⚠️ Corner Cases
There are a few things you should be aware of when using Crabtime:
- Caching is associated with the current file path. It means that if in a single file you have multiple Crabtime macros of the same name (e.g. by putting them in different modules within a single file), they will use the same Rust project under the hood, which effectively breaks the whole purpose of caching.
- You can't use Crabtime functions to generate consts. Instead, use
Crabtime::eval!
as shown above. This is because when expanding constants, macros need to produce an additional pair of{
and}
around the expanded tokens. If anyone knows how to improve this, please contact us. - Error spans from the generated code are not mapped to your source code. It means that you will still get nice, colored error messages, but the line/column numbers will be pointing to the generated file, not to your source file. This is an area for improvement, and I'd be happy to accept a PR that fixes this.
Crabtime::eval!
does not use caching, as there is no name we can associate the cache with.
⚠️ Troubleshooting
⚠️ Note: Rust IDEs differ in how they handle macro expansion. This macro is tuned for
rustc
and RustRover
’s expansion engines.
If your IDE struggles to correctly expand crabtime::output!
, you can switch to the
crabtime::output_str!
syntax described above. If you encounter this, please
open an issue to let us know!
[^supported_ides]: This code was thoroughly tested in rustc
, the IntelliJ/RustRover Rust expansion engine, and Rust Analyzer (VS Code, etc.).
Dependencies
~0.5–1MB
~24K SLoC