Rhai embed Engine, Scope, host types, and operation limits
Expose a narrow API, cap the work, and treat every script result as a Result.
*Let operators write rules without recompiling the service. Give the script only what you register, cap how much work it can do, and treat every script result as a Result.*
§I. Frame
C-days bridge Rust services to other runtimes. Rhai is the first: a scripting language built for embedding in Rust, with no access to the file system or network unless the host registers a function for it. The use case here is a request rule: a small script that decides allow or deny for a request, editable without a redeploy.
Done bar: compile a rule once, run it per request with a host type in scope, stop a runaway script with a hard limit, and tell a deny apart from an error.
§II. Three techniques
Register What You Expose (named technique). The script sees only what the host gives it.
let mut e = Engine::new();
e.register_type_with_name::<Request>("Request")
.register_get("path", Request::get_path)
.register_get("method", Request::get_method)
.register_get("bytes", Request::get_bytes);
e.register_fn("kib", |n: i64| n * 1024);
The script can read req.path, req.method, req.bytes, and call kib. It cannot set fields (no setters registered) or open a socket.
Cap the Work (named technique). A rule written by a person can loop forever. Set limits on the engine:
e.set_max_operations(10_000);
e.set_max_expr_depths(32, 32);
e.set_max_string_size(4_096);
When a script crosses the operation limit, evaluation stops with ErrorTooManyOperations.
Compile Once, Evaluate per Request (named technique). engine.compile(src) returns an AST and catches syntax errors at load time. Each request gets a fresh Scope with the request pushed in, then eval_ast_with_scope::<bool>. The ::<bool> is a contract: a script that returns a string is an error, not a deny.
§III. Run it
The rule:
if req.method == "GET" { return true; }
req.path.starts_with("/upload") && req.bytes <= kib(64)
cargo run (real):
GET /health 0 -> Ok(true)
POST /upload 1024 -> Ok(true)
POST /upload 70000 -> Ok(false)
DELETE /admin 0 -> Ok(false)
runaway -> Err("Too many operations (line 1, position 6)")
The last line runs loop { } true against the same engine. Without set_max_operations that call never returns and holds a Tokio worker forever (see Sunday: a held driver stalls every task on it).
cargo test: 4 passed, 0 warnings. Two tests pin the error shapes:
assert!(matches!(*err, EvalAltResult::ErrorTooManyOperations(_)));
assert!(matches!(*err, EvalAltResult::ErrorMismatchOutputType(..)));
A syntax error (if req.method == {) fails at compile, before any request.
§IV. Wiring it into a service
- Errors are values.
allowreturnsResult<bool, Box<EvalAltResult>>. Decide the policy forErrin Rust (deny and log is the usual choice); do not let a broken script read as "allowed". - Rhai evaluation is synchronous. In an Axum or Loco handler with tight limits it finishes in microseconds; with loose limits, move it to
spawn_blocking. - Reload by compiling the new source into a new
ASTand swapping it in. A script that fails to compile never replaces the working one.
§V. Drills
- A rule returns
"yes". Allow, deny, or error? (Error:ErrorMismatchOutputType.) - Which engine setting stops
loop { }? (set_max_operations.) - Can the script change
req.path? (No: only getters are registered.) - Where do syntax errors surface:
compileoreval? (compile.)
Close
Register What You Expose, Cap the Work, Compile Once. Give the script a narrow API, hard limits, and a typed result, and map every error to a decision in Rust. Next C-day: Rune.
Related
- Tome: TRPL ch9.2 (grounded-in), Programming Rust closures (referenced)
- Web: Rhai Book · max operations · custom types · rhai::Engine
- Prior Duha: Tokio runtime (2026-10-11), Loco skeleton (2026-10-12)