embassy-sync, lock-api mutex compat
I spent a bunch of time today trying to figure out why embassy
(an embedded
async runtime in Rust) forked the RawMutex
and Mutex
code from
lock_api
. This makes it incompatible
with foreign types implementing RawMutex
, such as flipperzero
's
FuriMutex
.
I wrote up a draft GitHub issue as I figured this out, thinking I had solved the issue. That appears below — at the end of this post, I explain what I actually figured out. Honestly, I'm mostly making this a blog post to preserve this issue that I spent several hours researching.
Original (draft) post
I'm doing a bit of work on the Flipper Zero, the rust
crate for which provides a
Mutex
that implements
lock_api::RawMutex
.
I have some previous firmware functionality written against embassy
that I'm
porting to the Flipper, and I started looking into using that Mutex
type to
back an embassy_sync::Mutex
for async support.
I noticed that embassy_sync::blocking
is a fork of lock_api
with a scoped
lock()
(rather than lock + unlock). I patched my embassy
fork to replace this with
lock_api
and haven't noticed any adverse effects -- all the embassy_sync
tests pass and empirically, embassy_sync::Mutex
works as expected.
Looking at #3175 and this
gist that
was linked there, the problem they're solving appears to be essentially about
reentrancy wrt. critical sections. For the sake of argument, let's consider an
impl of lock_api::RawMutex
for CriticalSectionRawMutex
that disallows
reentrancy (this is essentially what I have in my patch):
unsafe
(Notably, this design differs from the existing CriticalSectionRawMutex
in
that I'm tracking the acquire/release state explicitly rather than using the
closure form in critical_section::with
.)
As far as I can tell, this has the properties we want — a single
CriticalSectionRawMutex
isn't reentrant (i.e. we can't re-lock
the same
mutex without first unlocking it), but the "global reentrancy" of
critical_section
is preserved (multiple mutexes can be taken at once). I.e.,
this doesn't work (as desired):
let mutex = new;
mutex.lock;
mutex.lock; // spins forever
But this does:
let mutex1 = new;
let mutex2 = new;
mutex1.lock;
mutex2.lock;
Resolution
This actually doesn't satisfy the requirements for critical_section
,
specifically because it has the potential to violate nesting. While
the above example will work because the underlying RestoreState
s will
be released in the correct nesting order, this is trivially violated:
let mutex1 = new;
let mutex2 = new;
mutex1.lock;
mutex2.lock;
// Wrong order!
mutex1.unlock;
mutex2.unlock;
This is the critical issue with this approach that requires
scoped_mutex
.