Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 Aspect trait and JoinPoint
  • 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:

  1. Type-checks the generated code
  2. Inlines aspect calls (if possible)
  3. Optimizes away dead code
  4. 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 JoinPoint struct
  • References Aspect trait methods
  • Uses AspectError for error handling

aspect-runtime → aspect-pointcut:

  • Stores Pointcut instances
  • Calls matches() method during registration

aspect-weaver → aspect-core:

  • Generates calls to Aspect trait methods
  • Creates ProceedingJoinPoint instances

Runtime Communication

User code → aspect-std:

  • Calls aspect methods through Aspect trait
  • Passes JoinPoint context

aspect-std → aspect-core:

  • Implements Aspect trait
  • Returns AspectError on 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