Imagine:
- a state machine that transitions from one state to the next by value.
- some external API that forces you to use the state machine somewhere you can only access it through a mutable reference
&mut Machine
.
In order to transition the states by value you can wrap them in an Option
:
struct Machine {
state: Option<State>,
}
Now we can do:
// machine: &mut Machine
machine.state = Some(machine.state.take().unwrap().transition());
That works, but what doesn't sit right with me is the fact that the option should never be None
unless we caught and recovered from a panic in the transition
method.
I found a library called takeable but it's API allows take
and takeable-option which is fairly bare.
I want to limit the API to transitioning and borrowing. If we do that, I think we can optimize the case where panic = "abort"
by duplicating the held value temporarily.
Here is a short version of the code:
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub enum Transitionable<T> {
Ok(T),
#[cfg(not(panic = "abort"))]
Poisoned,
}
impl<T> Transitionable<T> {
pub fn new(value: T) -> Self {
Self::Ok(value)
}
pub fn try_into_inner(transitionable: Self) -> Result<T, PoisonError> {
match transitionable {
Transitionable::Ok(value) => Ok(value),
#[cfg(not(panic = "abort"))]
Transitionable::Poisoned => Err(PoisonError {}),
}
}
pub fn try_transition<F: FnOnce(T) -> T>(
transitionable: &mut Self,
f: F,
) -> Result<&mut Self, PoisonError> {
#[cfg(not(panic = "abort"))]
{
let value = match std::mem::replace(transitionable, Self::Poisoned {}) {
Self::Ok(value) => Ok(value),
Self::Poisoned => return Err(PoisonError {}),
}?;
*transitionable = Self::Ok(f(value));
}
#[cfg(panic = "abort")]
{
// SAFETY: Duplicating the value is safe because the process aborts if the transition
// function panics.
unsafe {
// TODO: Determine if we need to mess with ManuallyDrop, perhaps transitionable::Ok should
// contain a ManuallyDrop<T>.
let Self::Ok(value) = std::ptr::read(transitionable);
std::ptr::write(transitionable, Self::Ok(f(value)));
}
}
Ok(transitionable)
}
}
#[cfg(test)]
mod test {
use std::panic::catch_unwind;
use std::panic::AssertUnwindSafe;
use super::*;
#[test]
fn transition_works() {
struct A;
let mut t = Transitionable::new(A);
Transitionable::transition(&mut t, |a: A| a);
}
}
I know the code needs to be documented, more tests, and I can mark some things #[inline]
and #[track_caller]
. The full code implements (try_)get(_mut)
and Deref(Mut)
.
The main questions I have are:
- did I miss an existing library? (probably)
- is the
panic = "abort"
code sound? - how can run tests with
panic = "abort"
, I'm gettingwarning: `panic` setting is ignored for `test` profile
.