flagd-evaluator is a Rust-based feature flag evaluation engine designed to replace per-language JSON Logic implementations with a single core — one implementation to maintain, one test suite, consistent behavior across all languages. Thin wrapper libraries keep language-specific code minimal. It ships as:
- WASM module (~2.4MB) — for Java (Chicory), Go (wazero), JavaScript, .NET, and other WASM runtimes
- Native bindings — Python (PyO3), with more planned
- Rust library — direct API for Rust consumers
The best integration strategy is chosen per language based on benchmarks. For example, Python benchmarks showed PyO3 native bindings outperform WASM via wasmtime-py, while Go (wazero) and Java (Chicory) perform well with embedded WASM. Each wrapper must meet or exceed the performance of the language-native approach it replaces. Cross-language benchmarks validate this.
- WASM-First — Compiled to WebAssembly for cross-language portability
- No External Dependencies — Single WASM file, no JNI, no JavaScript bindings
- Chicory Compatible — Works with pure Java WASM runtimes (no native code)
- Memory Safe — Explicit alloc/dealloc, no panics, all errors returned as JSON
- Size Optimized — Aggressive compilation flags (
opt-level = "z", LTO,panic = "abort")
src/
├── lib.rs # Main entry point, WASM exports (update_state, evaluate)
├── evaluation.rs # Core flag evaluation logic, context enrichment ($flagd properties)
├── memory.rs # WASM memory management (alloc/dealloc, pointer packing)
├── storage/ # Thread-local flag state storage
├── operators/ # Custom JSON Logic operators (registered via datalogic_rs::Operator)
│ ├── fractional.rs # MurmurHash3-based consistent bucketing for A/B testing
│ └── sem_ver.rs # Semantic version comparison (=, !=, <, <=, >, >=, ^, ~)
├── model/ # Flag configuration data structures
└── validation.rs # JSON Schema validation against flagd schemas
All WASM export functions return a packed u64: upper 32 bits = pointer, lower 32 bits = length.
| Export | Signature | Description |
|---|---|---|
evaluate_logic |
(rule_ptr, rule_len, data_ptr, data_len) -> u64 |
Direct JSON Logic evaluation |
update_state |
(config_ptr, config_len) -> u64 |
Store flag configuration, returns changed flags |
evaluate |
(flag_key_ptr, flag_key_len, context_ptr, context_len) -> u64 |
Evaluate a stored flag |
alloc |
(len) -> *mut u8 |
Allocate WASM memory |
dealloc |
(ptr, len) |
Free WASM memory |
set_validation_mode |
(mode) -> u64 |
Set strict (0) or permissive (1) validation |
Caller allocates input buffers, callee allocates result buffers. Caller must free all allocations. UTF-8 JSON strings for all inputs/outputs.
- Never panic in WASM exports — All errors must be returned as JSON error responses
- Always validate UTF-8 — Use
string_from_memory()which returnsResult - Pointer lifetime — WASM memory is stable within a single function call but may be reallocated between calls
- Safety comments required — All
unsafeblocks must have// SAFETY:comments
The evaluator enforces strict resource limits at the WASM boundary and within the Rust API to prevent resource exhaustion from adversarial or accidentally oversized inputs. All limits are defined as constants in src/limits.rs.
| Constant | Value | Real-world meaning |
|---|---|---|
MAX_CONFIG_BYTES |
100 MB | ~125,000 complex flags at ~800 bytes each — covers the largest conceivable local-file deployment |
MAX_CONTEXT_BYTES |
1 MB | ~26,000 context fields at ~40 bytes each — far exceeds any realistic evaluation context |
MAX_REF_DEPTH |
64 | Maximum $ref hops during evaluator resolution — more than any real-world chain needs |
MAX_JSON_DEPTH |
128 | Maximum JSON nesting depth checked before parsing — prevents stack overflows |
- Config size (
MAX_CONFIG_BYTES): Checked inupdate_state_internal(WASM boundary) before reading memory. A host passing an oversizedconfig_lengets a JSON error without any memory access. - Context size (
MAX_CONTEXT_BYTES): Checked inevaluate_internalandevaluate_by_index_internalbefore reading context bytes. - JSON depth (
MAX_JSON_DEPTH): Pre-parse byte scan inFlagEvaluator::update_state(seelimits::check_json_depth). Fires beforeserde_jsonrecursion, preventing stack overflows from adversarial nesting. Also covers YAML configs, sinceupdate_state_from_yamlconverts to JSON first. $refdepth (MAX_REF_DEPTH): Depth counter inresolve_refs(seefeature_flag.rs). Counts only$refhops — JSON structural traversal does not consume the budget.
All limit violations return deterministic errors — no panics, no crashes:
- Config/context size violations →
{"success": false, "error": "... exceeds maximum ..."} - JSON depth violation →
{"success": false, "error": "JSON nesting depth exceeds ..."} $refdepth violation →{"success": false, "error": "$ref resolution depth limit of 64 exceeded ..."}- Evaluation with oversized context →
{"reason": "ERROR", "errorCode": "PARSE_ERROR", ...}
From Cargo.toml release profile:
[profile.release]
opt-level = "z" # Optimize for size
lto = true # Link-time optimization
codegen-units = 1 # Single codegen unit for better optimization
strip = true # Strip symbols
panic = "abort" # Remove panic unwinding infrastructureAlways build WASM with --no-default-features to exclude unnecessary dependencies.
The evaluator automatically injects standard $flagd properties into the evaluation context (see flagd provider spec):
| Property | Description |
|---|---|
$flagd.flagKey |
The flag being evaluated |
$flagd.timestamp |
Unix timestamp (seconds) at evaluation time |
targetingKey |
Defaults to empty string if not provided |
Two custom operators are implemented in src/operators/ and registered via datalogic_rs::Operator in src/operators/mod.rs. See the flagd custom operations spec for full details.
The starts_with and ends_with string-matching operators are built into datalogic-rs and require no custom implementation in this repository.
Uses the boon crate to validate flag configs against flagd-schemas:
- Strict (default): Reject invalid configs
- Permissive: Accept with warnings (for legacy compatibility)
Thread-local storage for flag configurations (src/storage/mod.rs). update_state detects and reports changed flags (added, removed, or mutated).
JSON Logic evaluation (lib.rs):
match logic.evaluate_json(&rule_str, &data_str) {
Ok(result) => EvaluationResponse::success(result),
Err(e) => EvaluationResponse::error(format!("{}", e)),
}Flag evaluation returns EvaluationResult with standardized error codes:
| Error Code | Meaning |
|---|---|
FLAG_NOT_FOUND |
Flag key not in configuration |
PARSE_ERROR |
JSON parsing or rule evaluation error |
TYPE_MISMATCH |
Resolved value doesn't match expected type |
GENERAL |
Other errors |
Resolution reasons: STATIC, DEFAULT, TARGETING_MATCH, DISABLED, ERROR, FLAG_NOT_FOUND
- Create new file in
src/operators/(e.g.,my_operator.rs) - Implement
datalogic_rs::Operatortrait - Register in
src/operators/mod.rsviacreate_evaluator() - Add tests in both unit tests and
tests/integration_tests.rs - Document in README.md under "Custom Operators"
- Primary logic is in
src/evaluation.rs - Context enrichment happens in
evaluate_flag()function - State retrieval uses thread-local storage via
get_flag_state() - Always maintain backward compatibility with the flagd provider specification
- Test with targeting rules, disabled flags, and missing flags
- All WASM-facing functions must use packed u64 returns
- Use
string_to_memory()to allocate and pack results - Use
string_from_memory()to read inputs (handles UTF-8 validation) - Document caller responsibilities in function doc comments
- Test with the Java example in
examples/java/
This WASM module is embedded in multiple language providers. The general integration pattern:
- Load WASM module
- Get function exports (
alloc,dealloc,evaluate_logic,update_state,evaluate) - For each call:
- Allocate memory for inputs using
alloc() - Write UTF-8 encoded JSON strings to WASM memory
- Call evaluation function with pointers and lengths
- Unpack returned u64 (
ptr = upper 32 bits,len = lower 32 bits) - Read result JSON from WASM memory
- Free all allocations using
dealloc()
- Allocate memory for inputs using
Memory lifecycle: Host application owns all memory allocation/deallocation decisions. WASM module only allocates result memory internally.
See examples/java/FlagdEvaluatorExample.java for a complete Java (Chicory) integration example. See python/README.md for native Python bindings via PyO3.
| Crate | Version | Purpose |
|---|---|---|
datalogic-rs |
4.0 | JSON Logic implementation |
serde, serde_json |
— | JSON serialization (no_std compatible with alloc) |
boon |
0.6 | JSON Schema validation |
murmurhash3 |
— | Hash function for fractional operator |
ahash |
— | Hash table implementation (SIMD-disabled for Chicory) |
getrandom |
— | Random number generation for WASM |
| Crate | Purpose |
|---|---|
cucumber |
Gherkin/BDD testing |
tokio |
Async runtime for tests |