This is my solution; it's not pretty but it works and passes the miri test. The general idea is we bundle a Mutex<()> and an optional MutexGuard for that mutex. I did not add proper handling of errors or naming to match the standard.
The structs:
struct MutexContainer {
mutex: Mutex<()>,
guard: Cell<Option<MutexGuard<'static, ()>>>,
}
pub struct OpaqueMutex {
_data: [usize;0],
}
Note that OpaqueMutex is simply an opaque handle to avoid leaking the internals of the MutexContainer, as is best practice during ffi. The key thing with this design is that the struct cannot move if it holds a guard.
Mutex creation:
pub unsafe extern "C" fn create_mutex(mutex: *mut *mut OpaqueMutex) -> libc::c_ulong {
*mutex = Box::into_raw(Box::new(
MutexContainer {
mutex: Mutex::new(()),
guard: Cell::new(None),
},
))
.cast();
0
}
Nothing fancy going on here, just leaking the mutex on the heap.
Mutex locking:
pub unsafe extern "C" fn lock_mutex(mutex: *mut OpaqueMutex) -> libc::c_ulong {
let container:&MutexContainer = &*mutex.cast();
let lock:MutexGuard<'static, _> = mem::transmute(container.mutex.lock().unwrap());
container.guard.set(Some(lock));
0
}
Here I use mem::transmute to lengthen the lifetime of the lock and store the result. The transmute is safe because we ensure that we don't move its parent mutex and we don't let it outlive the mutex here or elsewhere. The mutex will remain locked until the guard is dropped.
Mutex unlocking:
pub unsafe extern "C" fn unlock_mutex(mutex: *mut OpaqueMutex) -> c_ulong{
let container:&MutexContainer = &*mutex.cast();
if container.mutex.try_lock().is_ok(){
return !0; //can't unlock an unlocked mutex
} else {
container.guard.set(None);
}
0
}
Here we use the nonblocking try_lock to determine if the mutex is locked. If try_lock succeeds that means the mutex is unlocked and we return an error. Otherwise, we drop the guard by overwriting it with None.
Mutex drop:
pub unsafe extern "C" fn drop_mutex(mutex: *mut OpaqueMutex) -> c_ulong{
unlock_mutex(mutex);
drop(Box::from_raw(mutex.cast::<MutexContainer>()));
0
}
Here we ensure the mutex is unlocked to prevent a dangling reference by the guard and then reclaim the memory by reboxing the pointer.
Full playground link plus test harness.
Disclaimer: while this code passes miri, it is far from production-ready: there is basically no error handling. You should make sure that errors are handled robustly and sanity checks (like null and alignment checks) are made, especially since you are implementing a crypto library.
edit: the example main program invoked undefined behavior by erroneously locking the mutex outside of the new threads unlocking them inside the threads, rather than locking the mutex within each thread and unlocking them when done. Not sure why I thought that was a good idea.