Host Functions -- Intermediate
This guide covers organizing host functions into extensions, the extension
coordinator, namespace validation, lifecycle hooks, OS contributions,
child policies, and the EventLoopExtension.
Prerequisites: Read the Beginner guide first.
MontyExtension
For anything beyond a handful of functions, use MontyExtension to group
related functions into a namespaced, lifecycle-managed unit.
Key Members
| Member | Purpose |
|---|---|
namespace |
Unique prefix for all function names (e.g., storage_) |
functions |
List of HostFunctions this extension provides |
osContribution |
Prefix map for declarative OS call interception |
childPolicy |
How the extension propagates to child sandboxes |
priority |
Attachment order (higher priority attaches first) |
supportedBackends |
Which backends (FFI/WASM) this extension supports |
onAttach(ctx) |
Lifecycle hook called during attachment |
onDispose() |
Lifecycle hook called when the session ends |
Example Extension
class StorageExtension extends MontyExtension {
final Map<String, Object?> _store = {};
@override
String get namespace => 'storage';
@override
List<HostFunction> get functions => [
HostFunction(
schema: const HostFunctionSchema(
name: 'storage_get',
description: 'Get a value by key.',
params: [HostParam(name: 'key', type: HostParamType.string)],
),
handler: (args, ctx) async => _store[args['key']],
),
];
@override
Map<String, OsCallHandler>? get osContribution => {
'os.storage_size': (op, args, kwargs) async => _store.length,
};
@override
ChildPolicy get childPolicy => ChildPolicy.clone;
@override
MontyExtension createChildInstance(ChildSpawnContext context) =>
StorageExtension();
}
ExtensionCoordinator
ExtensionCoordinator collects extensions, validates them, and wires them onto
a bridge:
final coordinator = ExtensionCoordinator()
..register(StorageExtension())
..register(MathExtension());
await coordinator.attachTo(bridge);
OS Contributions
Extensions can intercept OS calls (like pathlib or os) declaratively
via osContribution. The coordinator merges these from all extensions:
@override
Map<String, OsCallHandler> get osContribution => {
'Path.': _myHandler,
'os.': _myHandler,
};
If two extensions claim the same prefix, attachTo() throws a StateError.
Child Policies
When a child sandbox is spawned (via SandboxExtension), the parent
coordinator propagates extensions to the child based on their childPolicy:
ChildPolicy.exclude(default): The extension is not present in children.ChildPolicy.inherit: The same instance is shared with the child.ChildPolicy.clone:createChildInstance()is called to create a fresh copy for the child.
EventLoopExtension
EventLoopExtension turns a one-shot execute() call into a long-running
cooperative exchange.
Flow Diagram
sequenceDiagram
participant D as Dart
participant P as Python
D->>P: execute(script_with_loop)
loop Event Loop
P->>D: el_emit(state)
Note right of D: Dart UI updates
P-->>D: el_recv() (blocks)
Note left of P: Python is paused
D->>P: dispatch(event)
Note right of D: User action triggers dispatch
end
D->>P: dispatch({action: "quit"})
P-->>D: Loop breaks, script finishes
The Pattern
Python calls el_recv() to pause and el_emit(value) to push data back:
while True:
el_emit({"status": "ready"})
event = el_recv() # Blocks until Dart calls dispatch()
if event["action"] == "quit": break
Dart Side
final ext = EventLoopExtension();
final session = MontyRuntime(extensions: [ext]);
ext.lastEmittedSignal.subscribe((v) => print('Python: $v'));
session.execute(script);
ext.dispatch({'action': 'quit'});
What's on ctx
Every handler signature is (args, ctx) async. ctx is a HostContext
exposing the platform plumbing your handler may need:
| Field | Purpose |
|---|---|
emit(BridgeEvent) / emitText(String) |
Push events into the execution stream — progress, intermediate results, custom events |
executionId |
Unique ID for this tool call; correlate with BridgeFunctionEmit and other per-call events |
cancelToken |
Cooperative cancellation signal — race long-running work against cancelToken.future to bail when ExecutionHandle.cancel() fires |
os |
Currently-registered OsCallHandler, for handlers that want to invoke OS primitives directly without routing through Python |
parent |
Narrow HostParentRef? view of the owning runtime — emitChildEvent for child-spawning extensions, schemas for tool introspection. Deliberately does not expose execute() (that would deadlock the bridge). |
subExecute |
Closure that runs a sub-script in a fresh Monty interpreter. Use when you need to drive another Python execution from inside your handler — see Inputs, run_script, and Sub-Execution. |
The parent / subExecute split is intentional: nested
execute() on the same bridge would always deadlock (_isExecuting
stays true through dispatch), so the API offers subExecute for the
safe path and a narrower HostParentRef for everything else.
Next Steps
The Advanced guide covers deep-dives into
SandboxExtension and complex production patterns. For sub-scripting,
see Inputs, run_script, and Sub-Execution.