How Monty Implements External Function Callbacks and Iterative Execution with start()/resume()
Monty pauses execution when bytecode hits an external function call, returning a serializable snapshot containing the full VM state, which the host can inspect, fulfill with native code, and resume via snapshot.run() to continue iterative execution.
Monty is Pydantic's sandboxed Python interpreter designed for secure execution of untrusted code. Unlike traditional embedded Python runtimes that block on I/O, Monty implements an iterative execution model where external function callbacks pause the VM and yield control back to the host, enabling fine-grained control over side effects and resource limits.
Core Rust Architecture
MontyRun::start() Entry Point
Execution begins in crates/monty/src/run.rs where MontyRun::start() initializes the heap, namespaces, and VM before running the module bytecode:
pub fn start<T: ResourceTracker>(
self,
inputs: Vec<MontyObject>,
resource_tracker: T,
print: &mut impl PrintWriter,
) -> Result<RunProgress<T>, MontyException> {
// 1️⃣ create heap & namespaces
let mut heap = Heap::new(executor.namespace_size, resource_tracker);
let mut namespaces = executor.prepare_namespaces(inputs, &mut heap)?;
// 2️⃣ instantiate VM
let mut vm = VM::new(&mut heap, &mut namespaces, &executor.interns, print);
// 3️⃣ run the module
let vm_result = vm.run_module(&executor.module_code);
let vm_state = vm.check_snapshot(&vm_result);
// 4️⃣ turn VM outcome into a high‑level progress value
handle_vm_result(vm_result, vm_state, executor, heap, namespaces)
}
Source: crates/monty/src/run.rs#L146-L166
RunProgress Enum
When the VM encounters an external function, it returns RunProgress::FunctionCall containing the function name, arguments, and a resumable snapshot:
pub enum RunProgress<T: ResourceTracker> {
/// Paused at an external function call.
FunctionCall {
function_name: String,
args: Vec<MontyObject>,
kwargs: Vec<(MontyObject, MontyObject)>,
call_id: u32,
state: Snapshot<T>,
},
/// Paused for pending async futures.
ResolveFutures(FutureSnapshot<T>),
/// Finished execution.
Complete(MontyObject),
}
Source: crates/monty/src/run.rs#L185-L203
Snapshot Struct for Resumable State
The Snapshot struct in crates/monty/src/run.rs captures the entire VM state including heap, namespaces, and the pending call ID:
pub struct Snapshot<T: ResourceTracker> {
executor: Executor,
vm_state: VMSnapshot,
heap: Heap<T>,
namespaces: Namespaces,
pending_call_id: u32,
}
Source: crates/monty/src/run.rs#L162-L173
Key methods on Snapshot:
run(result, print)– Consumes the snapshot, injects a return value or exception, and continues execution, producing a newRunProgress.tracker_mut()– Provides mutable access to the resource tracker for adjusting limits before resuming.
Source: crates/monty/src/run.rs#L84-L104
VM External Call Handling
When the bytecode VM encounters an external function call, it generates a deterministic call_id using the interning system in crates/monty/src/intern.rs and produces the RunProgress::FunctionCall variant.
Source for call-ID generation: crates/monty/src/intern.rs#L499-L505
Host-Side Bindings
Python Binding External Function Registry
The Python binding in crates/monty-python/src/external.rs provides ExternalFunctionRegistry to map external names to Python callables:
pub struct ExternalFunctionRegistry<'py> {
py: Python<'py>,
functions: &'py Bound<'py, PyDict>,
dc_registry: &'py Bound<'py, PyDict>,
}
Source: crates/monty-python/src/external.rs#L19-L27
The call method performs bidirectional conversion between MontyObject and Python objects:
- Lookup the named callable in the registry.
- Convert arguments using
monty_to_py. - Invoke the Python function via
call1orcall. - Convert the result back using
py_to_monty. - Wrap any Python exception as a Monty exception.
Source: crates/monty-python/src/external.rs#L39-L56
JavaScript Binding N-API Bridge
The JavaScript binding in crates/monty-js/src/monty_cls.rs exposes MontySnapshot and handles external function calls via N-API:
// Snapshot structure exposed to JS
pub struct MontySnapshot {
// fields filled by start()
}
Source: crates/monty-js/src/monty_cls.rs#L24-L40
The call_external_function bridge:
- Retrieves the JS object containing external functions.
- Converts each
MontyObjectargument to a N-APInapi_value. - Packs kwargs into a final object argument if present.
- Calls the JS function via
napi_call_function. - Converts the result back using
js_to_monty. - Returns an
ExternalResult(Return,Error, orFuture).
Source: crates/monty-js/src/monty_cls.rs#L18-L46
Resuming execution after the callback:
let snap = MontySnapshot { /* fields */ };
let result = snap.resume(ResumeOptions {
return_value: Some(js_val),
exception: None
})?;
The resume method internally calls snapshot.run(result, print_callback) and converts the new RunProgress back to either MontySnapshot, MontyComplete, or an exception.
Source: crates/monty-js/src/monty_cls.rs#L84-L102
End-to-End Execution Flow
The iterative execution follows this data flow:
Host (JS/Py) MontyRun::start VM (bytecode)
| --------------------> | --------------------> |
| | |
| <--------------------| <--------------------|
| RunProgress::FunctionCall (Snapshot) |
| | |
| call_external | |
| <--------------------| |
| | |
| resume() | |
| --------------------> | |
| | |
- Start:
MontyRun::start()initializes the VM and runs bytecode until completion or an external call. - Pause: When the VM hits an external function, it returns
RunProgress::FunctionCallcontaining the function name, arguments, and aSnapshot. - Inspect: The host examines the snapshot to determine which external function to invoke.
- Execute: The host runs the native implementation (Python, JavaScript, etc.).
- Resume: The host calls
snapshot.run(result, print)(or binding equivalents likeMontySnapshot.resume()), injecting the return value and continuing execution. - Iterate: The resumed execution may yield another
FunctionCall(nested calls),ResolveFutures(async), orComplete.
Practical Code Examples
Rust Core Implementation
For direct integration with the Rust crate, manually iterate through execution steps:
use monty::{MontyRun, MontyObject, RunProgress};
let runner = MontyRun::new(
"def greet(name):\n external(name)\n\ngreet('Bob')".to_owned(),
"demo.py",
vec![],
vec!["external".into()], // declare external function name
).unwrap();
match runner.start(vec![], monty::resource::NoLimitTracker, &mut monty::io::StdPrint)? {
RunProgress::FunctionCall { function_name, args, kwargs, state, .. } => {
// Host decides what to do
assert_eq!(function_name, "external");
let name = if let MontyObject::String(s) = &args[0] { s } else { "" };
// Return a greeting
let result = MontyObject::String(format!("Hello, {name}!"));
// Resume execution
let next = state.run(result, &mut monty::io::StdPrint)?;
// `next` should be Complete(...)
}
_ => panic!("unexpected progress"),
}
Key types: RunProgress::FunctionCall, Snapshot::run.
Python Host Implementation
Using the monty Python package to register and handle callbacks:
from monty import Monty, MontySnapshot
def hello(name):
return f"Hi, {name}!"
# Register the external function name and the Python implementation
m = Monty(
"print(hello('world'))",
externalFunctions=['hello'],
externalFunctionsImpl={'hello': hello},
)
# Start execution – we get a MontySnapshot because of the external call
snapshot: MontySnapshot = m.start()
assert snapshot.function_name() == "hello"
# The host (Python) already knows the implementation, but we could also
# fetch args from the snapshot if needed:
args = snapshot.args()
# Resume with the value returned by `hello`
snapshot = snapshot.resume(return_value=hello(args[0]))
# Execution finishes, result available via `snapshot.result()`
File containing the Python binding: crates/monty-python/src/monty_cls.rs.
JavaScript Host Implementation
Using the @pydantic/monty npm package:
import { Monty } from '@pydantic/monty';
// Host implementation
function double(x) {
return x * 2;
}
// Create interpreter; declare external function name
const m = new Monty('y = double(21)', { externalFunctions: ['double'] });
let progress = m.start(); // → MontySnapshot
if (progress instanceof MontySnapshot) {
// The VM paused awaiting `double`
console.log(progress.function_name()); // "double"
// Call the host function (could also be async)
const result = double(21);
// Resume interpreter with the return value
progress = progress.resume({ returnValue: result });
}
// `progress` is now a MontyComplete with final result
console.log(progress.output_value()); // 42
JS binding entry: crates/monty-js/src/monty_cls.rs.
Key Source Files
| Component | File | What it Provides |
|---|---|---|
| Core runner & snapshot | crates/monty/src/run.rs |
MontyRun::start, RunProgress, Snapshot |
| External‑function ID generation | crates/monty/src/intern.rs |
ExtFunctionId deterministic IDs |
| VM pause semantics | crates/monty/src/bytecode/vm/mod.rs |
FrameExit::ExternalCall handling |
| Python external‑function registry | crates/monty-python/src/external.rs |
ExternalFunctionRegistry and conversion helpers |
| JavaScript bridge to N‑API | crates/monty-js/src/monty_cls.rs |
call_external_function, MontySnapshot, ResumeOptions |
| Public JS API | crates/monty-js/src/lib.rs |
Monty, MontyComplete, MontySnapshot exported via N‑API |
| Public Python API | crates/monty-python/src/monty_cls.rs |
Monty, RunResult, external‑function handling |
Each file can be opened directly on GitHub, e.g.:
run.rs– https://github.com/pydantic/monty/blob/main/crates/monty/src/run.rsexternal.rs(Python) – https://github.com/pydantic/monty/blob/main/crates/monty-python/src/external.rsmonty_cls.rs(JS) – https://github.com/pydantic/monty/blob/main/crates/monty-js/src/monty_cls.rs
Summary
- Execution begins with
MontyRun::start(), which builds a heap, namespaces and a VM, then runs the bytecode. - When the VM encounters an external call, it pauses and returns
RunProgress::FunctionCall. - The
FunctionCallpayload contains a snapshot (Snapshot) that captures the full interpreter state. - Host code (Python, JavaScript, or any language with a binding) inspects the function name and arguments, invokes the corresponding external implementation, then resumes via
snapshot.run(result, …). - The resumed execution yields either another
FunctionCall, aResolveFutures(for async), orComplete. - Bindings (
monty-python,monty-js) wrap this flow in ergonomic high‑level objects (MontySnapshot,ResumeOptions, etc.) so developers can write simple “start → … → resume” loops without touching the internal VM.
This design cleanly separates the sandboxed Python interpreter from host side side‑effects, ensuring that all I/O, networking, or other unsafe operations are performed only by explicitly supplied external callbacks, preserving Monty’s security guarantees while still providing full extensibility.
Frequently Asked Questions
What is the difference between start() and resume() in Monty?
MontyRun::start() initializes a fresh VM instance and begins executing bytecode from the entry point of the provided module. When execution hits an external function, it returns a RunProgress::FunctionCall containing a Snapshot. Snapshot::run() (exposed as resume() in bindings) consumes this snapshot, injects the external function's return value or exception back into the VM, and continues execution until the next pause or completion.
How does Monty handle nested external function calls?
Monty supports nested external calls through iterative resumption. When you call snapshot.run() with a return value, the VM continues execution. If the resumed code immediately calls another external function, run() returns a new RunProgress::FunctionCall with a fresh snapshot. The host simply repeats the inspect → execute → resume loop until receiving RunProgress::Complete.
Can external functions be asynchronous in Monty?
Yes. Monty's RunProgress enum includes a ResolveFutures variant for async execution. When an external function returns a future or promise (depending on the binding), the VM pauses with ResolveFutures instead of FunctionCall. The host can then poll or await the native async operation and resume with the resolved value, allowing non-blocking I/O while maintaining Monty's sandboxed execution model.
How are exceptions handled when resuming execution?
When resuming via snapshot.run(), the host can pass either a return value or an exception. In Rust, this is handled by wrapping the result in the appropriate variant before calling run(). In Python and JavaScript bindings, the resume() method accepts optional return_value or exception parameters. If an exception is provided, Monty injects it into the VM as if the external function raised it, allowing standard Python exception handling to propagate naturally.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →