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

Maintaining Macro Compatibility

You will ensure your macros adhere to semver constraints while evolving the underlying AST logic.

In this lesson, you will learn how to evolve your procedural macros without breaking the code of users who depend on your library.

When you ship a procedural macro, the code it generates becomes part of your public contract. If you change the internal logic of your TokenStream manipulation, you risk emitting code that fails to compile for existing users. Maintaining compatibility requires viewing your macro not just as a tool, but as a versioned API.

The Problem: Implicit Dependencies

Often, a macro generates code that relies on your helper crate. For example, if your macro #[derive(Serialize)] expands to call my_crate::internal::encode(), you have locked your macro into that specific internal structure. If you later rename that module or move a function, all user code using your macro will break, even if you bumped your major version.

Strategies for Compatibility

To maintain stability, follow these two patterns:

  1. The Re-export Pattern: Never generate code that references your internal paths directly. Instead, have your macro verify or assume the existence of an entry point in the user's crate, or provide a stable path via a re-export.
    // Instead of generating: my_crate::internal::helper()
    // Generate code that calls a stable re-export:
    pub use crate::runtime::helper as __macro_helper;
    
  2. Stable AST Projection: When evolving your macro, avoid changing the shape of the generated tokens. If you are updating the logic inside your syn-based parsing, ensure the resulting quote! block produces the same structure. If you must change the output, use feature flags to allow users to opt-in to the new "v2" generated syntax, or use a bridge macro that maps old syntax to your new implementation.

Managing Schema Changes

If you modify your attribute arguments, you must treat the macro input like a serialized data structure. Add new arguments as Option types or use #[default] attributes in your syn structs. This ensures that a user’s existing macro invocation continues to parse correctly while allowing you to introduce expanded features incrementally.

Key Takeaway: Treat the output of your procedural macro as a public API surface that requires the same SemVer rigor as any library function or struct.