Composable Runtime Interceptor
Aspect-Oriented Programming for Wasm Components
The Composable Runtime Interceptor library creates an interceptor component that wraps another component's exported functions, inserting before and after advice hooks around every call without modifying the target component. The generated interceptor component can then be composed with any component that implements the generic advice interface and any component that implements the selected target interface(s).
┌─────────────┐ ┌─────────────┐
│ interceptor │────▶│ target │
caller ──▶│ (generated) │ │ component │
│ │◀────│ │
└──────┬──────┘ └─────────────┘
│
before()│after()
▼
┌─────────────┐
│ advice │
│ component │
└─────────────┘
The Advice Protocol
The interceptor imports modulewise:interceptor/advice@0.1.0. Any advice component implementation can export this interface.
interface advice {
use types.{arg, value};
resource invocation {
constructor(function-name: string, args: list<arg>);
before: func() -> before-action;
after: func(ret: option<value>) -> after-action;
}
}
For each intercepted call, the interceptor:
- Creates an
invocationwith the function name and arguments - Calls
before()so the advice can inspect/modify args, skip the target, or proceed - Calls the target function (unless skipped or error)
- Calls
after()with the return value so the advice can accept, repeat the call, or error
before-action
| Variant | Meaning |
|---|---|
proceed(list<arg>) |
Call the target with these args (complex-typed values use originals) |
skip(option<value>) |
Return this value directly without calling the target |
error(string) |
Trap immediately |
after-action
| Variant | Meaning |
|---|---|
accept(option<value>) |
Return this value to the caller |
repeat(list<arg>) |
Call the target again with these args |
error(string) |
Trap immediately |
arg and value
record arg {
name: string, // parameter name, e.g. "count"
type-name: string, // WIT type name, e.g. "u32"
value: value,
}
variant value {
str(string),
num-s64(s64),
num-u64(u64),
num-f32(f32),
num-f64(f64),
boolean(bool),
complex(string), // opaque (advice sees the type name but not the value)
}
Primitive types (bool, integers, floats, char) and strings are fully readable and writable by advice. Complex types (records, lists, variants, etc.) are passed as complex("") which means advice cannot read, modify, or replace them.
[!NOTE] If access to complex types is required within an interceptor implementation, implement a dedicated component that explicitly imports and exports the same interface as exported by the target component (instead of generic cross-cutting advice).
CLI
interceptor --world <world> [--wit <path>] [--match <pattern>]... --output <file>
| Flag | Default | Description |
|---|---|---|
--world |
(required) | World name whose exports define the interceptor contract |
--wit |
wit/ |
Path to WIT file or directory |
--match |
(none => intercept all) | Pattern for selective interception (repeatable) |
--output / -o |
(required) | Output path for the generated interceptor .wasm |
Examples
Intercept everything exported by my-world:
Intercept only say-hello in the greeter interface:
Intercept all functions in the greeter interface, bypass anything else:
Intercept all functions across all interfaces in the modulewise namespace:
Pattern Syntax
Patterns select which exported functions to intercept. Functions not matched are bypassed as direct aliases. If no --match flags are provided, all functions are intercepted.
| Pattern form | Matches |
|---|---|
namespace:pkg/iface#func |
Exact function in exact interface |
namespace:pkg/iface |
All functions in that interface |
namespace:pkg/* |
All functions in all interfaces of that package |
namespace:* |
All functions in all packages of that namespace |
* |
Everything |
func-name |
Direct (world-level) function by exact name |
func-* |
Direct functions matching the wildcard |
Library API
The Composable Runtime Interceptor is usable as a library crate for programmatic interceptor generation.
[]
= { = "composable-interceptor", = "0.1" }
From a WIT path
use Path;
let bytes = create_from_wit?;
write?;
From a Wasm Component binary
When a target component is available, the WIT world can be extracted from the component's embedded type information:
let target_bytes: & = /* ... */;
let interceptor_bytes = create_from_component?;
Both functions return validated wasm component bytes ready for composition.
Examples
| Example | Demonstrates |
|---|---|
examples/square |
Direct function exports, primitive types |
examples/uppercase |
String parameter and return value |
examples/logging |
Multiple interfaces, WASI logging import in advice |
[!NOTE] In each of the examples above, the advice components are implemented in Rust, but wasm composition works at the bytecode level regardless of source language. To demonstrate this, the
squareexample uses a target component implemented in Python, and theuppercaseexample uses a target component implemented in JavaScript. The logging example uses Rust for all of its components, but itsgreeterandcalculatorcomponents export the same WIT functions as those other two examples.
Limitations
- Complex types in advice: Records, lists, variants, etc. are forwarded opaquely. Advice can observe their presence (via
type-name) but cannot read, modify, or replace them. - Error handling: When advice returns
error(string), the interceptor traps. The error string is not currently surfaced to the host, but in the future might be written to an address that is known to the composable-runtime host for better error reporting.
How It Works
Given a WIT world, the interceptor generates three core wasm modules and assembles them into a single wasm component:
- main module: marshals arguments into the advice protocol, calls
before()andafter(), and handles the returned actions - shim module: provides indirect call stubs so the main module can be instantiated before its real imports are available
- fixup module: patches the shim's table with the real lowered function references at instantiation time
The component imports the target interface(s) and modulewise:interceptor/advice, then re-exports the target interface(s) with interception applied. Bypassed functions are aliased. The generated component is a standard wasm component with no special runtime support required. Notice that the run scripts in the examples use wasmtime directly instead of composable-runtime.