Crate Interactions
This chapter explains how the seven aspect-rs crates work together to provide a complete AOP framework. Understanding these interactions is crucial for advanced usage and extension.
Interaction Overview
User Code
↓
#[aspect(LoggingAspect::new())] ← aspect-macros
↓
Macro Expansion
↓
Generated Code using:
├── JoinPoint ← aspect-core
├── Aspect trait ← aspect-core
└── LoggingAspect ← aspect-std
↓
Runtime Execution
↓
Optional: AspectRegistry ← aspect-runtime
Phase 1: Basic Macro-Based AOP
Components Involved
- aspect-core: Provides
Aspecttrait andJoinPoint - aspect-macros: Implements
#[aspect]macro - aspect-std: Provides standard aspects
Data Flow
┌─────────────┐
│ User Code │
└──────┬──────┘
│ #[aspect(Logger)]
↓
┌─────────────────┐
│ aspect-macros │ Parse function
│ │ Extract metadata
│ │ Generate wrapper
└────────┬────────┘
│ TokenStream
↓
┌──────────────────┐
│ Expanded Code │
│ │
│ fn my_func() { │
│ let ctx = ... │ ← JoinPoint from aspect-core
│ aspect.before │ ← Aspect::before from aspect-core
│ // original │
│ aspect.after │
│ } │
└──────────────────┘
Example Interaction
Input (user code):
#![allow(unused)]
fn main() {
use aspect_core::prelude::*;
use aspect_macros::aspect;
use aspect_std::LoggingAspect;
#[aspect(LoggingAspect::new())]
fn calculate(x: i32) -> i32 {
x * 2
}
}
Step 1: Macro Parsing (aspect-macros)
The macro receives:
- Attribute:
LoggingAspect::new() - Function:
fn calculate(x: i32) -> i32 { x * 2 }
Step 2: Metadata Extraction
aspect-macros extracts:
#![allow(unused)]
fn main() {
FunctionMetadata {
name: "calculate",
params: vec![("x", "i32")],
return_type: "i32",
visibility: Visibility::Inherited,
// ...
}
}
Step 3: Code Generation
aspect-macros generates:
#![allow(unused)]
fn main() {
fn calculate(x: i32) -> i32 {
// From aspect-core
let __ctx = aspect_core::JoinPoint {
function_name: "calculate",
module_path: module_path!(),
location: aspect_core::Location {
file: file!(),
line: line!(),
},
};
// From aspect-std
let __aspect = aspect_std::LoggingAspect::new();
// Aspect::before from aspect-core trait
<aspect_std::LoggingAspect as aspect_core::Aspect>::before(
&__aspect,
&__ctx
);
// Original function body
let __result = {
let x = x;
x * 2
};
// Aspect::after from aspect-core trait
<aspect_std::LoggingAspect as aspect_core::Aspect>::after(
&__aspect,
&__ctx,
&__result
);
__result
}
}
Step 4: Compilation
The Rust compiler:
- Type-checks the generated code
- Inlines aspect calls (if possible)
- Optimizes away dead code
- Generates final binary
Phase 2: Pointcut-Based AOP
Additional Components
- aspect-pointcut: Parses and matches pointcut expressions
- aspect-runtime: Global registry for aspects
- aspect-weaver: Advanced code generation
Data Flow
┌──────────────────┐
│ #[advice] │
│ pointcut="..." │
└────────┬─────────┘
│
↓
┌────────────────────┐
│ aspect-runtime │
│ Register aspect │
│ + pointcut │
└────────┬───────────┘
│
↓
┌────────────────────┐
│ Compilation │
│ For each function │
└────────┬───────────┘
│
↓
┌────────────────────┐
│ aspect-pointcut │
│ Match against │
│ registered aspects │
└────────┬───────────┘
│ Matching aspects
↓
┌────────────────────┐
│ aspect-weaver │
│ Generate optimized │
│ wrapper code │
└────────────────────┘
Example Interaction
Step 1: Register Aspect
#![allow(unused)]
fn main() {
use aspect_macros::advice;
#[advice(
pointcut = "execution(pub fn *(..)) && within(crate::api)",
order = 10
)]
fn api_logging(pjp: ProceedingJoinPoint)
-> Result<Box<dyn Any>, AspectError>
{
println!("API call: {}", pjp.context().function_name);
pjp.proceed()
}
}
Step 2: Registration (aspect-runtime)
#![allow(unused)]
fn main() {
// Generated by #[advice] macro
static __ASPECT_REGISTRATION: Lazy<()> = Lazy::new(|| {
use aspect_runtime::*;
register_aspect(RegisteredAspect {
aspect: Arc::new(ApiLoggingAspect),
pointcut: Pointcut::parse(
"execution(pub fn *(..)) && within(crate::api)"
).unwrap(),
order: 10,
name: "api_logging".to_string(),
});
});
}
Step 3: Function Compilation
For each function in the crate:
#![allow(unused)]
fn main() {
pub fn fetch_user(id: u64) -> User {
// ...
}
}
Step 4: Pointcut Matching (aspect-pointcut)
#![allow(unused)]
fn main() {
let ctx = JoinPoint {
function_name: "fetch_user",
module_path: "crate::api",
// ...
};
// aspect-pointcut evaluates:
let pointcut = Pointcut::parse(
"execution(pub fn *(..)) && within(crate::api)"
)?;
let matches = pointcut.matches(&ctx);
// → true (public function in crate::api)
}
Step 5: Code Generation (aspect-weaver)
#![allow(unused)]
fn main() {
// aspect-weaver generates optimized code:
#[inline(always)]
pub fn fetch_user(id: u64) -> User {
const __CTX: JoinPoint = JoinPoint {
function_name: "fetch_user",
module_path: "crate::api",
location: Location { file: "api.rs", line: 42 },
};
let __pjp = ProceedingJoinPoint::new(
|| __original_fetch_user(id),
__CTX
);
match __ASPECT_API_LOGGING.around(__pjp) {
Ok(result) => *result.downcast::<User>().unwrap(),
Err(e) => panic!("Aspect error: {:?}", e),
}
}
fn __original_fetch_user(id: u64) -> User {
// Original function body
}
}
Phase 3: Automatic Weaving
Additional Components
- aspect-rustc-driver: Custom rustc driver for MIR analysis
Data Flow
┌──────────────────────┐
│ User Code │
│ (no annotations!) │
└──────────┬───────────┘
│
↓
┌──────────────────────┐
│ aspect-rustc-driver │
│ Command line args: │
│ --aspect-pointcut │
│ "execution(...)" │
└──────────┬───────────┘
│
↓
┌──────────────────────┐
│ rustc Compilation │
│ Normal compilation │
│ with callbacks │
└──────────┬───────────┘
│
↓
┌──────────────────────┐
│ AspectCallbacks │
│ Override queries │
└──────────┬───────────┘
│ TyCtxt access
↓
┌──────────────────────┐
│ MIR Analyzer │
│ Extract all funcs │
└──────────┬───────────┘
│ FunctionInfo
↓
┌──────────────────────┐
│ aspect-pointcut │
│ Match patterns │
└──────────┬───────────┘
│ Matched funcs
↓
┌──────────────────────┐
│ aspect-weaver │
│ Generate wrappers │
└──────────┬───────────┘
│
↓
┌──────────────────────┐
│ Optimized Binary │
└──────────────────────┘
Example Interaction
Step 1: Compile with Driver
aspect-rustc-driver \
--aspect-pointcut "execution(pub fn *(..))" \
--aspect-verbose \
main.rs --crate-type bin
Step 2: rustc Integration
// aspect-rustc-driver main()
fn main() {
let mut args = env::args().collect::<Vec<_>>();
// Extract aspect-specific flags
let config = AspectConfig::from_args(&mut args);
// Run rustc with custom callbacks
rustc_driver::RunCompiler::new(
&args,
&mut AspectCallbacks::new(config)
).run().unwrap();
}
Step 3: Compiler Callbacks
#![allow(unused)]
fn main() {
impl Callbacks for AspectCallbacks {
fn config(&mut self, config: &mut interface::Config) {
// Override the analysis query
config.override_queries = Some(|_sess, providers| {
providers.analysis = analyze_crate_with_aspects;
});
}
}
}
Step 4: MIR Analysis
#![allow(unused)]
fn main() {
fn analyze_crate_with_aspects(tcx: TyCtxt<'_>, (): ()) {
let analyzer = MirAnalyzer::new(tcx, verbose);
// Extract all functions from MIR
let functions = analyzer.extract_all_functions();
// → [
// FunctionInfo { name: "main", visibility: Public, ... },
// FunctionInfo { name: "fetch_user", visibility: Public, ... },
// FunctionInfo { name: "helper", visibility: Private, ... },
// ]
// Get pointcuts from config
let pointcuts = config.pointcuts;
// → ["execution(pub fn *(..))"]
// Match functions against pointcuts (aspect-pointcut)
for func in functions {
for pointcut in &pointcuts {
if pointcut.matches(&func) {
println!("✓ Matched: {}", func.name);
}
}
}
}
}
Step 5: Automatic Weaving
For matching functions, aspect-weaver generates code automatically (no manual annotations needed!).
Cross-Crate Dependencies
Dependency Chain
User Application
↓
depends on aspect-macros (for #[aspect])
↓
aspect-macros
↓
dev-depends on aspect-core (for tests)
↓
aspect-core
(no dependencies)
Runtime Dependencies
User Application (using aspects)
↓
uses aspect-std (for standard aspects)
↓
aspect-std
↓
depends on aspect-core
Optional Dependencies
User Application (using Phase 2)
↓
aspect-runtime (for global registry)
↓
aspect-pointcut (for pattern matching)
↓
aspect-core
Compilation Flow
Phase 1 Compilation
1. User writes code with #[aspect]
2. cargo build invokes rustc
3. rustc expands proc macros (aspect-macros)
4. Expanded code references aspect-core types
5. Linker resolves symbols from aspect-core
6. Binary created
Phase 2 Compilation
1. User writes code with #[advice]
2. cargo build invokes rustc
3. #[advice] macro registers aspect (aspect-runtime)
4. Other functions get woven if pointcut matches
5. aspect-weaver optimizes generated code
6. Binary created with registered aspects
Phase 3 Compilation
1. User writes code (no annotations)
2. aspect-rustc-driver invoked instead of rustc
3. Normal compilation proceeds
4. Callbacks intercept after MIR generation
5. MirAnalyzer extracts function metadata
6. aspect-pointcut matches functions
7. aspect-weaver generates wrappers
8. Compilation continues with woven code
9. Optimized binary created
Communication Patterns
Compile-Time Communication
aspect-macros → aspect-core:
- Generates code using
JoinPointstruct - References
Aspecttrait methods - Uses
AspectErrorfor error handling
aspect-runtime → aspect-pointcut:
- Stores
Pointcutinstances - Calls
matches()method during registration
aspect-weaver → aspect-core:
- Generates calls to
Aspecttrait methods - Creates
ProceedingJoinPointinstances
Runtime Communication
User code → aspect-std:
- Calls aspect methods through
Aspecttrait - Passes
JoinPointcontext
aspect-std → aspect-core:
- Implements
Aspecttrait - Returns
AspectErroron failure
Integration Points
1. Proc Macro Interface
#![allow(unused)]
fn main() {
// aspect-macros provides
#[proc_macro_attribute]
pub fn aspect(attr: TokenStream, item: TokenStream) -> TokenStream
// User code consumes
#[aspect(MyAspect)]
fn my_function() { }
}
2. Trait Implementation
#![allow(unused)]
fn main() {
// aspect-core defines
pub trait Aspect: Send + Sync { ... }
// aspect-std implements
impl Aspect for LoggingAspect { ... }
// User code uses
#[aspect(LoggingAspect::new())]
}
3. Registry Interface
#![allow(unused)]
fn main() {
// aspect-runtime provides
pub fn register_aspect(aspect: RegisteredAspect)
// aspect-macros (#[advice]) calls
register_aspect(RegisteredAspect { ... })
// User code benefits (automatically)
}
4. Pointcut Matching
#![allow(unused)]
fn main() {
// aspect-pointcut provides
impl Pointcut {
pub fn matches(&self, ctx: &JoinPoint) -> bool
}
// aspect-runtime uses
let matching = registry.aspects
.iter()
.filter(|a| a.pointcut.matches(ctx))
// aspect-rustc-driver uses
if pointcut.matches(&func_info) {
apply_aspect(func);
}
}
Performance Implications
Zero-Copy Interactions
JoinPoint is passed by reference:
#![allow(unused)]
fn main() {
fn before(&self, ctx: &JoinPoint) // No copy, no allocation
}
Static Dispatch
When aspect type is known at compile-time:
#![allow(unused)]
fn main() {
Logger::new().before(&ctx) // Direct call, no vtable
}
Dynamic Dispatch
When using trait objects:
#![allow(unused)]
fn main() {
let aspect: Arc<dyn Aspect> = ...;
aspect.before(&ctx) // Vtable lookup, small overhead
}
Inlining
With #[inline(always)]:
#![allow(unused)]
fn main() {
#[inline(always)]
fn wrapper() {
aspect.before(&ctx); // Can be inlined
original(); // Can be inlined
aspect.after(&ctx); // Can be inlined
}
// Entire wrapper may be inlined into caller
}
Error Handling Flow
User Function Error
↓
Caught by wrapper
↓
Convert to AspectError (if needed)
↓
Pass to aspect.after_error()
↓
Aspect handles error
↓
Propagate or recover
↓
Return to caller
Example
#![allow(unused)]
fn main() {
#[aspect(ErrorLogger)]
fn risky_operation() -> Result<i32, MyError> {
Err(MyError::Failed)
}
// Generated code:
fn risky_operation() -> Result<i32, MyError> {
let ctx = ...;
aspect.before(&ctx);
let result = {
Err(MyError::Failed)
};
match &result {
Ok(val) => aspect.after(&ctx, val),
Err(e) => {
let aspect_err = AspectError::from(e);
aspect.after_error(&ctx, &aspect_err);
}
}
result
}
}
See Also
- Crate Organization - Detailed crate responsibilities
- Principles - Design principles behind interactions
- Implementation - How interactions are implemented
- Phase 3 Architecture - Deep dive into rustc integration