splicer 2.3.0

Plan and generate middleware splice operations for WebAssembly component composition graphs.
Documentation

splicer 🔍✂️🪡

Plan and generate middleware splice operations for WebAssembly component composition graphs.

splicer reads:

  • A composition graph (JSON)
  • A splice configuration (YAML)

It produces a modified plan that injects middleware components according to declarative rules.

This tool is designed to work with component-based systems such as WASI HTTP services, but is interface-agnostic and can splice across any interface edge in a component graph.


Why splicer?

When building component-based systems, middleware insertion often requires:

  • Rewriting instantiation chains
  • Re-threading handler references
  • Maintaining correct edge ordering
  • Traversing nested provider chains

splicer automates that planning step.

Instead of manually restructuring component wiring, you define:

  • What interface to target
  • Where to inject middleware
  • What middleware components to insert

And splicer generates the modified composition plan.

A demo of splicer can be run using: cargo run --example demo

A more in-depth usage of splicer is done in the external component-interposition repo.


Adapter Components

Most middleware doesn't need to match the exact type signature of the interface it's being placed on. A logging middleware that prints "before" and "after" around every call works the same whether the target interface is wasi:http/handler or my:service/adder, it only needs the function name.

Splicer generates adapter components that bridge between a generic middleware WIT interface and the specific target interface. The middleware author writes against a simple contract; splicer handles all the type plumbing at composition time.

Middleware Tiers

Tier Capability WIT Status
Tier 1 Hook (name only) — on-call, on-return, should-block: middleware sees the call identity but not types or data wit/tier1/world.wit Supported
Tier 2 Observe — middleware sees the typed values flowing through (lifted into a structural attribute tree); cannot modify wit/tier2/world.wit (planned) Planned
Tier 3 Transform — middleware sees AND modifies the values; downstream is still called wit/tier3/world.wit (planned) Planned
Tier 4 Virtualize — middleware replaces the downstream entirely (mocks, virts, replayers) wit/tier4/world.wit (planned) Planned

Each tier strictly adds one capability. Middleware written for a lower tier works unchanged when higher tiers become available.

To write a tier-1 middleware, your component exports one or more of the interfaces defined in wit/tier1/world.wit.

When splicer splice detects that a middleware exports these interfaces (instead of the target interface directly), it automatically generates an adapter component and wires it into the composition.

For the full guide — including how to write a tier-1 middleware, how adapter detection works, and what the generated adapter does internally — see docs/adapter-components.md.

Builtin Middleware

Splicer ships pre-built middleware components embedded in the binary. Reference one from a splice config without supplying a path:

inject:
  - builtin: hello-tier1

The bare prefix marks tier-1 builtins that see only the call-id — no payload-derived data on the emitted signal. Value-aware tier-2 siblings — otel-spans, otel-metrics, otel-logs (unprefixed) — are planned but not yet implemented; they'll arrive once tier-2 codegen lands.

Name Tier Description
hello-tier1 1 println!s every wrapped call. Verifies splice rules fire.
otel-bare-spans 1 Emits a wasi:otel span per call (timing + call-id attrs, no payload).
otel-bare-metrics 1 Emits wasi:otel count + duration metrics per call (no payload).
otel-bare-logs 1 Emits a structured wasi:otel log per call (severity INFO, no payload).

Source crates live under builtins/; rebuild artifacts with make build-builtins.

See docs/splice-config.md for the full builtin: schema (short + long forms).


Installation

From source:

cargo build --release

Binary will be located at:

target/release/splicer

Usage

Splicer has two subcommands. Both produce a composed .wasm directly.

splicer splice: inject middleware into an existing composition

splicer splice <SPLICE_CFG> <COMP_WASM> [-o composed.wasm]

Reads splice rules from SPLICE_CFG (YAML), splits COMP_WASM into its sub-components, injects middleware per the rules, and writes the result to composed.wasm.

splicer compose: synthesize a composition from N components

splicer compose <COMP_WASM>... [-o composed.wasm]

Discovers the composition graph by matching the components' import/export surfaces and writes the composed result.

Common flags

