Skip to content

Rust Runtime

The Rust runtime (xript-runtime) executes user scripts inside a native QuickJS sandbox powered by rquickjs. It provides the same manifest-driven binding model and capability enforcement as the JS/WASM and Node.js runtimes, but as a Rust crate with no WASM overhead.

For applications that need to run in browsers or other JavaScript environments, use the JS/WASM Runtime. For Node.js-only applications, see the Node.js Runtime. For .NET applications, see the C# Runtime. For a comparison of all runtimes, see Choosing a Runtime.

Add xript-runtime to your Cargo.toml:

[dependencies]
xript-runtime = "0.1"
use xript_runtime::{create_runtime, RuntimeOptions, HostBindings, ConsoleHandler};
let manifest_json = r#"{
"xript": "0.1",
"name": "my-app",
"bindings": {
"greet": {
"description": "Returns a greeting.",
"params": [{ "name": "name", "type": "string" }]
}
}
}"#;
let mut bindings = HostBindings::new();
bindings.add_function("greet", |args: &[serde_json::Value]| {
let name = args.first()
.and_then(|v| v.as_str())
.unwrap_or("World");
Ok(serde_json::json!(format!("Hello, {}!", name)))
});
let runtime = create_runtime(manifest_json, RuntimeOptions {
host_bindings: bindings,
capabilities: vec![],
console: ConsoleHandler::default(),
})?;
use std::path::Path;
use xript_runtime::{create_runtime_from_file, RuntimeOptions, HostBindings, ConsoleHandler};
let runtime = create_runtime_from_file(
Path::new("manifest.json"),
RuntimeOptions {
host_bindings: HostBindings::new(),
capabilities: vec![],
console: ConsoleHandler::default(),
},
)?;
use xript_runtime::{create_runtime_from_value, RuntimeOptions, HostBindings, ConsoleHandler};
let manifest = serde_json::json!({
"xript": "0.1",
"name": "my-app"
});
let runtime = create_runtime_from_value(manifest, RuntimeOptions {
host_bindings: HostBindings::new(),
capabilities: vec![],
console: ConsoleHandler::default(),
})?;

RuntimeOptions has three fields:

FieldTypeDescription
host_bindingsHostBindingsMap of binding names to host functions
capabilitiesVec<String>List of capabilities granted to this script
consoleConsoleHandlerConsole output routing (log, warn, error callbacks)

Host bindings map binding names to Rust closures. Each closure receives a slice of serde_json::Value arguments and returns Result<serde_json::Value, String>:

let mut bindings = HostBindings::new();
bindings.add_function("add", |args: &[serde_json::Value]| {
let a = args.get(0).and_then(|v| v.as_f64()).unwrap_or(0.0);
let b = args.get(1).and_then(|v| v.as_f64()).unwrap_or(0.0);
Ok(serde_json::json!(a + b))
});

For namespace bindings, use add_namespace with a HashMap of member functions:

use std::collections::HashMap;
use std::sync::Arc;
use xript_runtime::HostFn;
let mut members: HashMap<String, HostFn> = HashMap::new();
members.insert("getName".into(), Arc::new(|_| Ok(serde_json::json!("Hero"))));
members.insert("getHealth".into(), Arc::new(|_| Ok(serde_json::json!(100))));
bindings.add_namespace("player", members);

Route console.log, console.warn, and console.error from scripts to Rust callbacks:

use xript_runtime::ConsoleHandler;
let console = ConsoleHandler {
log: Box::new(|msg| println!("[LOG] {}", msg)),
warn: Box::new(|msg| eprintln!("[WARN] {}", msg)),
error: Box::new(|msg| eprintln!("[ERROR] {}", msg)),
};

The default ConsoleHandler silently discards all output.

let result = runtime.execute("2 + 2")?;
// result.value == serde_json::json!(4)
// result.duration_ms == 0.1 (approx)

execute runs the code synchronously and returns an ExecutionResult:

FieldTypeDescription
valueserde_json::ValueThe result of the last expression
duration_msf64Wall-clock execution time in milliseconds

All errors are variants of XriptError:

VariantWhen
ManifestValidation { issues }Manifest fails structural validation
Binding { binding, message }Host function throws or is missing
CapabilityDenied { binding, capability }Calling a gated binding without the required capability
ExecutionLimit { limit }Script exceeds timeout or resource limits
Script(String)Script throws an uncaught error
Engine(String)QuickJS engine error
Json(serde_json::Error)Manifest JSON parsing failed
Io(std::io::Error)File I/O failed (for create_runtime_from_file)

The sandbox provides a restricted JavaScript environment powered by QuickJS (native, via rquickjs):

Available: Math, JSON, Date, Number, String, Boolean, Array, Object, Map, Set, Promise, RegExp, Symbol, Proxy, Reflect, typed arrays, parseInt, parseFloat, isNaN, isFinite, and standard error constructors.

Blocked: eval, new Function, process, require, import, fetch, setTimeout, setInterval, and all Node.js/browser-specific globals.

Frozen namespaces: Namespace objects are frozen with Object.freeze. Scripts cannot add, remove, or reassign namespace members.

Execution limits: The timeout_ms field in the manifest’s limits section controls how long a script can run (default 5000ms). The memory_mb field controls maximum heap size. The max_stack_depth field controls the maximum call stack size.

pub fn create_runtime(manifest_json: &str, options: RuntimeOptions) -> Result<XriptRuntime>;
pub fn create_runtime_from_file(path: &Path, options: RuntimeOptions) -> Result<XriptRuntime>;
pub fn create_runtime_from_value(manifest: Value, options: RuntimeOptions) -> Result<XriptRuntime>;
pub struct XriptRuntime { /* ... */ }
pub struct RuntimeOptions { /* host_bindings, capabilities, console */ }
pub struct HostBindings { /* ... */ }
pub struct ConsoleHandler { /* log, warn, error */ }
pub struct ExecutionResult { pub value: Value, pub duration_ms: f64 }
pub struct Manifest { pub xript: String, pub name: String, /* ... */ }
pub enum XriptError { /* ManifestValidation, Binding, CapabilityDenied, ... */ }
pub type HostFn = Arc<dyn Fn(&[Value]) -> Result<Value, String> + Send + Sync>;
pub type Result<T> = std::result::Result<T, XriptError>;