Course contentsShow
← Rust
Lesson 1574 of 1,91013. Metaprogramming with MacrosFree lesson

Versioned Macro APIs

You will implement internal versioning strategies to allow macro evolution without breaking dependent consumers.

In this lesson, you’ll learn how to implement internal versioning strategies to evolve your macro APIs without breaking existing downstream consumers.

The Problem: Macro Fragility

When you publish a procedural macro crate, you are effectively creating a language extension. Unlike standard functions where you can add an optional argument or use #[deprecated], a breaking change in macro syntax causes immediate, non-recoverable compilation errors for every user. Versioning isn't just about SemVer; it’s about providing a migration path within the same crate version.

The Strategy: Dispatching by Metadata

The most robust way to handle this is to use version-tagged entry points. Instead of changing the syntax of #[my_macro], you introduce a version attribute or use the existing proc_macro attribute to act as a dispatcher.

Consider a scenario where you want to move from #[my_macro(legacy_args)] to a cleaner #[my_macro(modern_config)]. You can implement a internal router:

#[proc_macro_attribute]
pub fn my_macro(args: TokenStream, input: TokenStream) -> TokenStream {
    let args_tree = parse_macro_input!(args as AttributeArgs);
    
    // Check for version markers in the macro arguments
    if is_v2_syntax(&args_tree) {
        expand_v2(args_tree, input)
    } else {
        // Transparently route to legacy logic
        expand_v1(args_tree, input)
    }
}

Implementing Internal Namespacing

Another strategy is Hidden API modules. You can define your macro logic in a internal module hierarchy (e.g., crate::v1::expand and crate::v2::expand). By using syn to parse the structure of the input, you can detect the presence of a "version marker" (like an internal trait or a specific attribute flag) and route the AST transformation accordingly.

This allows users to opt-in to new features by adding a version = "2" tag to their macro call, while you maintain the v1 parser for legacy support. By segregating the logic into separate modules, you ensure that your code remains testable and that side-effect changes in v2 never bleed into the stable v1 expansion logic.

Key Takeaway: Use internal dispatching based on macro arguments to allow multiple versions of your DSL to coexist, ensuring backward compatibility while you iterate on complex metaprogramming logic.