Running a CEL program compiled to Wasm
There are two ways to run a CEL program compiled to Wasm:
ferricel runCLI — simple command-line execution; does not support host extensions.ferricel-coreRust crate — more flexible; supports host extensions and custom configurations.
Using the ferricel CLI
Here’s a simple CEL program. Save it to a file named validate-balance.cel:
account.balance >= transaction.withdrawal
|| (account.overdraftProtection
&& account.overdraftLimit >= transaction.withdrawal - account.balance)
Compile it to a Wasm module:
ferricel build --expression-file validate-balance.cel -o validate-balance.wasm
Create a bindings file named validate-balance.bindings.json with the input data:
{
"account": {
"balance": 500,
"overdraftProtection": true,
"overdraftLimit": 1000
},
"transaction": {
"withdrawal": 700
}
}
Run the compiled Wasm module with the bindings:
ferricel run \
--bindings-file validate-balance.bindings.json \
validate-balance.wasm
Output:
true
Alternatively, pass bindings as an inline JSON string:
ferricel run \
--bindings-json '{"account":{"balance":500,"overdraftProtection":true,"overdraftLimit":0},"transaction":{"withdrawal":700}}' \
validate-balance.wasm
Output:
false
Using the ferricel-core Rust crate
The ferricel-core crate provides programmatic control over compilation and
evaluation. This is useful when you need to evaluate many expressions, integrate
CEL into a larger system, or use host extensions.
Basic evaluation
Compile a CEL expression and evaluate it with bindings:
#![allow(unused)]
fn main() {
use ferricel_core::{compiler, runtime};
let wasm = compiler::Builder::new()
.build()
.compile("x * 2 + 1")?;
let result = runtime::Builder::new()
.with_wasm(wasm)
.build()?
.eval(Some(r#"{"x": 10}"#))?;
assert_eq!(result, "21");
}
The eval method accepts JSON-encoded variable bindings and returns a
JSON-encoded result string.
Host extensions
Register host-provided functions that the CEL expression can call. Extensions are declared at compile time and implemented at runtime:
#![allow(unused)]
fn main() {
use ferricel_core::{compiler, runtime};
use ferricel_types::extensions::ExtensionDecl;
let abs_decl = ExtensionDecl {
namespace: None,
function: "abs".to_string(),
receiver_style: false,
global_style: true,
num_args: 1,
};
let wasm = compiler::Builder::new()
.with_extension(abs_decl.clone())
.build()
.compile("abs(x)")?;
let result = runtime::Builder::new()
.with_extension(abs_decl, |args| {
let n = args[0].as_i64().unwrap_or(0);
Ok(serde_json::Value::Number(n.abs().into()))
})
.with_wasm(wasm)
.build()?
.eval(Some(r#"{"x": -42}"#))?;
assert_eq!(result, "42");
}
For full documentation on flat extensions, dotted namespaces, and builder chains, see the Host Extensions chapter.
Logging
Configure logging during evaluation with a custom logger and log level:
#![allow(unused)]
fn main() {
use ferricel_core::runtime;
use ferricel_types::LogLevel;
use slog::{Drain, Logger, o};
let decorator = slog_term::PlainSyncDecorator::new(std::io::stderr());
let drain = slog_term::FullFormat::new(decorator).build().fuse();
let logger = Logger::root(drain, o!());
let result = runtime::Builder::new()
.with_logger(logger)
.with_log_level(LogLevel::Debug)
.with_wasm(wasm)
.build()?
.eval(Some(r#"{"x": 10}"#))?;
}
Custom Wasmtime configuration
For advanced use cases, provide your own wasmtime::Engine with custom settings
(e.g. fuel metering, epoch interruption):
#![allow(unused)]
fn main() {
use ferricel_core::{compiler, runtime};
use wasmtime::{Config, Engine as WasmEngine};
let mut config = Config::new();
config.consume_fuel(true);
let wasm_engine = WasmEngine::new(&config)?;
let wasm = compiler::Builder::new().build().compile("1 + 1")?;
let result = runtime::Builder::new()
.with_engine(wasm_engine)
.with_wasm(wasm)
.build()?
.eval(None)?;
}
Protobuf bindings
For type-safe variable bindings that preserve full fidelity (bytes, uint,
timestamp, duration, etc.), use eval_proto with protobuf-encoded bindings:
let result = engine.eval_proto(&bindings_proto_bytes)?;
This avoids the JSON round-trip and preserves exact type information.
Memory management and performance
Important
CEL programs compiled to Wasm use a leaking allocator: memory is never deallocated during evaluation. This is not an issue in practice because CEL expressions are short-lived.
However, the consequence is that each evaluation starts with a fresh Wasm instance. This ensures no memory carries over between calls and prevents unbounded memory growth.
To optimize performance despite this, the Builder uses wasmtime::InstancePre
to pre-link the module at build time. This amortizes compilation cost:
Builder::build()parses the Wasm bytes once and pre-links all host functions.- Each
eval()call only pays the cost of instantiation.
For workloads with many evaluations, creating a single Engine and reusing it
across multiple eval() calls is the recommended pattern.