Expand description
Β§cel-cxx: Modern Rust Interface for CEL
A high-performance, type-safe Rust interface for Common Expression Language (CEL), built on top of google/cel-cpp with zero-cost FFI bindings via cxx.
Β§ποΈ Architecture Overview
Β§Core Design Principles
- Type Safety: Compile-time verification of CEL expressions and function signatures
- Zero-Cost Abstractions: Direct FFI calls to CEL-CPP with minimal overhead
- Memory Safety: Rust ownership system prevents common integration bugs
- Ergonomic API: Builder patterns and automatic type inference reduce boilerplate
- Extensibility: Support for custom types and async operations
Β§Integration Architecture
The library provides a layered architecture that bridges Rust and CEL-CPP:
- Application Layer: High-level APIs for environment building and expression evaluation
- Type System Layer: Automatic conversions between Rust and CEL types
- FFI Layer: Zero-cost bindings to CEL-CPP via the
cxx
crate - CEL-CPP Layer: Googleβs reference implementation for parsing and evaluation
Β§π Quick Start
Β§Installation
Add to your Cargo.toml
:
[dependencies]
cel-cxx = "0.1.0"
# Optional features
cel-cxx = { version = "0.1.0", features = ["async", "derive", "tokio"] }
Β§Basic Expression Evaluation
use cel_cxx::*;
// 1. Build environment with variables and functions
let env = Env::builder()
.declare_variable::<String>("name")?
.declare_variable::<i64>("age")?
.register_global_function("adult", |age: i64| age >= 18)?
.build()?;
// 2. Compile expression
let program = env.compile("'Hello ' + name + '! You are ' + (adult(age) ? 'an adult' : 'a minor')")?;
// 3. Create activation with variable bindings
let activation = Activation::new()
.bind_variable("name", "Alice")?
.bind_variable("age", 25i64)?;
// 4. Evaluate
let result = program.evaluate(&activation)?;
println!("{}", result); // "Hello Alice! You are an adult"
Β§Custom Types with Derive Macros
use cel_cxx::*;
#[derive(Opaque, Debug, Clone, PartialEq)]
#[cel_cxx(type = "myapp.User")]
struct User {
name: String,
age: i32,
roles: Vec<String>,
}
impl User {
// Struct methods can be registered directly as CEL member functions
fn has_role(&self, role: &str) -> bool {
self.roles.contains(&role.to_string())
}
fn is_adult(&self) -> bool {
self.age >= 18
}
fn get_role_count(&self) -> i64 {
self.roles.len() as i64
}
}
impl std::fmt::Display for User {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "User({})", self.name)
}
}
let env = Env::builder()
.declare_variable::<User>("user")?
// β¨ Register struct methods directly - &self becomes CEL receiver
.register_member_function("has_role", User::has_role)?
.register_member_function("is_adult", User::is_adult)?
.register_member_function("get_role_count", User::get_role_count)?
.build()?;
let program = env.compile("user.has_role('admin') && user.is_adult()")?;
Β§π― Zero-Annotation Function Registration
The libraryβs flagship feature uses Generic Associated Types (GATs) to automatically infer function signatures, eliminating the need for manual type annotations:
use cel_cxx::*;
let env = Env::builder()
// β¨ Function signatures automatically inferred from Rust types!
.register_global_function("add", |a: i64, b: i64| a + b)?
.register_global_function("concat", |a: String, b: &str| a + b)?
.register_global_function("length", |s: &str| s.len() as i64)?
.register_global_function("parse", |s: &str| s.parse::<i64>())? // Result<i64, _> auto-handled
.build()?;
This system supports a wide variety of function patterns:
Β§Owned Type Parameters
Functions can accept owned values, which are automatically converted from CEL types:
use cel_cxx::*;
let env = Env::builder()
// Basic owned types
.register_global_function("add", |a: i64, b: i64| a + b)?
.register_global_function("concat", |a: String, b: String| a + &b)?
.register_global_function("sum_list", |nums: Vec<i64>| nums.iter().sum::<i64>())?
// Complex owned types
.register_global_function("process_map", |data: std::collections::HashMap<String, i64>| {
data.values().sum::<i64>()
})?
.register_global_function("handle_optional", |maybe_val: Option<String>| {
maybe_val.unwrap_or_else(|| "default".to_string())
})?
.build()?;
Β§Reference Type Parameters
Reference parameters enable zero-copy operations for performance-critical code:
use cel_cxx::*;
let env = Env::builder()
// String references - no copying required
.register_global_function("length", |s: &str| s.len() as i64)?
.register_global_function("starts_with", |text: &str, prefix: &str| text.starts_with(prefix))?
// Collection element references - containers hold owned values
.register_global_function("first", |items: Vec<i64>| items.first().copied().unwrap_or(0))?
.register_global_function("contains", |haystack: Vec<&str>, needle: &str| {
haystack.iter().any(|&s| s == needle)
})?
.build()?;
Β§Reference Type Return Values
Functions can return references to data within their parameters, enabling efficient data access:
use cel_cxx::*;
// Define functions that return references with proper lifetime annotations
fn get_domain(email: &str) -> &str {
email.split('@').nth(1).unwrap_or("")
}
fn get_substring(text: &str, start: i64) -> &str {
let start = start as usize;
if start < text.len() { &text[start..] } else { "" }
}
let env = Env::builder()
// Return string slices from borrowed parameters using named functions
.register_global_function("get_domain", get_domain)?
// Return owned values from owned containers using closures
.register_global_function("get_first", |items: Vec<String>| {
items.into_iter().next().unwrap_or_default()
})?
// Return references to parameter data using named functions
.register_global_function("get_substring", get_substring)?
.build()?;
Β§Direct Return Values vs Result Types
The system supports both direct return values and Result<T, E>
for error handling:
use cel_cxx::*;
use std::num::ParseIntError;
use std::io;
let env = Env::builder()
// Direct return values - always succeed
.register_global_function("double", |x: i64| x * 2)?
.register_global_function("format_name", |first: &str, last: &str| {
format!("{}, {}", last, first)
})?
// Result return values - can fail gracefully with standard library errors
.register_global_function("parse_int", |s: &str| -> Result<i64, ParseIntError> {
s.parse()
})?
.register_global_function("divide", |a: f64, b: f64| -> Result<f64, io::Error> {
if b == 0.0 {
Err(io::Error::new(io::ErrorKind::InvalidInput, "Division by zero"))
} else {
Ok(a / b)
}
})?
// Result with owned return values and concrete error types
.register_global_function("safe_index", |items: Vec<String>, idx: i64| -> Result<String, io::Error> {
let index = idx as usize;
items.get(index)
.cloned()
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "Index out of bounds"))
})?
.build()?;
Β§Synchronous vs Asynchronous Functions
Both sync and async functions are supported seamlessly:
use cel_cxx::*;
let env = Env::builder()
.use_tokio()
// Synchronous functions - execute immediately
.register_global_function("sync_add", |a: i64, b: i64| a + b)?
.register_global_function("sync_format", |name: &str| format!("Hello, {}", name))?
// Asynchronous functions - return futures
.register_global_function("async_fetch", async |id: i64| -> Result<String, std::io::Error> {
// Simulate async database call
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
Ok(format!("Data for ID: {}", id))
})?
.register_global_function("async_validate", async |email: &str| -> Result<bool, std::fmt::Error> {
// Simulate async validation service
tokio::time::sleep(std::time::Duration::from_millis(50)).await;
Ok(email.contains('@'))
})?
// Mixed sync and async in same environment
.register_global_function("process", |data: String| data.to_uppercase())?
.register_global_function("async_process", async |data: String| {
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
data.to_lowercase()
})?
.build()?;
// Async evaluation when any async functions are used
let program = env.compile("async_fetch(42) + ' - ' + async_validate('user@example.com')")?;
let result = program.evaluate(()).await?;
Β§Function Signature Examples
Hereβs a comprehensive overview of supported function signatures:
use cel_cxx::*;
// Define function that returns reference with proper lifetime annotation
fn substring_fn(s: &str, start: i64, len: i64) -> &str {
let start = start as usize;
let end = (start + len as usize).min(s.len());
&s[start..end]
}
// All of these function signatures are automatically inferred:
let env = Env::builder()
// No parameters
.register_global_function("now", || std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs() as i64)?
.register_global_function("pi", || std::f64::consts::PI)?
// Single parameter - various types
.register_global_function("abs", |x: i64| x.abs())?
.register_global_function("uppercase", |s: String| s.to_uppercase())?
.register_global_function("len", |s: &str| s.len() as i64)?
// Multiple parameters - mixed types (using named function for lifetime)
.register_global_function("substring", substring_fn)?
// Generic collections - owned containers
.register_global_function("join", |items: Vec<String>, sep: &str| items.join(sep))?
.register_global_function("filter_positive", |nums: Vec<i64>| {
nums.into_iter().filter(|&x| x > 0).collect::<Vec<_>>()
})?
// Optional types
.register_global_function("unwrap_or", |opt: Option<String>, default: String| {
opt.unwrap_or(default)
})?
// Result types for error handling with standard library errors
.register_global_function("safe_divide", |a: f64, b: f64| -> Result<f64, std::io::Error> {
if b == 0.0 {
Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, "Division by zero"))
} else {
Ok(a / b)
}
})?
.register_global_function("parse_float", |s: &str| -> Result<f64, std::num::ParseFloatError> {
s.parse()
})?
.build()?;
Β§π§ Advanced Features
Β§Async Support
When the async
feature is enabled, you can evaluate expressions asynchronously:
use cel_cxx::*;
let env = Env::builder()
.use_tokio()
.register_global_function("async_fetch", async |id: i64| -> Result<String, Error> {
// Simulate async database call
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
Ok(format!("Data for ID: {}", id))
})?
.build()?;
let program = env.compile("async_fetch(42)")?;
let result = program.evaluate(()).await?;
Β§Async Architecture Design
Supporting Rust async functions in CEL presents unique challenges since CEL-CPP doesnβt
natively support asynchronous or callback-based user-defined functions and variable providers.
When a Rust async function returns a Future
, it has already exited the current stack frame,
and the C++ CEL evaluation engine cannot schedule or await Rust futures.
cel-cxx solves this through an innovative dual-threading architecture:
-
Async-to-Blocking Bridge: When async functions or variable providers are registered, the entire program evaluation is moved to a blocking thread using
Runtime::spawn_blocking()
. The main async context receives a future that resolves when evaluation completes. -
Blocking-to-Async Bridge: When async callbacks are invoked within the blocking thread, the returned futures are dispatched back to the async runtime for execution, while the blocking thread waits for completion using
Runtime::block_on()
.
Β§Implementation Details
-
Lifetime Management: Since user-provided functions and variable providers can be capturing closures with complex lifetimes, cel-cxx uses the
async-scoped
crate to safely manage these lifetimes across thread boundaries. -
Multi-threaded Runtime Requirement: When using Tokio, the runtime must be multi-threaded because the implementation relies on
tokio::task::block_in_place()
, which panics in single-threaded runtimes.
This design enables seamless integration of async Rust code with the synchronous CEL-CPP evaluation engine, maintaining both performance and correctness across runtime boundaries.
Β§Function Overloads
The library supports function overloading with automatic type resolution:
use cel_cxx::*;
let env = Env::builder()
// Multiple functions with same name, different signatures
.register_global_function("process", |x: i64| x * 2)?
.register_global_function("process", |x: f64| x * 2.0)?
.register_global_function("process", |x: String| x.to_uppercase())?
.build()?;
// CEL will automatically choose the right overload based on argument types
let program1 = env.compile("process(42)")?; // Calls i64 version
let program2 = env.compile("process(3.14)")?; // Calls f64 version
let program3 = env.compile("process('hello')")?; // Calls String version
Β§Smart Reference Handling
The library automatically manages reference types with safe lifetime handling:
use cel_cxx::*;
use std::collections::HashMap;
// β
These reference patterns work automatically:
let env = Env::builder()
.declare_variable::<Vec<&str>>("string_refs")? // Borrowed strings
.declare_variable::<HashMap<i64, &str>>("lookup")? // Borrowed values
.declare_variable::<Option<&str>>("maybe_str")? // Optional borrows
.build()?;
// The library prevents unsafe patterns at compile time:
// β .declare_variable::<&Vec<String>>("invalid")? // Compiler error
Β§π Type System
The crate provides comprehensive type support with automatic conversions between CEL and Rust types. All types support the three core traits for seamless integration:
CEL Type | Rust Type | |||
---|---|---|---|---|
Declare | To CEL | From CEL | ||
TypedValue | IntoValue | FromValue | ||
null | () | β | β | |
bool | bool | β | β | |
int | i64 , i32 , i16 , isize | β | β | |
uint | u64 , u32 , u16 , usize | β | β | |
double | f64 , f32 | β | β | |
string | String , ArcStr , Box<str> , str | β | β | |
bytes | Vec<u8> , ArcBytes , Box<[u8]> , [u8] | β | β | |
duration | chrono::Duration | β | β | |
timestamp | chrono::DateTime<Utc> , SystemTime | β | β | |
list<T> | Vec<T> , VecDeque<T> , LinkedList<T> , [T] | β | β | |
map<K,V> | HashMap<K,V> , BTreeMap<K,V> , Vec<(K,V)> | β | β | |
optional<T> | Option<T> , Optional<T> | β | β | |
type | ValueType | β | β | |
error | Error | β | β | |
opaque | #[derive(Opaque)] struct | β | β |
Special Reference Support: All &T
types support Declare and To CEL operations,
enabling zero-copy function arguments like &str
, &[u8]
, &MyStruct
, etc.
Β§Type Conversion Examples
use cel_cxx::*;
use std::collections::VecDeque;
// Automatic conversions work seamlessly
let env = Env::builder()
// Different integer types all map to CEL int
.register_global_function("process_i32", |x: i32| x * 2)?
.register_global_function("process_i64", |x: i64| x * 2)?
// String types are interchangeable
.register_global_function("process_string", |s: String| s.to_uppercase())?
.register_global_function("process_str", |s: &str| s.len() as i64)?
// Container types work with any compatible Rust collection
.register_global_function("sum_vec", |nums: Vec<i64>| nums.iter().sum::<i64>())?
.register_global_function("sum_deque", |nums: VecDeque<i64>| nums.iter().sum::<i64>())?
.build()?;
Β§π οΈ Feature Flags
Feature | Description | Default |
---|---|---|
async | Async/await support for expressions and functions | β |
derive | Derive macros for custom types (#[derive(Opaque)] ) | β |
tokio | Tokio async runtime integration (requires async ) | β |
async-std | async-std runtime integration (requires async ) | β |
Β§π― Performance Characteristics
- Zero-cost FFI: Direct C++ function calls with no marshaling overhead
- Compile-time optimization: Function signatures resolved at compile time
- Memory efficient: Minimal allocations through smart reference handling
- Async overhead: Only when async features are explicitly used
- Type safety: Compile-time prevention of common integration errors
Β§π Examples
The crate includes comprehensive examples demonstrating various features:
- Basic Usage: Variable binding, function registration, expression evaluation
- Custom Types: Derive macros, member functions, type integration
- Async Support: Tokio and async-std integration examples
- Advanced Features: Function overloads, error handling, complex type conversions
Run examples with:
cargo run --example comprehensive
cargo run --example tokio --features="async,tokio"
Β§π₯οΈ Platform Support
Platform | Status | Notes |
---|---|---|
Linux | β Supported | Fully tested and supported |
macOS | β οΈ Untested | Should work but not regularly tested |
Windows | β Not Supported | CEL-CPP Bazel build scripts donβt support Windows |
Β§π CEL Feature Support
Β§β Supported Features
Feature | Status | Description |
---|---|---|
Basic Types | β | null , bool , int , uint , double , string , bytes |
Collections | β | list<T> , map<K,V> with full indexing and comprehensions |
Time Types | β | duration , timestamp with full arithmetic support |
Operators | β | Arithmetic, logical, comparison, and membership operators |
Functions | β | Built-in functions and custom function registration |
Variables | β | Variable binding and scoping |
Conditionals | β | Ternary operator and logical short-circuiting |
Comprehensions | β | List and map comprehensions with filtering |
Optional Types | β | optional<T> with safe navigation |
Custom Types | β | Opaque types via #[derive(Opaque)] |
Extensions | β | CEL language extensions and custom operators |
Macros | β | CEL macro expansion support |
Async Support | β | Async function calls and evaluation |
Function Overloads | β | Multiple function signatures with automatic resolution |
Type Checking | β | Compile-time type validation |
Β§π§ Planned Features
Feature | Status | Description |
---|---|---|
Protocol Buffer Integration | π§ Planned | Direct support for protobuf messages and enums as native CEL types |
Windows Support | π§ Planned | Requires CEL-CPP Windows build support |
Β§π Prerequisites
Β§System Requirements
- Rust: 1.70+ (for GATs support)
- C++ Toolchain: C++17 compatible compiler
- Linux: GCC 7+ or Clang 6+
- macOS: Xcode 10+ or Clang 6+
- Windows: MSVC 2019+ or Clang 6+
Β§Installation Verification
# Clone and test
git clone https://github.com/xjasonli/cel-cxx.git
cd cel-cxx
cargo test
# Run examples
cargo run --example comprehensive
cargo run --example tokio --features="async,tokio"
Β§π€ Contributing
We welcome contributions! Please see our Contributing Guide for details.
Β§Development Setup
# Setup development environment
git clone https://github.com/xjasonli/cel-cxx.git
cd cel-cxx
# Install dependencies
cargo build
# Run tests
cargo test --all-features
# Run examples
cargo run --example comprehensive
Β§Code Style
- Follow standard Rust formatting (
cargo fmt
) - Ensure all tests pass (
cargo test
) - Add documentation for public APIs
- Include examples for new features
Β§π Related Crates
This project is part of a growing ecosystem of CEL implementations in Rust. Hereβs how it compares to other notable projects:
Β§xjasonli/cel-cxx (This Project)
- Approach: FFI bindings to Googleβs official CEL-CPP implementation
- Performance: Zero-cost abstractions with direct C++ calls
- Type Safety: Compile-time verification via Rustβs type system
- Features: Full CEL spec support, async/await, custom types via derive macros
- Best For: Production applications requiring full CEL compatibility and maximum performance
Β§clarkmcc/cel-rust
- Approach: Native Rust interpreter with ANTLR-based parser
- Performance: Good performance for pure Rust, no C++ dependencies
- Features: Core CEL features, custom functions, concurrent execution
- Best For: Applications preferring pure Rust solutions or avoiding C++ dependencies
- Trade-offs: May not support all CEL spec features
Β§1BADragon/rscel
- Approach: Native Rust implementation with custom parser
- Features: Basic CEL evaluation, custom types and functions
- Best For: Lightweight applications with simple CEL expression needs
- Trade-offs: More limited feature set compared to other implementations
Β§thesayyn/cel-rs
- Approach: Native Rust with WebAssembly compilation target
- Features: Basic CEL features, WASM playground, browser compatibility
- Best For: Web applications and browser-based CEL evaluation
- Trade-offs: Focused on web use cases, may have limited server-side features
Β§Choosing the Right Implementation
Requirement | Recommendation |
---|---|
Full CEL spec compliance | cel-cxx (this project) |
Pure Rust, no C++ deps | clarkmcc/cel-rust |
WebAssembly/Browser support | thesayyn/cel-rs |
Lightweight, simple use cases | 1BADragon/rscel |
Maximum performance | cel-cxx (this project) |
Async/await support | cel-cxx (this project) |
Β§π License
Licensed under the Apache License 2.0. See LICENSE for details.
Β§π Acknowledgements
google/cel-cpp
- The foundational C++ CEL implementationdtolnay/cxx
- Safe and efficient Rust-C++ interoprmanoka/async-scoped
- Scoped async execution for safe lifetime management- The CEL community and other Rust CEL implementations for inspiration and ecosystem growth
Re-exportsΒ§
pub use async::AsyncStd;
async-std
pub use async::Tokio;
tokio
pub use async::Smol;
smol
pub use marker::*;
pub use env::*;
pub use program::*;
pub use error::*;
pub use function::*;
pub use variable::*;
pub use activation::*;
pub use kind::*;
pub use types::*;
pub use values::*;
pub use maybe_future::*;
ModulesΒ§
- activation
- Activation context for expression evaluation.
- async
async
- Asynchronous runtime support for CEL.
- env
- Environment for compiling CEL expressions.
- error
- Error types and error handling utilities.
- function
- Function registration and implementation utilities.
- kind
- CEL type system kinds and type classification.
- marker
- Marker types and traits for function and runtime polymorphism.
- maybe_
future - Conditional future types for async/sync compatibility.
- program
- Compiled CEL programs ready for evaluation. Compiled CEL program evaluation.
- types
- CEL type system types and type definitions.
- values
- CEL Value Types and Operations
- variable
- Variable declaration and binding utilities.
Derive MacrosΒ§
- Opaque
derive
- Derive macro for creating opaque types for cxx-cel.