flag description
-o, --output <PATH> Where to write the composed .wasm (default: composed.wasm).
--emit-wac [<PATH>] Also persist the intermediate WAC source (default: ./output.wac). Useful for auditing.
--plan Skip in-process compose; persist WAC + splits and print the wac compose ... command.
--splits-dir <DIR> (splice only) Persist split sub-components on disk instead of in a tempdir.
--package <NAME> Package name written to the generated WAC.
--skip-type-check (splice only) Demote contract type-check errors to warnings.

Library usage

The same pipeline is available as a Rust library:

let bundle = splicer::splice(splicer::SpliceRequest { /* ... */ })?;
let composed: Vec<u8> = bundle.to_wasm()?;

See examples/wac_compose.rs for a runnable end-to-end demo.


Configuration Format

Splicing behavior is defined in a YAML configuration file.

See full specification:

docs/splice-config.md

Example Configuration

version: 1

rules:
  - before:
      interface: wasi:http/handler
      provider:
        name: auth
    inject:
        - middleware-a
        - middleware-b

  - between:
      interface: wasi:http/handler
      inner:
        name: auth
      outer:
        name: handler
    inject:
        - tracing

Splice Semantics

splicer operates on interface edges in the graph.

If no matches are found, the generated wac will produce an identity component (roundtrips to same component).

Two matching modes are supported:

1. Single-Target Injection

Inject middleware for a given interface, optionally scoped to a specific provider.

before:
  interface: wasi:http/handler
  provider:
    name: auth

If provider.name is omitted, all providers of that interface are matched.


2. Between Injection

Inject middleware between two specific components connected via an interface edge.

between:
   interface: wasi:http/handler
   inner:
     name: auth
   outer:
     name: handler

This replaces:

handler → auth

With:

handler → middleware → auth

Middleware chains are traversed in reverse order during injection to preserve declared ordering.


Rule Ordering

Rules are applied in file order.

Later rules operate on the graph after earlier modifications.

This allows intentional stacking:

auth → logging → metrics → handler

Validation

The configuration will fail if:

  • version is missing or unsupported
  • Required fields are absent
  • Middleware list is empty

Testing

In-process unit tests live under src/ and exercise the adapter generator, WAC emitter, and composition planner directly:

cargo test --lib

End-to-end fuzz + run harness

tests/fuzz_and_run.rs scaffolds provider, consumer, and middleware crates in a tempdir, drives them through the full splicer pipeline (compose + splice for both between and before rules), and invokes the result under wasmtime to check the composition actually executes.

Two entry points, both #[ignore]'d (they build real crates — slow):

  • test_canned — a hardcoded catalog of 22 value-type shapes * 2 async modes * 2 split-kind pipelines = 88 combos. Same shapes every run. Quick-to-bisect canary for regressions in a known shape.

  • test_fuzzarbitrary-driven random shapes. Reproducible via SPLICER_FUZZ_SEED so any failure can be replayed. Each iter prints [i/N] progress (requires --nocapture).

# Canned catalog — 88 combos, ~2 min
cargo test --test fuzz_and_run -- --ignored --nocapture test_canned

# Fuzz at PR CI config (25 iters × 2 modes × depth 5, ~2 min)
SPLICER_FUZZ_ITERS=25 SPLICER_FUZZ_DEPTH=5 \
  cargo test --test fuzz_and_run -- --ignored --nocapture test_fuzz

# Replay a single failing iteration
SPLICER_FUZZ_SEED=<seed_from_output> SPLICER_FUZZ_ITERS=1 \
  cargo test --test fuzz_and_run -- --ignored --nocapture test_fuzz

Env knobs:

var default effect
SPLICER_FUZZ_SEED 0xDEADBEEF base RNG seed; each iter's shape uses seed + iter_idx
SPLICER_FUZZ_ITERS 30 iterations per async mode (sync + async both run)
SPLICER_FUZZ_DEPTH 4 max recursion depth for compound shapes
SPLICER_KEEP_TMPDIR unset preserve the tempdir for post-mortem inspection

Project Structure

splicer/
├── src/
├── docs/
│   └── splice-config.md
├── README.md

Design Principles

  • Declarative configuration
  • Deterministic ordering
  • Interface-driven matching
  • Graph-aware edge replacement
  • Middleware-agnostic

splicer does not assume HTTP semantics — it operates on generic interface edges.


Future Evolution

The configuration format is versioned:

version: 1

Breaking changes will increment the version number.