Skip to content

7.6. Rust

Rust is the fifth VMx source flavor. It lives under langs/rust/ as the vmx-rs Cargo package while exposing the crate namespace vmx.

7.6.1. Status

  • Source tree: langs/rust/
  • Package: vmx-rs
  • Current source line: vmx-rs 0.29.0 implementing spec 3.23.0
  • Publication status: crates.io release channel not published yet
  • License packaging: the crate ships the repository's Apache-2.0 text
  • Reactive primitive: VMx-owned hot-stream facade
  • Naming: Rust type names such as ComponentVm, snake_case methods such as construct() and dispose()
  • Conformance: all 403 library IDs are covered by behavioral Rust tests
  • Hub concurrency: ordinary producers retain synchronous calling-thread delivery while nested cross-hub callbacks enqueue without a wait cycle
  • Property notifications: notify_property_changed publishes to the hub and then the per-instance property_changed stream
  • Hint surfaces: hint() is immutable fixed metadata; modeled_hint() is recomputed from the model and publishes modeled_hint changes
  • Async commands: AsyncRelayCommand::builder() owns its task, predicate, additive triggers, cancellation mode, and fire-and-forget error stream
  • Async resources: AsyncResourceVm<T, D> accepts the shared hub and dispatcher, implements the ordinary component/node contract, and keeps resource phase in resource_status() / state().status() independently of lifecycle status
  • Threading: Dispatcher has paired foreground/background closure channels; component and read-only builders opt into background lifecycle work with .background(true), and background_errors() reports fire-and-forget failures

7.6.2. Fixed Aggregates

AggregateVm1 through AggregateVm6 expose immutable builders whose required component factories run at construct time. Typed component_1 through component_6 accessors return None before construction and the populated component afterward:

let aggregate = AggregateVm2::<ComponentVm, ComponentVm>::builder()
    .name("workspace")
    .hint("Two fixed child surfaces")
    .services(MessageHub::new(), NullDispatcher::new())
    .component_1(|| ComponentVm::new("navigation"))
    .component_2(|| ComponentVm::new("content"))
    .build()?;

assert!(aggregate.component_1().is_none());
aggregate.construct()?;
assert!(aggregate.component_1().is_some());

Factories are all evaluated and ownership-validated before any previous slot changes. Failed validation preserves the prior slots and parent links; reconstruction invokes the factories again and disposes replaced children.

7.6.3. Serviced Collections

Rust's ServicedObservableCollection<T> is distinct from ObservableList<T>. It owns an always-present local MessageHub stream and may also forward to an external hub:

let notes = ServicedObservableCollection::with_hub(owner_id, hub.clone());
let local = notes.collection_changed();
let subscription = local.subscribe(|message| render(message));

notes.push(first);
notes.push(second);
let old = notes.replace(0, revised)?;
notes.move_item(0, notes.len() - 1)?;     // one Move locally, then externally
notes.replace_all(server_snapshot);       // one Reset

remove removes the first equal value and returns false when absent; remove_at and replace return the removed or old item. Indices are usize, and out-of-range indexed operations fail atomically with VmxResult. Empty clear and same-index move are no-ops. Rust messages carry action plus optional old/new positions, sender ID, and property name; they intentionally carry no legacy index or item payload. The caller owns the subscription and stored items.

KeyedServicedObservableCollection<K,T> adds captured-key access while preserving that ordered surface. Rust keeps positional get(usize) and names the keyed operation get_by_key(&K) because methods cannot be overloaded:

let notes_by_id = KeyedServicedObservableCollection::with_hub(
    owner_id,
    hub.clone(),
    |note: &Note| Ok(note.id.clone()),
);
notes_by_id.push(first)?;
let note = notes_by_id.get_by_key(&first_id);
let added = notes_by_id.upsert(revised)?; // false: Replace at stable position
let removed = notes_by_id.remove_key(&first_id); // Option<Note>

Without an external hub, construct it with new(owner_id, key_of). contains_key tests membership. Keys require Eq + Hash + Send, not Clone. Projector and duplicate-key failures are atomic VmxResult failures. Captured keys do not follow mutable item properties; indexed replace or remove-then-push rekeys explicitly, and a same mutated instance can occupy two memberships. Lookup and target discovery are expected O(1), push is amortized O(1), and ordered middle shifts remain O(n). Local delivery precedes optional hub publication; a hub transaction defers only external delivery. The keyed type has no batch or VM lifecycle interface and never owns stored-item lifecycle.

7.6.4. Imperative Engine Bridge

Rust expresses the fixed source as hub + sender_id. subscribe_value returns VMx's Subscription; SubscribeValueOptions::default() uses PartialEq, while SubscribeValueOptions::with_equality(...) accepts custom equality without that bound:

use vmx::{SubscribeValueOptions, Subscription};

let selector_vm = camera_vm.clone();
let material_for_subscription = material.clone();
let exposure_subscription: Subscription = hub.subscribe_value(
    camera_vm.id(),
    move || selector_vm.model().exposure,
    move |exposure, _previous_exposure| {
        material_for_subscription.set_exposure(exposure);
    },
    SubscribeValueOptions::default().fire_immediately(true),
);

// Host adapter disposal:
exposure_subscription.dispose();

The callback receives (current, previous) by value; immediate delivery uses the initial value for both. The host adapter owns the subscription, and the selector reevaluates after every property message carrying this fixed sender ID rather than on every render frame.

7.6.5. Local Use

[dependencies]
vmx-rs = { path = "langs/rust" }
use vmx::{Command, ComponentVm, MessageHub, NullDispatcher, RelayCommand, VmxResult};

fn main() -> VmxResult<()> {
    let hub = MessageHub::new();
    let dispatcher = NullDispatcher::new();
    let vm = ComponentVm::with_services("hello-rust", hub, dispatcher);

    vm.construct()?;
    RelayCommand::new(|| println!("Hello from VMx Rust")).execute();
    vm.dispose()
}

7.6.6. Development

cargo fmt --manifest-path langs/rust/Cargo.toml -- --check
cargo clippy --locked --manifest-path langs/rust/Cargo.toml --all-targets -- -D warnings
cargo test --locked --manifest-path langs/rust/Cargo.toml
cargo run --locked --manifest-path examples/rust/console/hello-vmx/Cargo.toml
cargo test --locked --manifest-path examples/rust/tui/notes-showcase/Cargo.toml
cargo run --locked --manifest-path examples/rust/tui/notes-showcase/Cargo.toml -- --smoke

7.6.7. Showcase

Rust now ships a Ratatui Notes Showcase at examples/rust/tui/notes-showcase/. The host is intentionally a renderer and input adapter only: VMx view models own notebook selection, search/filtering, page state, form validation, save/revert commands, global token search, notifications, and edit/preview mode.

See Rust TUI Notes Showcase for the VM-layer diagram and run commands.

Use ADR-0080 for the adoption decision and ADR-0081 for the full-conformance cutover.