RFA-107 · Case file with fixtures · Case 79 of 694 · Cargo workspace evidence
Why a Proc-Macro Crate Cannot Export Ordinary Public Items
A proc-macro crate has a specialized compiler-facing export surface. Keep expansion helpers private, and place reusable runtime or public support APIs in an ordinary companion crate.
- Reviewed
- Rust
- Rust 1.98.1, Cargo 1.98.1
- Targets
- host compiler target
- Profiles
- check, dev, release, test
Direct answer
What this Rust failure means
- Why it happens
- A proc-macro target is a compiler-loaded plugin artifact whose external exports are restricted to function-like, derive, and attribute macro entry points.
- First discriminating check
- List public root items in the proc-macro crate and separate private expansion helpers from consumer-facing runtime API.
A procedural macro library looks like a Rust library, but it is not an ordinary reusable library artifact. The failing crate exports one valid macro and one normal public helper. Cargo reaches rustc, and rustc rejects the helper:
`proc-macro` crate types currently cannot export any items other than functions
tagged with `#[proc_macro]`, `#[proc_macro_derive]`, or `#[proc_macro_attribute]`
Making the helper private is enough for the small fixture. In a production macro package, the error often reveals that compile-time code and runtime API were placed in the same crate.
A proc macro is loaded by the compiler
The procedural macro reference defines macros as Rust functions that receive and return token streams. The compiled dynamic library is loaded during compilation of another crate. Its public exported interface is the set of procedural macro entry points the compiler recognizes.
This has a different role from an rlib linked into the final program. A normal public Config type or helper function does not become an API consumers can import from the macro crate in the usual way.
Cargo's library target documentation distinguishes crate types, including proc-macro. Setting proc-macro = true is not an annotation on one function; it changes the library target.
Private expansion helpers are normal
The repaired source keeps helper private and calls it from the macro entry point. Parser functions, validation logic, token emitters, and internal error utilities can all live privately in the macro crate.
They do not need pub merely because several modules use them. pub(crate) and narrower module visibility remain available for internal organization, while the crate's external exports stay limited to macro entry points.
I keep the actual entry function thin:
TokenStream -> parse -> validate -> internal model -> emit -> TokenStream
This makes parsing and validation easier to test as ordinary functions inside the crate.
Put public runtime API in a companion crate
Many macros generate code that refers to traits or types at runtime. Those items belong in a normal library crate. A common structure is:
my-library ordinary traits and runtime types
my-library-macros proc-macro implementation
The ordinary crate may depend on and re-export macros for user convenience. The macro crate may depend on the ordinary crate only if that direction does not create a package cycle. Often I keep shared contracts small or generate paths that refer to the public facade.
The split is visible in widely used Rust projects because it matches compilation reality: the macro runs on the host during compilation, while generated runtime code may target another architecture.
Re-exporting creates path questions
If the normal crate re-exports a derive macro, generated code should use a stable path. Renamed dependencies, crate self-use, and facade crates can make a hard-coded absolute crate name fail.
I test at least two consumers: one using the expected dependency name and one renaming it in Cargo.toml. I also test the macro inside the facade crate if that use is supported. The solution can involve $crate for declarative macros or resolving package names carefully for procedural output; there is no universal string that works for every topology.
This path problem is separate from the export restriction, but the companion-crate design makes it visible early.
Do not change crate type to dodge the message
Removing proc-macro = true permits ordinary exports, but then #[proc_macro] functions are not valid macro exports. Trying to create one target that is both the runtime library and proc-macro library blurs two incompatible artifact roles.
Similarly, marking every public helper with a proc-macro attribute is not a repair. Macro entry points must have the required token-stream signature and are invoked by the compiler, not as normal functions.
My debugging sequence
When this export error appears, I do this:
- Confirm which library target has
proc-macro = true. - List every externally public root item in that crate.
- Keep parsing and expansion helpers private.
- Move consumer-facing traits, data, and runtime helpers into an ordinary crate.
- Check the dependency direction for cycles.
- Test macro expansion from a separate consumer crate, including renamed dependencies.
The restriction is easier to remember when I stop treating the proc-macro crate as a general library. It is a compiler plugin boundary with three recognized entry forms. Everything else is either internal implementation or belongs in a normal artifact.