[][crates.io]
[][libs.rs]
[][documentation]
[][rust-version]
[][license]
[crates.io]: https://crates.io/crates/borrow_mutex
[libs.rs]: https://lib.rs/crates/borrow_mutex
[documentation]: https://docs.rs/borrow_mutex
[rust-version]: https://www.rust-lang.org
[license]: https://github.com/darsto/borrow_mutex/blob/master/LICENSE
# BorrowMutex
**Very initial version!** Use with caution
[`BorrowMutex`] is an async Mutex which does not require wrapping the target
structure. Instead, a `&mut T` can be lended to the mutex at any given time.
This lets any other side borrow the `&mut T`. The mutable ref is borrow-able
only while the lender awaits, and the lending side can await until someone
wants to borrow. The semantics enforce at most one side has a mutable reference
at any given time.
This lets us share any mutable object between distinct async contexts
without [`Arc`]<[`Mutex`]> over the object in question and without relying
on any kind of internal mutability. It's mostly aimed at single-threaded
executors where internal mutability is an unnecessary complication.
Still, the [`BorrowMutex`] is Send+Sync and can be safely used from
any number of threads.
The most common use case is having a state handled entirely in its own
async context, but occasionally having to be accessed from the outside -
another async context.
Since the shared data doesn't have to be wrapped inside an [`Arc`],
it doesn't have to be allocated on the heap. In fact, BorrowMutex does not
perform any allocations whatsoever. The
[`tests/borrow_basic.rs`](https://github.com/darsto/borrow_mutex/blob/master/tests/borrow_basic.rs)
presents a simple example where *everything* is stored on the stack.
## Safety
The API is unsound when futures are forgotten ([`core::mem::forget()`]).
For convenience, none of the API is marked unsafe.
See [`BorrowMutex::lend`] for details.
Hopefully the unsound code could be prohibited in future rust versions
with additional compiler annotations.
## Example
```rust
use borrow_mutex::BorrowMutex;
use futures::FutureExt;
struct TestObject {
counter: usize,
}
let mutex = BorrowMutex::<16, TestObject>::new();
let f1 = async {
// try to borrow, await, and repeat until we get an Err.
// The Err can be either:
// - the mutex has too many concurrent borrowers (in this example we
// have just 1, and the max was 16)
// - the mutex was terminated - i.e. because the lending side knows it
// won't lend anymore
// We eventually expect the latter here
while let Ok(mut test) = mutex.request_borrow().await {
test.counter += 1; // mutate the object!
println!("f1: counter: {}", test.counter);
drop(test);
// `test` is dropped, and so the mutex.lend().await on the
// other side returns and can use the object freely again.
// we'll request another borrow in 100ms
smol::Timer::after(std::time::Duration::from_millis(100)).await;
}
};
let f2 = async {
let mut test = TestObject { counter: 1 };
// local object we'll be sharing
loop {
if test.counter >= 20 {
break;
}
// either sleep 200ms or lend if needed in the meantime
futures::select! {
_ = smol::Timer::after(std::time::Duration::from_millis(200)).fuse() => {
if test.counter < 10 {
test.counter += 1;
}
println!("f2: counter: {}", test.counter);
}
_ = mutex.wait_to_lend().fuse() => {
// there's someone waiting to borrow, lend
mutex.lend(&mut test).unwrap().await
}
}
}
mutex.terminate().await;
};
futures::executor::block_on(async {
futures::join!(f1, f2);
});
```
Both futures should print interchangeably. See `tests/borrow_basic.rs` for
a full working example.
# What if Drop is not called?
Unfortunately, **Undefined Behavior**. With [`core::mem::forget()`] or similar
called on [`LendGuard`] we can make the borrow checker believe the lended
`&mut T` is no longer used, while in fact, it is:
```rust,should_panic
# use borrow_mutex::BorrowMutex;
# use futures::Future;
# use futures::task::Context;
# use futures::task::Poll;
# use core::pin::pin;
struct TestStruct {
counter: usize,
}
let mutex = BorrowMutex::<16, TestStruct>::new();
let mut test = TestStruct { counter: 1 };
let mut test_borrow = pin!(mutex.request_borrow());
let _ = test_borrow
.as_mut()
.poll(&mut Context::from_waker(&futures::task::noop_waker()));
let mut t1 = Box::pin(async {
mutex.lend(&mut test).unwrap().await;
});
let _ = t1
.as_mut()
.poll(&mut Context::from_waker(&futures::task::noop_waker()));
std::mem::forget(t1);
// the compiler thinks `test` is no longer borrowed, but in fact it is
let Poll::Ready(Ok(mut test_borrow)) = test_borrow
.as_mut()
.poll(&mut Context::from_waker(&futures::task::noop_waker()))
else {
panic!();
};
// now we get two mutable references, this is strictly UB
test_borrow.counter = 2;
test.counter = 6;
assert_eq!(test_borrow.counter, 2); // this fails
```
Similar Rust libraries make their API unsafe exactly because of this reason -
it's the caller's responsibility to not call [`core::mem::forget()`] or similar
([async-scoped](https://docs.rs/async-scoped/0.9.0/async_scoped/struct.Scope.html#method.scope))
However, this Undefined Behavior is really difficult to trigger in regular
code. It's hardly useful to call [`core::mem::forget()`] on a future.
[`Arc`]: std::sync::Arc
[`Mutex`]: std::sync::Mutex