25 releases
| 0.2.41 | Nov 28, 2025 |
|---|---|
| 0.2.40 | Nov 27, 2025 |
| 0.2.39 | Oct 27, 2025 |
| 0.2.23 | Sep 30, 2025 |
| 0.1.0 | Sep 26, 2025 |
#144 in WebAssembly
21 downloads per month
Used in 4 crates
(3 directly)
55KB
1.5K
SLoC
cardinal-wasm-plugins
cardinal-wasm-plugins is the host runtime that executes WebAssembly middleware inside Cardinal. It is responsible for loading modules, wiring the import surface, and running the guest code in either inbound or outbound mode.
Execution model
CardinalProxy
│
├─ inbound middleware → WasmRunner (ExecutionType::Inbound)
│ • read-only access to headers/query/body
│ • returns `should_continue` (0/1)
│
└─ outbound middleware → WasmRunner (ExecutionType::Outbound)
• can mutate response headers/status
• observes the request context as well
The canonical entry point exported by a WASM module is handle(ptr: i32, len: i32) -> i32. The return value maps to should_continue (1) or responded (0). __new (AssemblyScript) or a compatible allocator must also be present so the host can write the request body into guest memory when needed.
Core types
WasmPlugin: loads bytes from disk (or memory), validates required exports, and remembers the configured memory/handle symbols.WasmInstance: wraps the instantiated module, guest memory, andFunctionEnv<ExecutionContext>so host imports can mutate state.ExecutionContext: enum withInboundandOutboundvariants. Inbound mode surfacesExecutionRequest(headers, query string, optional body). Outbound mode extends that withExecutionResponse(mutableresp_headers,status).WasmRunner: orchestrates a run—copying the current context into the guest, invokinghandle, and harvesting results.
Host imports
| Import | Mode | Description |
|---|---|---|
get_header(name, out_ptr, out_cap) |
inbound + outbound | copy a header value into guest memory; returns byte count or -1 |
get_query_param(key, out_ptr, out_cap) |
inbound + outbound | similar to get_header, but for query parameters |
set_header(name, value) |
outbound only | stage a response header to be written back to Pingora |
set_status(code) |
outbound only | override the HTTP status sent to the client |
abort(code, msg_ptr, msg_len) |
both | abort execution; surfaces as CardinalError::InternalError(InvalidWasmModule) |
Inbound code is intentionally read-only: it can veto a request by returning 0, but it cannot mutate headers/state on the way to the upstream backend.
Fixture-driven tests
The crate’s unit tests load fixtures from tests/wasm-plugins/<case>:
📁 tests/wasm-plugins/
├─ allow/
│ ├─ plugin.ts (AssemblyScript source)
│ ├─ plugin.wasm (compiled)
│ ├─ incoming_request.json
│ └─ expected_response.json
├─ inbound-allow/
├─ inbound-block/
└─ outbound-tag/
incoming_request.json feeds ExecutionContext, while expected_response.json specifies:
{
"execution_type": "outbound", // or "inbound"
"should_continue": true,
"status": 200,
"resp_headers": {
"x-example": "value"
}
}
For inbound tests, status/resp_headers must be omitted; the runner enforces this to keep fixtures honest.
authoring WASM middleware
- Write AssemblyScript (or any language that compiles to WASM) using the imports above. AssemblyScript examples live alongside the fixtures.
- Compile with
tests/wasm-plugins/compile.shor equivalent tooling (npx asc plugin.ts -o plugin.wasm --optimize --exportRuntime). - Reference the
.wasmfile incardinal-config:
[[plugins]]
wasm = { name = "audit", path = "filters/audit/plugin.wasm" }
During runtime, PluginContainer loads the module, and PluginRunner invokes it in the appropriate phase.
Error handling
WasmRunner::run returns CardinalError variants when:
- required exports are missing (
InvalidWasmModule) - the guest traps or calls
abort - memory writes fail
Callers should treat these errors as fatal—Cardinal responds with 500 and logs the failure.
Extending the runtime
- New host imports can be added under
src/host/(mirroring the existing modules). Updatemake_importsso the appropriate functions are only exposed in the modes that make sense. - To support alternative languages/runtimes, ensure they can export the same C ABI (
handle, allocator). The current implementation assumes linear memory via Wasmer 6.
Dependencies
~7–21MB
~270K SLoC