Expand description
§Lease
A std-ish primitive for temporary ownership transfer in Rust.
This crate provides a comprehensive solution for the fundamental problem of temporarily transferring
ownership of values across scopes, closures, and async boundaries. It solves the “cannot borrow
across .await” problem and enables scoped mutation patterns that are otherwise impossible in safe Rust.
§Installation
[dependencies]
lease = "0.1"For no_std environments (embedded, WASM, etc.):
lease = { version = "0.1", default-features = false }§Core Philosophy
Ownership Leasing is built on three principles:
- Zero-overhead by default - Operations that can be zero-cost are zero-cost
- Memory safety first - Never compromise Rust’s safety guarantees
- Explicit trade-offs - Make all performance and safety trade-offs visible to users
§Lease vs Borrow: Why Ownership Leasing Exists
§The Problem with Borrowing
Rust’s borrowing system is excellent for most use cases, but has fundamental limitations when you need temporary ownership transfer:
fn problematic() {
let mut data = vec![1, 2, 3];
std::thread::spawn(move || {
// Borrow checker would error: `data` doesn't live long enough
// The closure captures `&mut data` but the thread might outlive it
data.push(4);
});
}§The Solution: Ownership Leasing
Leasing temporarily transfers full ownership across scopes, closures, and async boundaries while maintaining safety:
use lease_rs::lease;
let mut data = vec![1, 2, 3];
tokio::spawn(async move {
// Leasing works: full ownership temporarily transferred
let (data, ()) = lease(data, |mut owned| {
owned.push(4);
(owned, ()) // Return the modified data
});
assert_eq!(data, [1, 2, 3, 4]);
}).await;§Key Differences
| Aspect | Borrowing (&mut T) | Leasing (lease()) |
|---|---|---|
| Ownership | Reference only | Full ownership transfer |
| Lifetime | Borrow checker enforced | Explicit scope control |
| Async/.await | Cannot cross .await | Full ownership across .await |
| Closures | Limited by lifetimes | Move semantics |
| Safety | Compile-time guarantees | Runtime safety + compile-time |
| Performance | Zero-cost | Zero-cost |
| Flexibility | High for simple cases | High for complex patterns |
§When to Use Leasing
- Across async boundaries (
.await,tokio::spawn) - Complex closure patterns requiring move semantics
- Temporary ownership transfer with guaranteed restoration
- Self-referential structures that need reconstruction
- Error recovery patterns where you control final state
§When Borrowing is Better
- Simple synchronous code without complex ownership
- Performance-critical inner loops (borrowing is slightly faster)
- When you don’t need ownership (just mutation)
§Alternatives & Comparisons
| Approach | When it works | Limitations vs lease |
|---|---|---|
Manual ptr::read/write + ManuallyDrop | Sync only | Very verbose, easy to get UB on panic/cancellation |
std::mem::replace | Simple cases | No async support |
tokio::sync::Mutex / Arc<Mutex<T>> | Shared access | Runtime locking overhead, changes semantics |
Channels (mpsc, crossbeam) | Thread boundaries | Different model, more allocation |
scopeguard / custom RAII guard | Manual cancellation safety | You write the guard yourself every time |
lease is the sweet spot for temporary exclusive ownership across async boundaries with cancellation safety.
§Performance at a Glance
Most functions are zero-cost. Only async mutable operations with cancellation safety have runtime cost:
- Zero-cost (9/11 functions):
lease,lease_async,try_lease,try_lease_async,lease_mut,try_lease_mut,lease_async_mut_unchecked,try_lease_async_mut_unchecked - Non-zero-cost (2/11 functions):
lease_async_mut,try_lease_async_mut(requireT: Clone)
§API Overview
The API is divided into four main categories:
§Owned Variants (Zero-cost)
lease()/lease_async()- Transfer owned values across closures/futurestry_lease()/try_lease_async()- With error propagation
§Mutable Reference Variants
lease_mut()/try_lease_mut()- Sync mutable reference leasinglease_async_mut()/try_lease_async_mut()- Async mutable reference leasing with explicit error handlinglease_async_mut_unchecked()/try_lease_async_mut_unchecked()- Zero-cost async mutable (panics on cancellation)
§Convenience Macros
lease_with!()- Ergonomic syntax for common patternstry_lease_with!()- With error propagationlease_async_with!()/try_lease_async_with!()- Async variants
§API Decision Tree: Which Function to Use?
§Step 1: Do you have owned data or a mutable reference?
- Owned data (
T): Uselease*()functions - Mutable reference (
&mut T): Uselease_mut*()functions
§Step 2: Is this async code (crossing .await points)?
- No (sync): Use sync variants (
lease(),lease_mut()) - Yes (async): Continue to Step 3
§Step 3: Does your closure need to return errors?
- No: Use plain variants (
lease_async(),lease_async_mut()) - Yes: Use
try_*variants (try_lease_async(),try_lease_async_mut())
§Step 4: For async mutable references - cancellation safety?
- Need cancellation safety (tokio::select!, timeout): Use checked variants
- With errors:
try_lease_async_mut()(recommended) - Without errors:
lease_async_mut()(requires T: Clone)
- With errors:
- Cancellation impossible (fire-and-forget): Use unchecked variants
- With errors:
try_lease_async_mut_unchecked() - Without errors:
lease_async_mut_unchecked()(zero-cost)
- With errors:
§Quick Reference Table
| Data Type | Async? | Errors? | Cancellation Risk | Function |
|---|---|---|---|---|
T (owned) | No | No | N/A | lease() |
T (owned) | No | Yes | N/A | try_lease() |
T (owned) | Yes | No | N/A | lease_async() |
T (owned) | Yes | Yes | N/A | try_lease_async() |
&mut T | No | No | N/A | lease_mut() |
&mut T | No | Yes | N/A | try_lease_mut() |
&mut T | Yes | No | Safe | lease_async_mut() |
&mut T | Yes | Yes | Safe | try_lease_async_mut() |
&mut T | Yes | No | Unsafe | lease_async_mut_unchecked() |
&mut T | Yes | Yes | Unsafe | try_lease_async_mut_unchecked() |
§Performance Priority?
- Zero-cost critical: Use unchecked variants or owned variants
- Safety critical: Use checked variants (accept clone cost)
§Performance Characteristics
§Zero-Cost vs. Non-Zero-Cost: Complete Breakdown
| Function | Zero-Cost? | Cost Details | When to Use |
|---|---|---|---|
lease() | YES | Truly zero-cost, monomorphized | Always |
lease_async() | YES | Zero-cost, same as manual async | Always |
try_lease() | YES | Zero-cost | Always |
try_lease_async() | YES | Zero-cost | Always |
lease_mut() | YES | Near zero-cost (ptr ops only) | Always |
try_lease_mut() | YES | Near zero-cost | Always |
lease_async_mut_unchecked() | YES | Zero-cost success path | Fire-and-forget only |
try_lease_async_mut_unchecked() | YES | Zero-cost success path | Fire-and-forget only |
lease_async_mut() | NO | One clone() per operation | General async use |
try_lease_async_mut() | NO | One clone() per operation | General async use |
§Zero-Cost Operations (Use Always)
- Owned variants (
lease,lease_async,try_lease,try_lease_async): Compile to identical assembly as manual implementations - Sync mutable variants (
lease_mut,try_lease_mut): Minimal overhead beyond pointer operations - Unchecked async variants: Zero-cost success path, panic on cancellation
- Macros: Same cost as their underlying functions
§Operations with Runtime Cost
- Checked async mutable (
lease_async_mut,try_lease_async_mut): OneT::clone()per operation - Cost: O(size_of::
) time and memory - Trade-off: Safety vs. performance
- When acceptable: When cancellation safety justifies the clone cost
§Benchmark Considerations
- For
T: Copytypes, cloning is often free (stack copying) - For large
Ttypes, consider if the safety guarantee justifies the clone cost - Profile your specific use case - the clone cost may be negligible compared to async overhead
§Safety Guarantees
§Memory Safety
- All variants: Memory-safe under Rust’s definition
- No undefined behavior in any code path (safe or unsafe)
- Drop-correct: All values are properly dropped or returned
- Exception-safe: Panics don’t compromise memory safety
§Cancellation Safety
- Owned variants: Fully cancellation-safe (values are owned)
- Sync mutable: Safe if
catch_unwindis not used maliciously - Checked async mutable: Cancellation-safe via automatic restoration + explicit error handling
- Unchecked async: Panic on cancellation to prevent UB
§Thread Safety
- Send + Sync: All types that implement the bounds
- No internal mutability beyond what’s exposed
- Async variants: Compatible with tokio’s threading model
§The Clone Trade-off: Cancellation Safety vs. Zero-cost
§The Problem
When async operations can be cancelled (like tokio::select!, tokio::time::timeout), the future
might be dropped before completion. If the future has taken ownership of a value and started
modifying it, we need a way to restore the original state to prevent undefined behavior.
§The Solution: Clone-based Safety
Since Rust doesn’t provide general “undo” for arbitrary mutations, we clone the original value. When cancellation occurs:
- The future is dropped (potentially losing modified data)
- The
CancellationGuardautomatically restores the cloned original - No data loss, no UB, but at the cost of one clone per operation
§Zero-cost Alternative: Panic on Cancellation
The _unchecked variants take ownership without cloning, achieving true zero-cost… but panic
if cancelled. This prevents UB while maintaining performance, but requires that cancellation
is impossible in your use case.
§When to Choose Which
§Performance-First Choice
- Use zero-cost variants for all cases where they work (9/11 functions)
- Only use checked async mutable when you need cancellation safety AND are willing to pay the clone cost
§Safety vs. Performance Trade-off
- Use checked variants (
lease_async_mut) when: - Cancellation is possible (tokio::select!, timeout, etc.)
- You want graceful error handling with original values
- The clone cost is acceptable
- Use unchecked variants (
lease_async_mut_unchecked) when: - Cancellation is impossible (fire-and-forget tasks)
- You need truly zero-cost operation
- Panic on cancellation is acceptable
§Decision Guide
Zero-cost options (always prefer these):
lease(data, |owned| { /* transform */ })- Zero-costlease_async(data, |owned| async move { /* async work */ })- Zero-costlease_mut(&mut data, |owned| { /* mutate */ })- Zero-cost
Only when you need async mutable + cancellation safety:
lease_async_mut(&mut data, |owned| async move { /* cancellable work */ })- Non-zero cost (Clone required)
Only for fire-and-forget scenarios:
lease_async_mut_unchecked(&mut data, |owned| async move { /* no cancellation */ })- Zero-cost but dangerous
§Real-world Example with tokio::select!
# #[cfg(feature = "std")]
# async fn example() {
use lease_rs::lease_async_mut;
use tokio::select;
use tokio::time::{sleep, Duration};
let mut data = vec![1, 2, 3];
let result = select! {
res = lease_async_mut(&mut data, |mut v| async move {
sleep(Duration::from_secs(10)).await; // Long operation
v.push(4);
(v, Ok("completed")) // your error type
}) => res,
_ = sleep(Duration::from_millis(1)) => {
// Cancellation happened - data was automatically restored to [1,2,3]
// No panic, no error returned, no UB
return;
}
};
match result {
Ok(msg) => println!("Success: {}", msg),
Err(e) => println!("Your closure returned an error: {:?}", e),
}
# }§Error Handling Philosophy
§Explicit vs. Automatic
- Traditional APIs: Hide errors, restore state automatically
- Ownership Leasing: Force explicit error handling with cloned originals
§Why This Design?
Cancellation is silent and automatic. The CancellationGuard RAII guard ensures the
original value is restored if the future is cancelled, preventing undefined behavior
while keeping the API simple and intuitive.
§Error Types
- Result<R, E>: Direct return of your custom errors from the closure
- Cancellation is silent: Original value restored automatically via RAII guard
- Only user errors bubble up: No need to handle cancellation explicitly
§Common Patterns & Anti-patterns
§Good Patterns
use lease_rs::lease_async_mut;
async fn example() {
let mut data = vec![1, 2, 3];
// You control what gets left behind, even on error
let result = lease_async_mut(&mut data, |mut owned| async move {
if owned.is_empty() {
// On error, you control what value is left in the slot
owned.push(999); // Error state left behind
(owned, Err(Box::new(std::io::Error::new(std::io::ErrorKind::Other, "data was empty"))))
} else {
owned.push(4); // Success state left behind
(owned, Ok("done"))
}
}).await;
match result {
Ok(msg) => println!("Success: {}", msg),
Err(e) => println!("Error: {}, slot contains: {:?}", e, data),
}
}§Anti-patterns
use lease_rs::{lease_async_mut, lease_async_mut_unchecked};
async fn bad_example() {
let mut data = vec![1, 2, 3];
// DON'T: Ignore errors
let result: Result<(), ()> = lease_async_mut(&mut data, |owned| async move {
(owned, Ok(()))
}).await; // Error silently ignored!
// DON'T: Use unchecked when cancellation is possible
tokio::select! {
_ = lease_async_mut_unchecked(&mut data, |owned| async move {
(owned, ()) // Returns tuple, not Result
}) => {},
_ = tokio::time::sleep(tokio::time::Duration::from_millis(100)) => {}, // This will panic!
}
}§Performance Tips
§Maximize Zero-Cost Usage
- Prefer zero-cost functions (9/11 available) - they have no runtime overhead
- Use
lease_async_mut_uncheckedonly when cancellation is truly impossible - Avoid
lease_async_mutunless cancellation safety is required
§Optimizing Non-Zero-Cost Operations
- Batch operations to amortize clone costs across multiple uses
- Consider
Arc<T>ifTis expensive to clone but needs shared ownership - Use
T: Copytypes where possible (clone is free) - Profile first - async overhead often dominates clone costs
- Consider alternative architectures if clone cost is prohibitive
§Platform Support
- no_std: Full sync API available
- std + tokio: Full async API with cancellation safety
- WASM: Compatible (no tokio-specific code)
- Embedded: Works with allocation-free types
§Edge Cases & Robustness
§Fully Covered
- Panic inside closure/future (owned cases)
- Early return with
?(error propagation) !Unpintypes (owned, no pinning required)!Sendtypes (sync variants only)- Multi-tuple leasing (arbitrary nesting)
- Mutable reference leasing (no
Defaultbound) Result/Optionpropagation through closures- Const contexts (zero runtime cost)
- FFI handles (raw pointer safety)
- Drop-order verification (RAII compliance)
- Performance-critical paths (zero-overhead success)
§Limitations
- Async mutable operations require
T: Clone(unless using unchecked) - Unchecked variants panic on cancellation (by design)
- Cannot lease across thread boundaries (use channels instead)
- Macros require specific closure signatures
§Implementation Details
§Zero-cost Abstraction
- All success paths compile to identical assembly as manual implementations
#[inline(always)]ensures no function call overhead- Monomorphization eliminates trait dispatch
§Memory Layout
- No heap allocation in success paths
- Stack-only operations for owned types
- Minimal stack usage (similar to manual patterns)
§Drop Semantics
CancellationGuardensures cleanup on panic/cancellation- All resources properly managed via RAII
- Exception-safe even with malicious
catch_unwind
§Migration from Manual Patterns
§Before (manual, error-prone):
// Error-prone manual implementation (don't do this)
let mut data = vec![1, 2, 3];
let original = data.clone(); // Manual clone for error recovery
// Complex async error handling logic would go here...
// Easy to forget to restore `data` on error!§After (automatic, safe):
use lease_rs::lease_async_mut;
async fn safe_example() {
let mut data = vec![1, 2, 3];
// You control what gets left behind, even on error
let result: Result<(), Box<dyn std::error::Error + Send + Sync>> = lease_async_mut(&mut data, |mut owned| async move {
owned.push(4);
(owned, Ok(()))
}).await;
// On error, you choose what value is left in the slot
let result = lease_async_mut(&mut data, |mut owned| async move {
if owned.len() > 10 {
// Leave error state behind
owned.clear();
(owned, Err(Box::new(std::io::Error::new(std::io::ErrorKind::Other, "too large"))))
} else {
owned.push(5);
(owned, Ok("added"))
}
}).await;
}§Testing & Validation
The crate includes comprehensive tests covering:
- 31 unit tests (100% coverage)
- Async cancellation scenarios
- Panic safety
- Thread safety
- Performance regression prevention
- Edge case validation
All documentation examples are tested and guaranteed to compile.
Macros§
- lease_
async_ with std - Async lease macro for asynchronous operations.
- lease_
pinned_ async_ with std - Pinned async lease macro (for self-referential futures / !Unpin data).
- lease_
with - Convenience macro for leasing values with mutation.
- try_
lease_ async_ with std - Try-async-lease macro for fallible asynchronous operations.
- try_
lease_ with - Try-lease macro for fallible operations with Result propagation.
Functions§
- lease
- Leases full ownership of
valuetof.fmust return the (possibly transformed) value + result. - lease_
async std - Async lease - perfect for crossing any number of
.awaitpoints. - lease_
async_ mut std - Async mutable lease with explicit error handling and cancellation safety.
- lease_
async_ mut_ unchecked std - Async mutable lease - true zero-cost, panics on cancellation.
- lease_
mut - Leases ownership from a mutable reference (general
T, noDefaultbound). - lease_
pinned_ async std - Pinned async lease (for self-referential futures, requires
T: Unpin). - try_
lease std - Guard that restores the original value on cancellation. Fallible lease (owned value).
- try_
lease_ async std - Fallible async lease.
- try_
lease_ async_ mut std - Fallible async mutable lease with explicit error handling.
- try_
lease_ async_ mut_ unchecked std - Fallible async mutable lease - true zero-cost, panics on cancellation.
- try_
lease_ mut - Fallible mutable lease - always restores
Teven onErr.