Hedronite · Backend Dev · Phase C · Tue 2026-10-13 · BACKFILL

Rhai embed Engine, Scope, host types, and operation limits

Expose a narrow API, cap the work, and treat every script result as a Result.

Lesson Class: Duha (T3 · phase C Bridges)
Cursor: C row 1: Rhai embed
Lab: rhai 1.26.1 · cargo test 4 passed
Next C-day: Rune embed
Register What You Expose
Getters only, no setters.
Cap the Work
set_max_operations stops loop { }.
Compile Once
AST per rule, Scope per request.
A string is not a deny.

*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

§V. Drills

  1. A rule returns "yes". Allow, deny, or error? (Error: ErrorMismatchOutputType.)
  2. Which engine setting stops loop { }? (set_max_operations.)
  3. Can the script change req.path? (No: only getters are registered.)
  4. Where do syntax errors surface: compile or eval? (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