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.31.0 implementing spec 3.25.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 407 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 - Completion handles:
AsyncValue<T>is an executor-neutralFuturewith a blockingwait(). When amaporand_thencallback panics, the handle it returns settles as panicked:wait()and.awaitre-raise the panic andwait_result()returns anAsyncValuePanic, so no waiter hangs (ADR-0138) - 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.3.1. Ownership-Test Conventions¶
The caller controls each stored VM's construct, destruct and dispose operations. A serviced collection retains an item as an ordinary Rust value while it is a member; that retention does not transfer semantic lifecycle ownership. A returned old value, lookup result or snapshot can keep a shared item alive after removal. Cloning the collection shares its backing store without cloning every item, and Rust's collection-change history has no item payload to retain.
COL-055 and COL-062 use test-private shared wrappers around real
ComponentVm instances. After each operation they count every known wrapper
handle and check lifecycle hooks and parent state. Once returned and snapshot
handles are released, the caller disposes each VM exactly once, releases its
last wrapper, and checks both weak-reference expiration and a separate native
release counter. Rust Drop is not treated as VM disposal. These exact counts
are deterministic because the tests control every holder and run mutations
synchronously; they do not count the VM's internal implementation references.
The eight C#, Python, TypeScript and Swift counterpart tests already use direct lifecycle observables rather than the Rust reference-count tautology. Their probe coverage varies: C# observes disposal, Swift omits some lifecycle categories, Python's keyed test does not retain failed-operation candidates, and TypeScript's keyed test instruments only one item. They do not assert exactly-once caller cleanup. These are limits of the test evidence, not proof of a collection implementation defect. The existing ownership contract and 403-ID catalog are unchanged.
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.6.1. Coverage¶
The coverage floor job in .github/workflows/rust.yml measures library
coverage with cargo-llvm-cov on a pinned toolchain. It fails when line,
region, or function coverage falls below the Rust floors in
tools/coverage-floors.json:
cargo install cargo-llvm-cov --version 0.9.1 --locked
cargo llvm-cov --locked --all-features --manifest-path langs/rust/Cargo.toml \
--json --output-path rust-coverage.json
python3 tools/check-coverage-floor.py --flavor rust --report rust-coverage.json
The denominator is langs/rust/src/ only. Test, example, and build files are
not counted, so a change that touches only generated or resource files cannot
fail the gate. Removing a test that exercised library code lowers the figure and
can. A failure names each source file that gained uncovered lines since the
recorded baseline, with that file's unexecuted line ranges as llvm-cov show
marks them. The counts come from llvm-cov's summaries, which count a line once
for each function that spans it, so a closure's lines count twice and a file's
ranges can hold fewer lines than its count. The job uploads the llvm-cov export
and a provenance file that names the commit, toolchain, and measured figures.
cargo-llvm-cov builds into its own target directory, so instrumented code
never reaches the package job or a published crate.
Coverage says which lines ran, not whether a test would catch a wrong result. It does not replace the conformance assertions.
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.