Good library APIs don't just work when used correctly — they resist misuse. Rust's type system gets you halfway there, but a few deliberate patterns close the remaining gaps. This guide covers practical techniques we've applied across real RF engineering crates.
#[must_use] — Don't Let Results VanishThe most common API mistake: calling a function and ignoring its return value.
// Bug: the converted value is silently discarded
db_to_linear(gain_db);
This compiles, runs, and does nothing useful. The #[must_use] attribute turns this into a compiler warning:
#[must_use = "this returns the converted value and does not modify the input"]
pub fn db_to_linear(db: f64) -> f64 {
10.0_f64.powf(db / 10.0)
}
warning: unused return value of `db_to_linear` that must be used
--> src/main.rs:4:5
|
4 | db_to_linear(gain_db);
| ^^^^^^^^^^^^^^^^^^^^^^
|
= note: this returns the converted value and does not modify the input
Functions that return computed values (pure functions):
#[must_use]
pub fn cascade_noise_figure(nf1_db: f64, nf2_db: f64, gain1_db: f64) -> f64 {
// ...
}
Types that represent results — apply it to the struct itself:
#[must_use = "LinkBudget contains the analysis results"]
pub struct LinkBudget {
pub margin_db: f64,
pub cn0_db_hz: f64,
// ...
}
When NOT to use it: Functions called primarily for side effects (writing files, printing, modifying state). If the return value is just a status indicator that's sometimes useful, #[must_use] will generate noise.
When retrofitting #[must_use] to an existing crate, don't sprinkle it randomly. Sweep systematically:
() valuescargo clippy — it also suggests #[must_use] candidates via clippy::must_use_candidatecargo clippy -- -W clippy::must_use_candidate 2>&1 | head -40
#[doc(alias)] — Help Users Find Your APIEngineers search for APIs using domain terminology that doesn't always match Rust naming conventions. doc(alias) bridges the gap:
/// Convert noise figure (dB) to noise temperature (Kelvin).
#[doc(alias = "NF to T")]
#[doc(alias = "noise temperature")]
#[doc(alias = "T_noise")]
pub fn noise_figure_to_temperature(nf_db: f64) -> f64 {
let nf_linear = 10.0_f64.powf(nf_db / 10.0);
290.0 * (nf_linear - 1.0)
}
Now searching for "T_noise" or "noise temperature" in cargo doc or docs.rs finds this function. Especially valuable when:
noise_figure_to_temperature) but the domain uses symbols (T_N, NF)Instead of accepting raw f64 everywhere and checking at runtime, validate at the boundary:
/// A frequency that is guaranteed positive and finite.
#[derive(Debug, Clone, Copy)]
pub struct Frequency {
hz: f64,
}
#[derive(Debug, thiserror::Error)]
#[error("frequency must be positive and finite, got {0}")]
pub struct InvalidFrequency(f64);
impl Frequency {
pub fn from_hz(hz: f64) -> Result<Self, InvalidFrequency> {
if hz > 0.0 && hz.is_finite() {
Ok(Self { hz })
} else {
Err(InvalidFrequency(hz))
}
}
pub fn from_ghz(ghz: f64) -> Result<Self, InvalidFrequency> {
Self::from_hz(ghz * 1e9)
}
pub fn hz(&self) -> f64 {
self.hz
}
}
Now every function that takes Frequency knows it's valid — no defensive checks scattered through the codebase. The validation happens once, at creation.
f64 Is FineNot everything needs wrapping. Use validated types when:
Skip it when the function is simple arithmetic that works on any finite float.
#[non_exhaustive]When your crate defines enums that might grow, #[non_exhaustive] forces downstream users to handle the _ wildcard — so adding a variant is never a breaking change:
#[non_exhaustive]
#[derive(Debug, Clone, Copy)]
pub enum Modulation {
Bpsk,
Qpsk,
Psk8,
Apsk16,
Apsk32,
}
Downstream code must include a wildcard arm:
match modulation {
Modulation::Bpsk => { /* ... */ }
Modulation::Qpsk => { /* ... */ }
_ => { /* handle future variants */ }
}
Trade-off: This reduces exhaustiveness checking for downstream users. Only use it for enums you genuinely expect to extend.
#![warn(missing_docs)] — Enforce DocumentationAt the crate root:
#![warn(missing_docs)]
Every public item without a doc comment triggers a compiler warning. This is a quality gate — it's easy to add a function and forget to document it, but the compiler won't let you forget.
Pair with #![deny(missing_docs)] once coverage is complete to prevent regressions.
/// Compute free-space path loss.
///
/// # Arguments
///
/// * `distance_m` — Distance in meters (must be positive)
/// * `frequency_hz` — Carrier frequency in hertz (must be positive)
///
/// # Returns
///
/// Path loss in dB (always positive).
///
/// # Formula
///
/// ```text
/// FSPL(dB) = 20·log₁₀(4πdf/c)
/// ```
///
/// # Examples
///
/// ```
/// use rfconversions::free_space_path_loss;
///
/// let fspl = free_space_path_loss(1000.0, 1e9);
/// assert!((fspl - 92.45).abs() < 0.1);
/// ```
#[must_use]
pub fn free_space_path_loss(distance_m: f64, frequency_hz: f64) -> f64 {
// ...
}
Include the formula in text code blocks — engineers want to verify the math, not just read prose.
A checklist for publishing a defensive crate:
#![warn(missing_docs)] at crate root#[must_use] on all pure public functions and result types#[doc(alias)] for domain-specific search terms#[non_exhaustive] on enums you expect to extendcargo clippy with must_use_candidate and missing_docs_in_private_itemscargo semver-checks before every publish (catches accidental breaking changes)cargo mutants to verify tests actually exercise the logicThese patterns compound. A crate with all eight is remarkably hard to misuse — and the compiler does the enforcement, not code review.