I'm doing a hobby project in Rust and I have used a couple of unsafe
tricks that I'd like to share.
Let's imagine the following:
Normally, a lock guard has a lifetime parameter (e.g. struct MutexGuard<'a, T>
with lifetime parameter 'a
) that is used to guarantee that the lock guard cannot outlive the lock.
The way this guarantee is enforced prevents us from sending an owned lock guard across a channel.
Here's an example of a compilation error:
error[E0597]: `buf` does not live long enough
--> src/main.rs:16:17
|
13 | let buf = Arc::new(Buffer {
| --- binding `buf` declared here
...
16 | let guard = buf.data.blocking_lock();
| ^^^ borrowed value does not live long enough
...
19 | guard,
| ----- this usage requires that `buf` is borrowed for `'static`
20 | });
21 | }
| - `buf` dropped here while still borrowed
for the following code:
fn producer(tx: Sender<LockedBuffer>) {
let buf = Arc::new(Buffer {
data: Mutex::new(vec![1,2,3]),
});
let guard = buf.data.blocking_lock();
tx.send(LockedBuffer {
buf: buf.clone(),
guard,
});
}
struct LockedBuffer {
buf: Arc<Buffer>,
guard: MutexGuard<'static, Vec<u8>>,
}
struct Buffer {
data: Mutex<Vec<u8>>,
// ....
}
The borrow check is preventing us from doing something potentially unsafe, as it does not understand that we're packing buf
and guard
into a single struct, and guard
references inside buf
.
Rust's borrow check is just a local analysis, so it's natural that it fails to see that.
We can easily work around by extending the lifetime to 'static
with std::mem::transmute
:
let guard = buf.data.blocking_lock();
// SAFETY: the `guard`'s referent `buf` is kept alive for the lifetime of `guard`,
// because it's stored in `LockedBuffer`, and `LockedBuffer` is not destructured.
let guard: MutexGuard<'static, _> = unsafe { std::mem::transmute(guard) };
tx.send(LockedBuffer {
buf: buf.clone(),
guard,
});
The trick works because Rust uses lifetimes only for borrow checking, and lifetimes are completely erased after borrow-checking. That means that as long as we don't anything bad at runtime, having a “wrong” lifetime does no harm.
In general, transmute
can be dangerous, but in this case its use is sound, since we take additional measures to ensure that the lifetime constrains between a lock and its guard are satisfied.
Note that there is a yoke
crate that uses a similar technique (but is much more advanced and safe).
Normally, Mutex
es and RwLock
s in Rust act as containers that wrap the protected resource.
This design leads to an ergonomic and safe API, but can be restrictive at times.
I have a case where I have a node-based data structure and I want to use a single lock to control access to set of fields of all nodes instead of having a mutex inside of each node.
This access pattern can be implemented with UnsafeCell
, which requires some unsafe code around it.
In this specific case, the use of UnsafeCell
is pretty hard to make safe in general.
But it is still useful and can be sound.
It works like this:
struct Buffers {
lock: Mutex<BuffersLockToken>,
buffers: Vec<Buffer>,
}
struct BuffersLockToken;
UnsafeCell
around the protected data:
struct Buffer {
flags: UnsafeCell<u32>,
}
Sync
for this struct (by default, UnsafeCell
is not shareable across threads):
unsafe impl Sync for Buffer {}
impl Buffer {
// SAFETY: `flags_mut` should not be called multiple times on the same `Buffer` with the same `lock_evidence`
unsafe fn flags_mut<'a>(&'a self, _lock_evidence: &'a MutexGuard<'a, BuffersLockToken>) -> &'a mut u32 {
// SAFETY: the access is protected by the lock on owning `Buffers`
// this is not completely safe, but safe enough.
unsafe {
&mut *self.flags.get()
}
}
}
let buffers = Buffers { ... };
let guard = buffers.lock.blocking_lock();
*buffers.buffers[0].flags_mut(&guard) = 123;
Even though this usage requires unsafe at the call site (as the user of flags_mut
must ensure some of the safety invariants), it is still totally worth it in my project.