WASM Script Runtime
How Aether executes WebAssembly scripts with per-script resource caps, priority scheduling, hot-reload, and server-side AOT compilation.
Aether runs all user-authored and engine scripts inside a sandboxed WebAssembly runtime. Scripts are compiled to .wasm modules, verified for integrity, and executed under strict resource limits so that no single script can starve the simulation loop. This guide covers the runtime architecture, resource policies, scheduling model, and server-side AOT pipeline.
Overview
The scripting system lives in the aether-scripting crate and is split into two layers:
- Client runtime -- JIT-compiled WASM execution with fuel metering, SHA-256 integrity checks, and host API bindings. Used when scripts run locally on a player's device.
- Server runtime -- AOT-compiled native artifacts optimized for deterministic, low-latency execution on Linux servers. Used by authoritative world instances that process hundreds of scripts per tick.
Both layers share the same host API surface but differ in compilation strategy and resource enforcement scope.
Key Concepts
Fuel Metering
Every script execution is assigned a fuel budget. Each WASM instruction consumes one unit of fuel. When fuel runs out, the runtime suspends the script and reports a MeteringOutcome::BudgetExceeded result. This prevents infinite loops and guarantees bounded execution time per tick.
Resource Caps
Resource limits are enforced at two levels:
| Level | Controls |
|---|---|
| Per-script | CPU time, memory allocation, action rate (spawns, RPCs, storage writes) |
| World-level | Total CPU budget per tick, aggregate memory, maximum scripted entity count |
The world-level budget is typically 8 ms per tick. The scheduler divides this budget across all active scripts based on their priority.
Script Priority and Aging
Each script carries a priority value. The scheduler processes higher-priority scripts first. To prevent starvation, deferred (low-priority) scripts accumulate an aging bonus that gradually raises their effective priority over time.
Architecture
The runtime follows a pipeline from source bytes to sandboxed execution:
WASM Source Bytes
-> Compilation (JIT on client, AOT on server)
-> Integrity Verification (SHA-256 manifest)
-> Module Cache
-> Resource-Limited Execution
-> Script Sandbox
On the server side, the ServerRuntime struct orchestrates the full pipeline:
use aether_scripting::server_runtime::{
ServerRuntime, ServerRuntimeConfig, AotTarget,
ServerResourcePolicy, SyscallPolicy,
};
// Create a server runtime with default configuration
let config = ServerRuntimeConfig::from_env();
let mut runtime = ServerRuntime::new(config)?;
// Compile a WASM module to a native artifact
let artifact = runtime.compile_aot(&wasm_bytes, AotTarget::LinuxX64)?;
// Register the artifact under a script ID
runtime.register_artifact(script_id, artifact)?;
// Load and execute with resource limits
let module = runtime.load_module(script_id, version)?;
let policy = ServerResourcePolicy {
fuel_budget: 1_000_000,
memory_cap_mb: 64,
syscall_policy: SyscallPolicy::deny_all(),
};
let result = runtime.execute(&module, &policy)?;
Host API Surface
Scripts interact with the engine through six API traits. Each trait defines a set of callable functions that the runtime bridges into the host engine:
| Trait | Capabilities |
|---|---|
EntityApi | Query, spawn, destroy, and modify entities |
PhysicsApi | Apply forces, raycasts, overlap tests |
UIApi | Show/hide UI elements, update text |
AudioApi | Play sounds, adjust spatial audio parameters |
NetworkApi | Send RPCs, broadcast events |
StorageApi | Read and write persistent key-value data |
All API calls are rate-limited. For example, entity spawns and storage writes are capped per tick to prevent abuse:
use aether_scripting::{EntityApi, RateLimitedAction};
// The runtime enforces rate limits transparently.
// Exceeding the cap returns an ActionRateLimited error.
let entity = entity_api.spawn_entity(prefab_id)?;
Server-Side AOT Pipeline
Client-side JIT compilation introduces cold-start latency and non-deterministic performance. The server pipeline eliminates this with ahead-of-time compilation.
Compilation Targets
The AotTarget enum defines supported platforms:
pub enum AotTarget {
LinuxX64,
LinuxAArch64,
}
The AOT compiler uses Wasmtime's Engine::precompile_module to produce native artifacts. Each artifact includes a SHA-256 hash of both the source WASM and the compiled output for integrity verification.
Artifact Registry
The ArtifactRegistry stores compiled artifacts indexed by (script_id, version). Before loading a module, the registry verifies that the artifact hash matches the expected manifest:
// Register and verify an artifact
runtime.register_artifact(script_id, artifact)?;
// Loading verifies the manifest automatically
let module = runtime.load_module(script_id, version)?;
If the hash does not match, the registry rejects the artifact with a ManifestVerificationError.
Hot-Reload
Scripts can be updated without restarting the world. The hot-reload manager performs an atomic version swap:
- Upload a new artifact version.
- Verify its integrity against the manifest.
- Prepare a new
ModuleSlotwith the pending version. - Atomically swap: mark the new version as active, the old as draining.
- After all in-flight executions on the old version complete, unload it.
let new_artifact = runtime.compile_aot(&updated_wasm, AotTarget::LinuxX64)?;
let outcome = runtime.hot_reload(script_id, new_artifact)?;
match outcome {
ReloadOutcome::Success { old_version, new_version } => {
log::info!("Reloaded script {script_id}: v{old_version} -> v{new_version}");
}
ReloadOutcome::Rollback { reason } => {
log::warn!("Reload rolled back: {reason}");
}
}
World-Level Orchestration
The TickScheduler in aether-world-runtime drives script execution each tick. The orchestration flow is:
- The scheduler collects all runnable scripts and sorts them by effective priority.
- It allocates the per-tick CPU budget across scripts proportionally.
- Each script executes within its fuel allotment.
- If the world-level budget is exhausted, remaining scripts are deferred to the next tick.
- An overload tracker monitors a 10-second sliding window. If average utilization exceeds the threshold, the scheduler force-suspends the lowest-priority scripts.
Overload Protection
When the world is under heavy scripting load, the runtime applies back-pressure:
- Sliding window -- Tracks CPU usage over the last 10 seconds.
- Suspension -- Scripts that consistently exceed their budget are suspended.
- Graceful degradation -- Suspended scripts receive a callback opportunity to save state before being paused.
Configuration
All runtime settings are controlled through environment variables:
| Variable | Default | Description |
|---|---|---|
AETHER_SERVER_AOT_CACHE_DIR | /tmp/aether/aot_cache | Directory for AOT artifact storage |
AETHER_SERVER_MAX_MODULES | 256 | Maximum loaded modules per world |
AETHER_SERVER_DEFAULT_FUEL | 1000000 | Default fuel budget per script execution |
AETHER_SERVER_DEFAULT_MEMORY_MB | 64 | Default memory cap per script (MB) |
AETHER_SCRIPT_MAX_OPS | 10000 | Max instructions per VM execution |
AETHER_SCRIPT_MAX_VARS | 1024 | Max variables per script |
Key Types
| Type | Description |
|---|---|
ServerRuntime | Top-level orchestrator composing AOT, registry, limits, and hot-reload |
AotArtifact | Compiled native bytes with SHA-256 hash and target metadata |
ArtifactManifest | SHA-256 of source WASM, native artifact, version, and target |
ServerResourcePolicy | Per-script CPU time limit, memory cap, fuel budget, syscall restrictions |
HotReloadManager | Manages module slots and performs atomic version swaps |
MeteringOutcome | Whether execution completed within budget or was terminated |
SyscallPolicy | Allowed and denied syscall categories (filesystem, network, clock) |