Architecture Overview
Advanced Technical Content
This section is intended for contributors, library developers, and users who need deep technical understanding of jsrun's internals. For general usage, see the Concepts section.
Multi-Layer Design
jsrun is built with three distinct layers that communicate via well-defined boundaries:
graph TB
subgraph "Layer 1: Python API"
PYRT[Python Runtime class]
end
subgraph "Layer 2: Rust-Python Bridge"
BRIDGE[src/lib.rs<br/>PyO3 bindings]
PYRT_RS[src/runtime/python.rs]
end
subgraph "Layer 3: Rust Core"
HANDLE[src/runtime/handle.rs<br/>RuntimeHandle]
subgraph "Runtime Thread"
TOKIO[Tokio Runtime<br/><i>single-threaded event loop</i>]
subgraph "Inside Event Loop"
DISPATCHER[RuntimeDispatcher]
CORE[RuntimeCoreState]
DENO[deno_core::JsRuntime]
V8[V8 Isolate]
end
end
end
PYRT --> BRIDGE
BRIDGE --> PYRT_RS
PYRT_RS --> HANDLE
HANDLE -->|spawns thread| TOKIO
TOKIO -->|block_on| DISPATCHER
DISPATCHER -->|owns| CORE
CORE -->|contains| DENO
DENO -->|wraps| V8
Layer 1: Python API
Location: python/jsrun/__init__.py
The user-facing Python interface that provides:
- Convenience functions (
jsrun.eval(),jsrun.eval_async()) - High-level abstractions over the Rust runtime
- Pythonic API design
Layer 2: Rust-Python Bridge
Location: src/lib.rs, src/runtime/python.rs
PyO3-based bindings that:
- Expose Rust types to Python (e.g.,
Runtime,RuntimeConfig,JsFunction) - Handle Python GIL management
- Convert between Python and Rust types
- Define Python exceptions (
PyRuntimeError)
Key classes:
Runtime- Python wrapper aroundRuntimeHandleJsFunction- Python proxy for JavaScript functionsJsStream- Python wrapper for JavaScript async iteratorsJsUndefined- Sentinel for JavaScriptundefined
Layer 3: Rust Core
Location: src/runtime/
The core JavaScript execution engine built on deno_core and V8:
- RuntimeHandle (
handle.rs) - Thread-safe handle for communicating with runtime - RuntimeDispatcher (
runner.rs) - Multiplexes command processing with async job execution - RuntimeCoreState (
runner.rs) - Owns the V8 isolate and runtime state - RuntimeConfig (
config.rs) - Configuration options - PythonOpRegistry (
ops.rs) - Host function registry for Python-JavaScript bridge - PythonModuleLoader (
loader.rs) - Module resolution and loading - Context (
context.rs) - Isolated execution contexts
Key Components
RuntimeHandle
Purpose: Thread-safe communication between Python thread and runtime thread
pub struct RuntimeHandle {
sender: mpsc::UnboundedSender<HostCommand>,
shutdown_state: Arc<Mutex<ShutdownState>>,
// ...
}
Key characteristics:
- Clone-safe (uses
Arcinternally) - Sends commands via async channel (
mpsc::UnboundedSender) - Does NOT auto-shutdown on drop (explicit
close()required) - Thread-safe via
Arc<Mutex>for shutdown state
Lifetime: The handle does not own the runtime thread. You can clone handles and they'll all point to the same runtime. The runtime only shuts down when close() is explicitly called.
RuntimeCoreState
Purpose: Owns the V8 isolate and processes commands on the runtime thread
struct RuntimeCoreState {
js_runtime: JsRuntime,
registry: PythonOpRegistry,
module_loader: Rc<PythonModuleLoader>,
fn_registry: Rc<RefCell<HashMap<u32, StoredFunction>>>,
termination: TerminationController,
// ...
}
Key characteristics:
- Lives on the dedicated runtime thread
- NOT Send or Sync (V8 isolates are single-threaded)
- Owned by
RuntimeDispatcherwhich processesRuntimeCommandmessages from handle - Uses
deno_core::JsRuntimedirectly (not wrapped in a custom struct) - Handles promise polling with microtask checkpoints via async jobs
Thread model: Each RuntimeCoreState runs on its own OS thread with:
- V8 isolate (single-threaded JavaScript execution)
- Tokio single-threaded runtime (for async operations)
- Command receiver loop managed by
RuntimeDispatcher
Command Flow
The communication between Python and JavaScript follows this pattern:
sequenceDiagram
participant Python
participant Handle as RuntimeHandle
participant Channel as Command Channel
participant Tokio as Tokio Runtime
participant Dispatcher as RuntimeDispatcher
participant Core as RuntimeCoreState
participant V8 as V8 Isolate
Python->>Handle: runtime.eval("2 + 2")
Handle->>Channel: Send RuntimeCommand::Eval
Channel->>Tokio: Runtime thread event loop
Tokio->>Dispatcher: Receive command
Dispatcher->>Core: Process sync command
Core->>V8: Compile and execute script
V8-->>Core: Return result
Core-->>Dispatcher: Return result
Dispatcher-->>Channel: Send result via responder
Channel-->>Handle: Receive result
Handle-->>Python: Return converted value
Command types (defined in RuntimeCommand enum):
Eval/EvalAsync- Execute JavaScript codeBindObject- Register Python objectCallFunctionSync/CallFunctionAsync- Invoke JavaScript functionRegisterPythonOp- Register Python function as JavaScript opSetModuleResolver/SetModuleLoader- Configure module systemAddStaticModule- Register static moduleGetStats- Query runtime statisticsTerminate/Shutdown- Close runtime
Thread Safety Guarantees
V8 Isolate Constraints
V8 isolates are NOT thread-safe:
- Cannot be moved between threads (not
Send) - Cannot be accessed from multiple threads (not
Sync) - All V8 operations must happen on the thread that created the isolate
jsrun handles this by:
- Creating each isolate on a dedicated thread
- Never moving the isolate
- Using message passing for cross-thread communication
RuntimeHandle Safety
At the Rust level, RuntimeHandle IS thread-safe:
- Uses
Arcfor shared ownership - Commands sent via thread-safe channel
- Can be cloned and used from multiple threads
- Shutdown protected by
Arc<Mutex<ShutdownState>>
However, the Python Runtime class is NOT thread-safe:
- Marked as
unsendablein PyO3 (cannot be sent across threads) - Uses
RefCellinternally (not thread-safe) - Cannot be passed to another Python thread
- Each thread must create its own
Runtimeinstance
This design choice prevents accidental misuse from Python while allowing the underlying Rust implementation to be efficient.
GIL Release
Python's Global Interpreter Lock (GIL) is released during:
- JavaScript execution (
eval,eval_async) - Waiting for command responses
- Module evaluation
This enables true parallelism: Python threads can run while JavaScript executes.
Data Flow and Type Conversion
Python → JavaScript
- Python object →
python_to_js_value()→JSValue(Rust) JSValue→serde_json::to_value()→serde_json::Value- Pass to
deno_core→ Converted to V8 value - V8 value available in JavaScript
JavaScript → Python
- V8 value →
deno_coreextraction →serde_json::Value serde_json::Value→js_value_to_python()→ Python object
Special Cases
Undefined: JavaScript undefined uses a sentinel (jsrun.undefined) because Python has no native equivalent.
Binary data:
- JS Uint8Array/ArrayBuffer → Python bytes
- Python bytes/bytearray/memoryview → JS Uint8Array
Dates: JS Date ↔ Python datetime (UTC normalized)
Sets: JS Set ↔ Python set
BigInt: JS BigInt ↔ Python int (arbitrary precision)
Functions: JS functions become JsFunction proxies in Python that send CallFunction commands when invoked.
Ops System
The ops system allows Python functions to be called from JavaScript:
sequenceDiagram
participant JS as JavaScript
participant V8
participant Op as Op Callback (Rust)
participant Python
JS->>V8: __host_op_sync__(opId, args)
V8->>Op: Invoke registered op
Op->>Python: Call Python function
Python-->>Op: Return result
Op-->>V8: Serialize result
V8-->>JS: Return value
Key features:
- Permission-based (ops require specific permissions)
- Sync and async variants
- JSON serialization for arguments and results
- Automatic error propagation
Performance Considerations
Memory Management
- Each runtime has its own V8 heap (configured via
max_heap_size) - Serialization limits prevent OOM attacks (
max_depth,max_bytes) - Circular reference detection during conversion
- Automatic garbage collection by V8
Promise Polling
Async evaluation (eval_async) polls promises by:
- Checking if result is a promise
- Running microtask checkpoint
- Yielding to Tokio runtime
- Repeating until promise settles or timeout
This can be CPU-intensive for long-running promises.
Command Channel Overhead
Each Python API call involves:
- Serialization (Python → Rust)
- Channel send/receive
- Deserialization (Rust → V8)
- Reverse path for results
For tight loops, batch operations when possible.
Module System Architecture
The module system handles JavaScript import statements through a multi-stage resolution and loading pipeline:
sequenceDiagram
participant JS as JavaScript Code
participant V8
participant Resolver as Custom Resolver
participant Static as Static Module Registry
participant Loader as Custom Loader
participant Core as RuntimeCoreState
JS->>V8: import 'my-module'
V8->>Core: Resolve module specifier
Core->>Resolver: resolve('my-module', referrer)
alt Custom resolver returns specifier
Resolver-->>Core: 'custom://my-module'
Core->>Loader: load('custom://my-module')
Loader-->>Core: Module source code
else Resolver returns None
Resolver-->>Core: None
Core->>Static: lookup('my-module')
alt Found in static registry
Static-->>Core: Module source code
else Not found
Static-->>Core: Error: Module not found
end
end
Core->>V8: Compile and instantiate module
V8-->>JS: Module namespace object
Static modules:
- Registered in
ModuleLoaderbefore evaluation viaadd_static_module() - Resolved synchronously from in-memory map
- Checked after custom resolver returns
None
Custom loaders:
- Resolver: Maps specifier → resolved specifier (e.g.,
"math"→"file:///math.js") - Loader: Loads module source code asynchronously (can fetch from network, database, etc.)
- Supports dynamic imports and top-level await
Resolution order:
- Custom resolver (if set)
- Static module registry (if resolver returns
None) - Error if no match found