Skip to content

JulienEllie/cel-comparison

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

cel-cxx vs rscel: Protobuf CEL Comparison

A head-to-head comparison of two Rust CEL (Common Expression Language) implementations, focused exclusively on protobuf message input/output — the primary use case for CEL in production systems.

cel-cxx rscel
Implementation Rust bindings to cel-cpp via cxx FFI Pure Rust CEL interpreter with bytecode VM
Protobuf input Serialized bytes (&[u8]) Box<dyn MessageDyn>
Protobuf output Extract bytes via Value::as_protobuf_bytes() Not supported
Type checking Full static type checking against descriptor pool None (dynamic only)

Capability Matrix

28 protobuf-specific tests covering scalars, nested messages, repeated fields, maps, enums, well-known types, message construction, and output extraction.

cel-cxx: 28/28 pass. rscel: 12/28 pass.

# Feature CEL Expression cel-cxx rscel
1 Scalar string field msg.name PASS PASS
2 Scalar int field msg.id PASS PASS
3 Bool field access person.active PASS PASS
4 Nested message access person.address.city PASS PASS
5 Deep nested access order.buyer.address.zip PASS PASS
6 Enum as int person.status == 1 PASS PASS
7 Enum named constant person.status == test.Status.STATUS_ACTIVE PASS FAIL
8 Repeated field size person.tags.size() PASS FAIL (panic)
9 Repeated field index person.tags[0] PASS FAIL (panic)
10 Repeated field exists person.tags.exists(t, t == 'cel') PASS FAIL (panic)
11 Repeated message index order.items[0].name PASS FAIL (panic)
12 Repeated message filter order.items.filter(i, i.price > 1000).size() PASS FAIL (panic)
13 Repeated message all order.items.all(i, i.price > 0) PASS FAIL (panic)
14 Map field access order.labels['priority'] PASS FAIL (panic)
15 Map field in 'priority' in order.labels PASS FAIL (panic)
16 Map field size order.labels.size() PASS FAIL (panic)
17 WKT Timestamp event.created_at > timestamp('2023-...') PASS FAIL
18 WKT Duration event.ttl == duration('3600s') PASS FAIL
19 WKT Wrapper unbox event.optional_count == 42 PASS FAIL
20 WKT Struct dot access event.metadata.env PASS FAIL
21 has() on present field has(msg.name) PASS PASS
22 Boolean expression msg.id > 10 && msg.name == 'Alice' PASS PASS
23 Arithmetic msg.id * 2 + 1 PASS PASS
24 String concat 'Hello, ' + msg.name PASS PASS
25 Message construction test.SimpleMessage{name: 'Bob', id: 25} PASS FAIL
26 Protobuf output bytes Extract result as proto bytes PASS FAIL
27 Complex comprehension order.items.filter(i, i.price > 1000).size() > 0 PASS FAIL (panic)
28 Ternary on proto field msg.id > 40 ? 'big' : 'small' PASS PASS

rscel fails on repeated fields and maps because its protobuf integration calls get_singular_field_or_default() which panics on non-singular fields. It also lacks support for well-known types, named enum constants, message construction, and protobuf output extraction.

Conformance Tests

16 tests where both engines must produce identical results. All pass, with one documented semantic difference:

  • .size() returns Int in cel-cxx and UInt in rscel

Benchmark Results

All benchmarks model production usage patterns: environments and compiled programs are cached, only bind + evaluate is measured on the hot path.

Run on Apple Silicon. Results are representative — exact numbers will vary by machine.

Hot Path (cached env + program, eval only)

Benchmark cel-cxx rscel Speedup
Simple field (msg.name) 1.63 us 8.42 us 5.2x
Boolean expr (id > 10 && name == 'Alice') 1.98 us 8.99 us 4.5x
Nested field (person.address.city) 2.84 us 9.00 us 3.2x
Complex expr (arithmetic + string + ternary) 2.43 us 9.66 us 4.0x

