Continuing the discussion from Passing Rust closures to C:
I created a module which allows converting closures to closures with a longer lifetime. I originally needed this to create a nicer interface when passing Rust closures to Lua (as I didn't like the requirement to make all Rust closures having a 'static
lifetime or the lifetime of the Lua virtual machine.
The trick is to have a scope handle, similar to what crossbeam
or rayon
do with their scoped threads.
Throughout the discussion in the above linked thread, I came to the conclusion that the mechanism may be of broader interest and not just be limited to (Lua) sandboxing. That's why I came up with an own module (that I would like to turn into a crate eventually).
For the context:
This is what I came up with, so far:
use std::marker::PhantomData;
use std::mem::transmute;
use std::sync::Arc;
pub struct Scope<'scope> {
arc: Arc<()>,
phantom: PhantomData<fn(&'scope ()) -> &'scope ()>,
}
pub fn scope<'scope, O, R>(run: O) -> R
where
O: FnOnce(&Scope<'scope>) -> R,
{
run(&Scope::<'scope> {
arc: Default::default(),
phantom: PhantomData,
})
}
impl<'scope> Scope<'scope> {
pub fn extend_mut<'long, A, R, E, F>(
&self,
mut error: E,
mut func: F,
) -> Box<dyn 'long + FnMut(A) -> R>
where
'long: 'scope,
A: 'long,
R: 'long,
E: 'long + FnMut(A) -> R,
F: 'scope + FnMut(A) -> R,
{
let weak = Arc::downgrade(&self.arc);
let func_or_error = Box::new(move |arg: A| -> R {
match weak.upgrade() {
Some(_) => func(arg),
None => error(arg),
}
});
unsafe {
transmute::<
Box<dyn 'scope + FnMut(A) -> R>,
Box<dyn 'long + FnMut(A) -> R>,
>(func_or_error)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::Mutex;
#[test]
fn test_in_scope() {
let errorstate: Arc<Mutex<i32>> = Arc::new(Mutex::new(0));
let clone = errorstate.clone();
let error = move |_| {
*clone.lock().unwrap() += 3;
};
let mut i: Box<i32> = Box::new(0);
scope(|s| {
let mut closure = s.extend_mut(error, |_| {
*i += 2;
});
closure(());
});
assert_eq!(*i, 2);
assert_eq!(*errorstate.lock().unwrap(), 0);
}
#[test]
fn test_out_of_scope() {
let errorstate: Arc<Mutex<i32>> = Arc::new(Mutex::new(0));
let clone = errorstate.clone();
let error = move |_| {
*clone.lock().unwrap() += 7;
};
let mut i: Box<i32> = Box::new(0);
let mut closure = scope(|s| {
let closure = s.extend_mut(error, |_| {
*i += 5;
});
closure
});
closure(());
drop(closure);
assert_eq!(*i, 0);
assert_eq!(*errorstate.lock().unwrap(), 7);
}
#[test]
fn test_out_of_scope_with_drop() {
let errorstate: Arc<Mutex<i32>> = Arc::new(Mutex::new(0));
let clone = errorstate.clone();
let error = move |()| {
*clone.lock().unwrap() += 13;
};
let mut i: Box<i32> = Box::new(0);
let mut closure = scope(|s| {
let closure = s.extend_mut(error, |_| {
*i += 11;
});
closure
});
assert_eq!(*i, 0);
drop(i);
closure(());
assert_eq!(*errorstate.lock().unwrap(), 13);
}
}
Output:
running 3 tests
test tests::test_out_of_scope ... ok
test tests::test_in_scope ... ok
test tests::test_out_of_scope_with_drop ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Caveat: Rust code currently can't be generic over function arity, which makes it less convenient (or more work for this hypothetical generic library) if the callback you want to "weaken" is expected to have more than one.
A workaround could be to pass a single tuple and to provide another closure which unwraps the tuples. Or the crate could provide methods like extend_mut_1arg_1retval
, extend_mut_1arg_2retvals
extend_mut_1arg
, extend_mut_2args
, etc. Neither option seems nice though.
In the process I realized that you're double-boxing your scoped closures, which you might want to avoid.
I kept that consideration in mind and managed to use only one indirection. See Playground example above, where the closure that performs the match
on the Arc
works directly with the provided closures, and the lifetime transmutation happens afterwards. Thanks for your hint!
Also many thanks to @Yandros for pointing out the problem of covariance/invariance regarding scope handles:
There is an importance nuance, here: that
'beyond_cleanup
lifetime is used as a lower-bound for the area of owned-usability off
(the point of all this design). It's important that such lower-bound not be allowed to shrink[1]. The official phrasing for this property is that we don't wanttype ScopeHandle<'beyond_cleanup>
to be covariant in'beyond_cleanup
. It should, at best, be contravariant (lower bound is allowed to grow), and, in practice, invariant (neither cov. nor contra.) is just saner: let's not allow it to do something we don't need it to be allowed to do (even if it would be harmless in this instance). Hence thePhantomInvariant
I hope I adequately handled this problem by including a PhantomData<fn(&'scope ()) -> &'scope ()>
in my scope handle.
I would be happy about a short review and comments on whether the unsafe
in my above code is sound.