- CEL-CXX
CEL-CXX
A type-safe Rust library for Common Expression Language (CEL), built on top of cel-cpp with zero-cost FFI bindings via cxx.
Documentation
- English Documentation - Complete documentation and guides
- 中文文档 - 中文文档和指南
For detailed guides on architecture, function registration, type system, and advanced features, see the documentation directory.
Quick Start
Installation
Add to your Cargo.toml:
[]
= "0.2.4"
# Optional features
= { = "0.2.4", = ["tokio"] }
# Protobuf derive macros (choose your backend)
= { = "0.2.4", = ["prost"] }
= { = "0.2.4", = ["protobuf-legacy"] }
Basic Expression Evaluation
use *;
// 1. Build environment with variables and functions
let env = builder
.?
.?
.register_global_function?
.build?;
// 2. Compile expression
let program = env.compile?;
// 3. Create activation with variable bindings
let activation = new
.bind_variable?
.bind_variable?;
// 4. Evaluate
let result = program.evaluate?;
println!; // "Hello Alice! You are an adult"
# Ok::
Custom Types with Derive Macros
use *;
// Specify type name in CEL type system.
// Generates `std::fmt::Display` impl for User` with `Debug` trait.
// or you can specify a custom format.
// Generates `std::fmt::Display` impl with custom format.
let env = builder
.?
// ✨ Register struct methods directly - &self becomes CEL receiver
.register_member_function?
.register_member_function?
.register_member_function?
.build?;
let program = env.compile?;
# Ok::
Platform Support
Legend:
- ✅ Tested: Confirmed working with automated tests
- 🟡 Should work: Build configuration exists but not tested in CI
Cross-Compilation Support
cel-cxx includes built-in support for cross-compilation via cross-rs. The build system automatically detects cross-compilation environments and configures the appropriate toolchains.
Usage with cross-rs:
# Install cross-rs
# Build for aarch64
Note: Not all cross-rs targets are supported due to CEL-CPP's build requirements. musl targets and some embedded targets may not work due to missing C++ standard library support or incompatible toolchains.
Android Build Instructions
Android builds require additional setup beyond the standard Rust toolchain:
Prerequisites:
- Install Android NDK and set
ANDROID_NDK_HOME - Install
cargo-ndkfor simplified Android builds
# Install cargo-ndk
# Add Android targets
Building for Android:
# Build for ARM64 (recommended)
# Build for ARMv7
# Build for x86_64 (emulator)
# Build for i686 (emulator)
Why cargo-ndk is required:
ANDROID_NDK_HOMEconfigures Bazel for CEL-CPP compilationcargo-ndkautomatically sets upCC_{target}andAR_{target}environment variables needed for the Rust FFI layer- This ensures both the C++ (CEL-CPP) and Rust (cel-cxx-ffi) components use compatible toolchains
CEL Feature Support
Core Language 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 |
| Variables | ✅ | Variable binding and scoping |
| Conditionals | ✅ | Ternary operator and logical short-circuiting |
| Comprehensions | ✅ | List and map comprehensions with filtering |
| Custom Types | ✅ | Opaque types via #[derive(Opaque)] |
| Protobuf Message Type | ✅ | Native protobuf messages and enums as first-class CEL types with field access, message construction, and round-trip serialization |
| Macros | ✅ | CEL macro expansion support |
| Function Overloads | ✅ | Multiple function signatures with automatic resolution |
| Type Checking | ✅ | Compile-time type validation |
Standard Library
| Feature | Status | Description |
|---|---|---|
| Built-in Functions | ✅ | Core CEL functions: size(), type(), has(), etc. |
| String Functions | ✅ | contains(), startsWith(), endsWith(), matches() |
| List Functions | ✅ | all(), exists(), exists_one(), filter(), map() |
| Map Functions | ✅ | Key/value iteration and manipulation |
| Type Conversion | ✅ | int(), double(), string(), bytes(), duration(), timestamp() |
| Math Functions | ✅ | Basic arithmetic and comparison operations |
Optional Value Support
| Feature | Status | Description |
|---|---|---|
| Optional Types | ✅ | optional<T> with safe navigation and null handling |
| Safe Navigation | ✅ | ?. operator for safe member access |
| Optional Chaining | ✅ | Chain optional operations without explicit null checks |
| Value Extraction | ✅ | value() and hasValue() functions for optional handling |
| Optional Macros | ✅ | optional.of(), optional.ofNonZeroValue() macros |
Extension Libraries
| Extension | Status | Description |
|---|---|---|
| Strings Extension | ✅ | Advanced string operations: split(), join(), replace(), format() |
| Math Extension | ✅ | Mathematical functions: math.greatest(), math.least(), math.abs(), math.sqrt(), bitwise ops |
| Lists Extension | ✅ | Enhanced list operations: flatten(), reverse(), slice(), unique() |
| Sets Extension | ✅ | Set operations: sets.contains(), sets.equivalent(), sets.intersects() |
| Regex Extension | ✅ | Regular expression support: matches(), findAll(), split() |
| Encoders Extension | ✅ | Encoding/decoding: base64.encode(), base64.decode(), URL encoding |
| Bindings Extension | ✅ | Variable binding and scoping enhancements |
Runtime Features
| Feature | Status | Description |
|---|---|---|
| Custom Functions | ✅ | Register custom Rust functions with automatic type conversion |
| Async Support | ✅ | Async function calls and evaluation with Tokio integration |
| Custom Extensions | ✅ | Build and register custom CEL extensions |
| Performance Optimization | ✅ | Optimized evaluation with caching and short-circuiting |
Protobuf Integration
cel-cxx supports native Protocol Buffer messages as first-class CEL types. You can bind serialized protobuf messages as variables, access their fields in CEL expressions, construct new messages, and extract results back to Rust.
Compiling Proto Descriptors
CEL needs a FileDescriptorSet (a binary descriptor file) to understand your .proto types.
The recommended approach is protox, a pure-Rust protobuf
compiler that requires no external binary:
use Message;
let fds = compile.unwrap;
let descriptor_bytes = fds.encode_to_vec;
Alternatively, you can use protoc directly:
Setting Up the Environment
use *;
# let descriptor_bytes: = vec!;
let env = builder
.with_file_descriptor_set
.declare_variable_with_type?
.build?;
# Ok::
Binding Protobuf Input
Serialize your message to bytes (e.g., via prost) and bind it:
# use *;
# let serialized_bytes: = vec!;
let activation = new
.bind_variable_dynamic?;
# Ok::
Accessing Fields in CEL
CEL expressions can access all protobuf field types:
msg.name // scalar fields
msg.address.city // nested message fields
msg.tags[0] // repeated fields
msg.labels["env"] // map fields
msg.status == my.package.Status.ACTIVE // enum constants
has(msg.optional_field) // field presence
Message Construction
Construct new protobuf messages directly in CEL:
my.package.MyMessage{name: "Alice", id: 42}
Extracting Results
# use *;
#
Typed API with Derive Macros
If you have compile-time protobuf types (via prost-build or protobuf-codegen), derive
macros let you skip the manual StructValue::from_bytes plumbing and use the standard
typed API instead:
// With the `prost` or `protobuf-legacy` feature enabled:
let env = builder
.with_file_descriptor_set
.? // instead of declare_variable_with_type(...)
.build?;
let activation = new
.bind_variable?; // instead of bind_variable_dynamic(...)
let result = program.evaluate?;
let recovered = from_value?; // instead of StructValue::from_value(...)
The derives are injected during code generation in your build.rs -- one line per type
for prost, or a small CustomizeCallback for protobuf-codegen.
See the Protobuf Derive Macros Guide for full setup instructions, feature flags, and examples.
Well-Known Type Handling
cel-cpp automatically converts well-known types to their CEL equivalents:
| Protobuf Type | CEL Type | Notes |
|---|---|---|
google.protobuf.Duration |
duration |
Full arithmetic and comparison support |
google.protobuf.Timestamp |
timestamp |
Full arithmetic and comparison support |
google.protobuf.Int64Value, StringValue, BoolValue, ... |
Primitives | Auto-unboxed to int, string, bool, etc. |
google.protobuf.Struct |
map |
Dynamic map with dot-notation access |
google.protobuf.Any |
— | has() works; full unpacking depends on cel-cpp support |
Performance Note
Protobuf messages cross the Rust/C++ FFI boundary via serialization: messages are serialized to bytes on the Rust side and deserialized into arena-allocated C++ messages for evaluation, then serialized back when extracting results. This adds overhead proportional to message size.
Zero-copy is not currently implemented for two reasons. First, the architectural change to keep C++ arena-allocated messages alive across the FFI boundary would be significant. Second, true zero-copy would require both cel-cxx and the Rust protobuf library to link against the exact same C++ protobuf library instance so that message pointers can be passed directly across the boundary. The official Google Rust protobuf crate (protobuf) supports a C++ kernel backend that would make this possible in theory, but it currently requires Bazel (not cargo), and there is no shared protobuf-cpp-sys crate that both libraries could depend on. cel-cxx also compiles its own copy of C++ protobuf as part of cel-cpp via cmake, so integrating a shared dependency would require reworking the build. Until the ecosystem converges, serialization/deserialization is the only correct approach.
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