Protobuf Round Trip (cel-cxx only)

Operation Time
eval + extract proto bytes 1.83 us
eval bool only 1.55 us

rscel cannot extract protobuf bytes from evaluation results.

Comprehensions on Repeated Fields (cel-cxx only)

Operation Time
exists() 5.73 us
filter().size() 6.16 us
all() 5.72 us
map().size() 6.25 us

rscel panics on repeated fields — these operations are exclusively available in cel-cxx.

Throughput (10k requests, same cached program)

Engine Time Requests/sec
cel-cxx 15.6 ms ~641k/s
rscel 85.2 ms ~117k/s

cel-cxx is 5.5x faster at sustained throughput.

Pipeline (5 cached policy rules per request)

Engine Time
cel-cxx 8.3 us
rscel 11.1 us

Cache Miss (first compile + eval)

Engine Time
cel-cxx 32.4 us
rscel 23.8 us

rscel is 1.4x faster on cold start due to lighter compilation (no type checking, no FFI overhead).

Amortization (compile once, eval N times)

N evals cel-cxx rscel Winner
1 31.9 us 20.0 us rscel 1.6x
10 46.7 us 97.2 us cel-cxx 2.1x
100 225.9 us 907.6 us cel-cxx 4.0x
1000 2.02 ms 8.98 ms cel-cxx 4.4x

The crossover point is ~3 evaluations — after that, cel-cxx's faster eval dominates the total cost.

Why cel-cxx is Faster at Evaluation

cel-cxx wraps Google's cel-cpp, which uses an optimized C++ evaluation engine with efficient protobuf field access via the descriptor/reflection API. Protobuf messages stay as serialized bytes and are deserialized directly by the C++ runtime.

rscel is a pure Rust bytecode VM. Each evaluation requires constructing a Box<dyn MessageDyn> and binding it through the protobuf reflection layer, plus interpreting bytecode rather than running compiled native code.

The compilation speed difference goes the other way: rscel's compiler is a lightweight Rust parser that emits bytecodes, while cel-cxx performs full type checking against the protobuf descriptor pool and crosses the FFI boundary.

Why This Matters for Production

In a real deployment (policy engine, API gateway, event processing):

  1. Expressions are compiled once and cached — compilation cost is amortized to near zero
  2. The hot path is bind + eval, executed per request — this is where cel-cxx's 4-5x speedup compounds
  3. Protobuf I/O is the standard format — cel-cxx handles the full protobuf feature set (repeated, maps, WKTs, output extraction) while rscel only handles singular scalar fields

Prerequisites

  • Rust stable (1.80+)
  • CMake and a C++17 compiler (cel-cxx builds cel-cpp from source)

cel-cxx is pulled from the feat/native-protobuf-support branch. rscel uses version 1.0.4 from crates.io (the latest version compatible with stable Rust).

Running

# Run capability tests (feature matrix)
cargo test --test capability -- --nocapture

# Run conformance tests (both engines must agree)
cargo test --test conformance

# Run benchmarks
cargo bench

Note: the first build will take several minutes as cel-cxx compiles cel-cpp from source via CMake.

Project Structure

cel-comparison/
├── proto/test.proto           # Shared protobuf schema
├── build.rs                   # Compile proto for both prost and protobuf-rs
├── src/
│   ├── lib.rs                 # CmpValue type, encoding helpers, shared modules
│   ├── cel_cxx_runner.rs      # cel-cxx wrapper (bytes-based API)
│   └── rscel_runner.rs        # rscel wrapper (MessageDyn-based API)
├── tests/
│   ├── capability.rs          # 28 feature matrix tests
│   └── conformance.rs         # 16 same-result tests
└── benches/
    └── protobuf.rs            # criterion benchmarks (7 groups)

About

Head-to-head comparison of cel-cxx vs rscel for protobuf CEL evaluation in Rust

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages