splicer ๐Ÿ”โœ‚๏ธ๐Ÿชก

June 4, 2026 ยท View on GitHub

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

splicer reads:

  • A component binary as .wasm (the composition or a single component)
  • A splice configuration (YAML)

It produces a new composed .wasm (or the underlying plan pieces) that injects middleware components according to declarative rules.

splicer is interface-agnostic โ€” it operates on any WIT interface edge in a component graph. The same splice rules apply whether the targeted interface is wasi:http/handler, wasi:cli/run, my:app/orders, or a custom in-house contract.


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 declarative rules on:

  • which interface to target,
  • what middleware to wire in, and
  • where to invoke the middleware.

splicer either produces the newly composed .wasm directly, or (with --plan) emits the underlying plan pieces (generated wac + split sub-components) for inspection or manual composition.

splicer operates directly on the component binary (no source code, and no original wac even when the input is itself a composition). The graph is discovered from the binary alone.


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 my:service/adder, wasi:cli/run, or wasi:http/handler; 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

TierData accessCalls downstream?CapabilityContractStatus
Tier 1none (call-id only)yes (skippable)Hooks: middleware sees the call identity but not types or datawit/tier1/world.witSupported
Tier 2read-onlyyes (skippable)Observe: middleware sees the typed values flowing through; cannot modifywit/tier2/world.witSupported
Tier 3read + writeyesTransform: middleware sees AND modifies the values; downstream is still calledsplicer_tool_sdk::TransformStrategySupported
Tier 4read + writenoVirtualize: middleware replaces the downstream entirely (mocks, virts, replayers)splicer_tool_sdk::VirtualizeStrategySupported

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

"Skippable" (tier 1 / tier 2) vs "no" (tier 4) are different things. With gate::should-call the adapter still generates the downstream call and asks the middleware at runtime whether to invoke it. It's a per-call gate. With virtualization the downstream call is not in the adapter at all; it cannot be reached, regardless of runtime state.

Tier-1 and tier-2 middleware are components: your wasm exports one or more of the interfaces defined in the relevant tier WIT world (e.g. wit/tier1/world.wit). Tier-2 hooks receive arguments and results lifted into a structural field-tree (defined in wit/common/world.wit), so observation middleware can inspect typed values without depending on the target interface's concrete types.

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.

Tier-3 and tier-4 middleware are Rust strategy crates implementing TransformStrategy or VirtualizeStrategy from splicer-tool-sdk. Splicer codegens a per-target wrapper at splice-time; the wrapper is the adapter. See tier-3 / tier-4.

Tier-3/4 currently ships only via builtins. User-form tier-3/4 (point splicer at your own strategy crate) is planned.

For Rust authors, splicer-tool-sdk ships ready-made building blocks for middleware and downstream tools. Common operations on lifted typed values live in one place, so your middleware and any consuming tools (decoders, replay drivers, fixture sanitizers) get them for free instead of each crate re-implementing them.

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


Builtins

Splicer ships middleware as builtins, referenced by name in a splice config:

inject:
  - builtin: hello-tier1

Tier-1/2 builtins are pre-built wasm fetched on demand from ghcr.io/ejrgilbert/splicer/builtins/*. Tier-3/4 builtins are Rust strategy crates embedded in splicer's binary; the wrapper is codegen'd and compiled per-target at splice-time (requires cargo and wasm32-wasip1 on PATH).

NameTierDescription
hello-tier11println!s every wrapped call. Verifies splice rules fire.
hello-tier22println!s every wrapped call with lifted arg + result values.
hello-tier33Pass-through transform; println!s before/after each wrapped call.
hello-tier44Returns R::default() instead of forwarding. Requires R: Default.
otel-bare-spans1Emits a wasi:otel span per call (timing + call-id attrs, no payload).
otel-bare-metrics1Emits wasi:otel count + duration-histogram metrics (no payload).
otel-bare-logs1Emits a structured wasi:otel log per call (configurable severity, no payload).

See docs/splice-config.md for the full builtin: schema (short + long forms, the config: block, and the local-override โ†’ cache โ†’ OCI resolution order).

The CLI includes helpful information on builtins. Run: splicer builtin to view.


Build + Install

From crates.io:

cargo install splicer

Or as a library dependency:

[dependencies]
splicer = "2"

The library entry point is splicer::splice(SpliceRequest) -> Bundle; examples/wac_compose.rs is a runnable end-to-end demo.

From source (for development):

cargo build --release
# binary at target/release/splicer

Builtin source crates live under builtins/. You don't need to build them to use splicer, they're pulled from ghcr.io/ejrgilbert/splicer/builtins/* on demand. To rebuild local artifacts (for iterating on a builtin without re-publishing), run:

make build-builtins

Builds land in assets/builtins/; point SPLICER_BUILTINS_DIR at that directory to short-circuit the OCI pull.

To kick the tires, cargo run --example demo runs a self-contained demo; for a fuller walkthrough see the external component-interposition repo.


Configuration Format

Splicing behavior is defined in a YAML configuration file:

version: 1
rules:
  - before:
      interface: my:app/orders
      provider:
        name: validate
    inject:
      - builtin: hello-tier1

Rules can match on three independent axes (mix and match as needed):

AxisFieldWhat it gates
Interface nameinterface:The target interface's name (glob or list, required).
Node nameprovider: / inner: / outer:The instance's display name on the provider/inner/outer side (glob or list, optional).
Function shapeall-funcs:The matched interface's function shapes โ€” async, scope, arg/result properties (optional).

See docs/splice-config.md for the full specification.


Testing

Unit tests cover the adapter generator, WAC emitter, and composition planner: cargo test --lib.

End-to-end coverage lives in tests/fuzz_and_run.rs. It scaffolds provider/consumer/middleware crates, drives them through the full splicer pipeline (compose + splice, before and between), and invokes the result under wasmtime. Two entry points (both #[ignore]'d โ€” they build real crates):

  • test_canned โ€” a hardcoded catalog of value-type shapes crossed with async modes and split-kind pipelines. Deterministic; the regression canary.
  • test_fuzz โ€” arbitrary-driven random shapes, reproducible via SPLICER_FUZZ_SEED (replay any failing iter by re-running with its seed and SPLICER_FUZZ_ITERS=1).
cargo test --test fuzz_and_run -- --ignored --nocapture test_canned
cargo test --test fuzz_and_run -- --ignored --nocapture test_fuzz

Env knobs:

vardefaulteffect
SPLICER_FUZZ_SEED0xDEADBEEFbase RNG seed; each iter's shape uses seed + iter_idx
SPLICER_FUZZ_ITERS30iterations per async mode (sync + async both run)
SPLICER_FUZZ_DEPTH4max recursion depth for compound shapes
SPLICER_KEEP_TMPDIRunsetpreserve the tempdir for post-mortem inspection