Native Crate Architecture
The Rust crate (native/src/) provides the C FFI layer between Dart and
the monty interpreter. Four source files handle the full surface:
handle.rs--MontyHandlestate machine and execution logiclib.rs--extern "C"FFI entry pointserror.rs-- panic boundary, C string helpers, exception serializationconvert.rs--MontyObjectto/fromserde_json::Valueconversion
Handle Lifecycle
monty_create(code, ext_fns, script_name)
→ MontyHandle { state: Ready(MontyRun), limits: None }
┌──── monty_run() ──────────────────────────────────────────┐
│ Ready → run(tracker, print) → Complete { result_json } │
└────────────────────────────────────────────────────────────┘
┌──── monty_start() ────────────────────────────────────────┐
│ Ready → start(tracker, print) │
│ → Complete | Paused{Limited,NoLimit} | Futures{...} │
│ │
│ monty_resume(value_json) / monty_resume_with_error(msg) │
│ Paused → snapshot.run(result, print) │
│ → Complete | Paused | Futures │
│ │
│ monty_resume_as_future() │
│ Paused → snapshot.run_pending(print) │
│ → Complete | Paused | Futures │
│ │
│ monty_resume_futures(results_json, errors_json) │
│ Futures → snapshot.resume(ext_results, print) │
│ → Complete | Paused | Futures │
└────────────────────────────────────────────────────────────┘
monty_free(handle)
→ drop(Box::from_raw(handle))
FFI Boundary Contract
All data crosses the boundary as JSON strings (NUL-terminated UTF-8).
Errors use an out-parameter pattern: out_error: *mut *mut c_char.
- Rust to Dart strings: Allocated with
CString::into_raw(). Dart reads and frees viamonty_string_free(). - Dart to Rust strings: Passed as
*const c_char, parsed byparse_c_str()(null check + UTF-8 validation). - Progress functions use the
ffi_progress!macro which handles: handle null check,catch_ffi_panicboundary, and error out-parameter dispatch -- reducing each FFI function to its essential logic.
Tracker Abstraction
The monty crate is generic over ResourceTracker (LimitedTracker for
bounded execution, NoLimitTracker for unbounded). Since HandleState
stores Snapshot<T> and FutureSnapshot<T>, the enum needs separate
variants for each tracker type (7 total: Ready, PausedLimited,
PausedNoLimit, FuturesLimited, FuturesNoLimit, Complete,
Consumed).
The TrackerExt trait maps each tracker type to its corresponding
HandleState constructors:
trait TrackerExt: monty::ResourceTracker + Sized {
fn into_paused(snapshot: Snapshot<Self>, meta: PendingMeta) -> HandleState;
fn into_futures(snapshot: FutureSnapshot<Self>, call_ids_json: String) -> HandleState;
}
This allows a single generic process_progress<T: TrackerExt>() method
to handle all RunProgress variants, dispatching to the correct
HandleState via the trait without duplicating match arms.
PrintWriter Drain Pattern
Every execution call (run, start, resume*) captures print output
via PrintWriter::Collect(String::new()). After the call completes,
drain_print() moves the collected string into self.print_output.
The run_snapshot_op() helper combines this with error dispatch:
fn run_snapshot_op<T: TrackerExt>(
&mut self,
f: impl FnOnce(&mut PrintWriter) -> Result<RunProgress<T>, MontyException>,
) {
let mut print = PrintWriter::Collect(String::new());
let result = f(&mut print);
self.drain_print(print);
// dispatch Ok(progress) or Err(exc)
}
This eliminates the repeated init/drain/match pattern across all resume methods.