Rust Learning Path
This page links Rust concepts to the concrete places in the code where they appear — a common thread to learn along the way. It grows with the project.
|
Besides being a real product, |
Workspace & crates
The project is a Cargo workspace with several crates (wusel-core,
wusel-fuse, wusel-desktop, wusel-mock, wusel) that share dependency versions via
[workspace.dependencies]. A crate is the compilation unit; a workspace bundles
several of them with a shared Cargo.lock and target/.
-
Read up:
Cargo.toml(root) and theCargo.tomlof each crate.
Error handling: Result, enum, ?
Rust has no exceptions. Errors are values of type Result<T, E>.
-
wusel-coredefines its own errorenum(error.rs) with thethiserrorderive — an error can have several variants (Http,Auth,LoginPending, …). -
The
?operator returns an error early and converts it in the process automatically (thanks to#[from]). -
wuselusesanyhowfor convenient, context-rich errors in the binary — rule of thumb:thiserrorin libraries,anyhowin binaries.
Ownership & borrowing (the core)
Every value has exactly one owner; a value is moved when you pass it on.
-
Real-world example: In
webdav.rs,PartialEntry::finish(self)consumed the value. In a loop this led to a "use after move". Solution:std::mem::take(&mut cur)takes the value out and leaves aDefaultbehind — the next iteration starts fresh. A typical Rust pattern.
Traits & the decorator pattern
A trait describes behavior (like an interface).
-
trait ContentSource:LiveWebDavimplements it (reads live),CachingSourceimplements the same trait and wraps a live source (decorator). The FUSE layer knows onlydyn ContentSourceand notices nothing of the cache. See Architecture.
Traits as a test seam (and #[cfg(test)] modules)
The same trait mechanism is also how you get a dependency out of the way of a
test — Rust has no mocking framework, and needs none.
-
trait Secretsinkeyring.rsis everythingcredentials.rsneeds from the OS keyring: four methods. The product passes the one real implementation (keyring::Os); the tests pass aHashMapthat can be told to be absent, locked, or forgetful. Note what that changes: the interesting behaviour — falling back to the file — is no longer a property of the machine running the test. -
&dyn Secretsis dynamic dispatch: one compiled function, the implementation chosen at runtime through a vtable. The alternative, a genericfn store<S: Secrets>(…), dispatches statically and compiles a copy per type. Here the call happens once per login, so the simpler signature wins. -
A
#[cfg(test)] pub(crate) mod fakelets the double live next to the real backend and still vanish from release builds. Test code can be shared across modules of the same crate this way —credentials.rsuses `keyring’s fake. -
The risk of every double is that it drifts from the real thing. The answer is a contract test: one function taking
&dyn Secrets, run against the fake and againstOs. Written once, it holds both to the same promises.
Conditional compilation: #[cfg(…)]
Code can be included or excluded per platform.
-
wusel-fuse/src/lib.rs:#[cfg(target_os = "linux")]includes the FUSE path only where a driver exists; otherwise a stub takes over. This way the build does not break on other platforms.
Cargo features
Optional functionality behind switches.
-
wuselhas thefusefeature, which activates the optional dependencywusel-fuse(dep:wusel-fuse). Without the featurewuselbuilds as a pure CLI — handy on macOS (the mount is Linux-only). Build with:cargo build -p wusel --features fuse.
Async & the sync↔async bridge
Network I/O runs async via tokio (async fn, .await).
-
For this
wuselstarts atokio::runtime::Runtimeand callsblock_on(…)(seecmd_logininmain.rs). -
Tension: FUSE callbacks are synchronous/blocking. The adapter has to bridge the async world to the sync world — see the FUSE ↔ async bridge in the architecture.
Shared mutable state: Arc, Mutex, and an atomic in front of the lock
Rust has no synchronized: to share something mutable between threads you wrap
it — Arc for shared ownership, Mutex for exclusive access.
-
health.rs(Reachability) is a compact example of all three moving parts. Several threads — the FUSE dispatchers, the syncer, the hydrator, the push listener — report every request outcome to one instance held asArc<Reachability>. -
An
AtomicBoolsits in front of theMutex: the healthy path (a successful request, thousands per minute) is one relaxed atomic load and returns without ever taking the lock. The mutex is the truth; the atomic is only a hint that says "nothing is wrong, do not bother". -
The lock is deliberately not held while notifying: the guard is dropped — by ending the block it lives in — before the D-Bus call, so a slow desktop cannot make requests queue behind it. Deciding inside the lock and acting outside it is the pattern to copy.
-
unwrap_or_else(|e| e.into_inner())onlock()is poison tolerance: a thread that panicked while holding the lock must not take the whole mount down with it.