← Back to lab
Lab

ulo

A macro that cannot see a type's impl block should emit the same call for every type and let method resolution choose the definition.

↓

What a macro can see

A procedural macro attached to a struct receives the struct's tokens and nothing else. The impl block beside it, where a NestJS-style provider's constructor and lifecycle hooks are written, is not in its input. NestJS reads those at runtime by reflection. Rust has no reflection.

Dispatch instead of detection

  • Detecting whether a type has a constructor, or a given hook, is not possible, because the impl is not in the input. Specialization would let generated code branch on what a type implements, and it is unstable.
  • A blanket trait gives every type a no-op default for each hook.
  • The attribute on a user-written hook emits an inherent method of the same name that forwards to it.
  • Generated startup code calls that name on every provider. Where the inherent method exists it wins method resolution. Otherwise the no-op runs.
  • Five hooks are called on every provider whether it defines them or not. The same mechanism, returning an Option, decides which enhancer roles a type registers.

The concrete-type rule

  • The blanket impl also covers Arc<Struct>, and the provider wrapper holds an Arc. Method syntax on the wrapper binds the no-op at the Arc layer. The hook never fires and nothing reports it.
  • Generated code uses fully qualified call syntax.
  • Inside a generic function the bound is erased and the dispatch always takes the fallback.

Boot-time wiring

  • The container is a registry keyed by each type's name, built in per-module topological order.
  • A missing provider or a dependency cycle fails at boot with the chain named in the error. Nothing checks either at compile time.
  • For a while two code paths built those keys in two formats, and an alias worked where the written generic did not. One function now produces every key.

Calls through a pointer or a generic

Dispatch works where the generated call is made on the concrete type. Through a smart pointer with method syntax, or inside generic code, it returns the no-op and reports nothing. A framework that must call hooks from generic code needs either the detection it cannot have or a trait the user implements by hand.

Up next
winston→