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-rs0.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 asconstruct()anddispose() - 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_changedpublishes to the hub and then the per-instanceproperty_changedstream - Hint surfaces:
hint()is immutable fixed metadata;modeled_hint()is recomputed from the model and publishesmodeled_hintchanges - 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 inresource_status()/state().status()independently of lifecycle status - Threading:
Dispatcherhas paired foreground/background closure channels; component and read-only builders opt into background lifecycle work with.background(true), andbackground_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.