Skip to main content

Crate lease_rs

Crate lease_rs 

Source
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:

  1. Zero-overhead by default - Operations that can be zero-cost are zero-cost
  2. Memory safety first - Never compromise Rust’s safety guarantees
  3. 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

AspectBorrowing (&mut T)Leasing (lease())
OwnershipReference onlyFull ownership transfer
LifetimeBorrow checker enforcedExplicit scope control
Async/.awaitCannot cross .awaitFull ownership across .await
ClosuresLimited by lifetimesMove semantics
SafetyCompile-time guaranteesRuntime safety + compile-time
PerformanceZero-costZero-cost
FlexibilityHigh for simple casesHigh 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

ApproachWhen it worksLimitations vs lease
Manual ptr::read/write + ManuallyDropSync onlyVery verbose, easy to get UB on panic/cancellation
std::mem::replaceSimple casesNo async support
tokio::sync::Mutex / Arc<Mutex<T>>Shared accessRuntime locking overhead, changes semantics
Channels (mpsc, crossbeam)Thread boundariesDifferent model, more allocation
scopeguard / custom RAII guardManual cancellation safetyYou 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 (require T: Clone)

§API Overview

The API is divided into four main categories:

§Owned Variants (Zero-cost)

  • lease() / lease_async() - Transfer owned values across closures/futures
  • try_lease() / try_lease_async() - With error propagation

§Mutable Reference Variants

  • lease_mut() / try_lease_mut() - Sync mutable reference leasing
  • lease_async_mut() / try_lease_async_mut() - Async mutable reference leasing with explicit error handling
  • lease_async_mut_unchecked() / try_lease_async_mut_unchecked() - Zero-cost async mutable (panics on cancellation)

§Convenience Macros

  • lease_with!() - Ergonomic syntax for common patterns
  • try_lease_with!() - With error propagation
  • lease_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): Use lease*() functions
  • Mutable reference (&mut T): Use lease_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)
  • Cancellation impossible (fire-and-forget): Use unchecked variants
    • With errors: try_lease_async_mut_unchecked()
    • Without errors: lease_async_mut_unchecked() (zero-cost)

§Quick Reference Table

Data TypeAsync?Errors?Cancellation RiskFunction
T (owned)NoNoN/Alease()
T (owned)NoYesN/Atry_lease()
T (owned)YesNoN/Alease_async()
T (owned)YesYesN/Atry_lease_async()
&mut TNoNoN/Alease_mut()
&mut TNoYesN/Atry_lease_mut()
&mut TYesNoSafelease_async_mut()
&mut TYesYesSafetry_lease_async_mut()
&mut TYesNoUnsafelease_async_mut_unchecked()
&mut TYesYesUnsafetry_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

FunctionZero-Cost?Cost DetailsWhen to Use
lease()YESTruly zero-cost, monomorphizedAlways
lease_async()YESZero-cost, same as manual asyncAlways
try_lease()YESZero-costAlways
try_lease_async()YESZero-costAlways
lease_mut()YESNear zero-cost (ptr ops only)Always
try_lease_mut()YESNear zero-costAlways
lease_async_mut_unchecked()YESZero-cost success pathFire-and-forget only
try_lease_async_mut_unchecked()YESZero-cost success pathFire-and-forget only
lease_async_mut()NOOne clone() per operationGeneral async use
try_lease_async_mut()NOOne clone() per operationGeneral 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): One T::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: Copy types, cloning is often free (stack copying)
  • For large T types, 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_unwind is 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:

  1. The future is dropped (potentially losing modified data)
  2. The CancellationGuard automatically restores the cloned original
  3. 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-cost
  • lease_async(data, |owned| async move { /* async work */ }) - Zero-cost
  • lease_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_unchecked only when cancellation is truly impossible
  • Avoid lease_async_mut unless cancellation safety is required

§Optimizing Non-Zero-Cost Operations

  • Batch operations to amortize clone costs across multiple uses
  • Consider Arc<T> if T is expensive to clone but needs shared ownership
  • Use T: Copy types 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)
  • !Unpin types (owned, no pinning required)
  • !Send types (sync variants only)
  • Multi-tuple leasing (arbitrary nesting)
  • Mutable reference leasing (no Default bound)
  • Result/Option propagation 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

  • CancellationGuard ensures 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_withstd
Async lease macro for asynchronous operations.
lease_pinned_async_withstd
Pinned async lease macro (for self-referential futures / !Unpin data).
lease_with
Convenience macro for leasing values with mutation.
try_lease_async_withstd
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 value to f. f must return the (possibly transformed) value + result.
lease_asyncstd
Async lease - perfect for crossing any number of .await points.
lease_async_mutstd
Async mutable lease with explicit error handling and cancellation safety.
lease_async_mut_uncheckedstd
Async mutable lease - true zero-cost, panics on cancellation.
lease_mut
Leases ownership from a mutable reference (general T, no Default bound).
lease_pinned_asyncstd
Pinned async lease (for self-referential futures, requires T: Unpin).
try_leasestd
Guard that restores the original value on cancellation. Fallible lease (owned value).
try_lease_asyncstd
Fallible async lease.
try_lease_async_mutstd
Fallible async mutable lease with explicit error handling.
try_lease_async_mut_uncheckedstd
Fallible async mutable lease - true zero-cost, panics on cancellation.
try_lease_mut
Fallible mutable lease - always restores T even on Err.