I'm still having a hard time following through your post. I haven't worked with scopeguard
yet, but I understand what it's supposed to do. But where I can't follow is what a PhantomInvariant
is, for example.
I also don't understand why there is an impl Drop
within a function body in the "Final signature".
Anyway, I tried to put those pieces of your post which I did understand together, and this is what I came up with:
struct Machine {}
struct ClosureContext<'a> {
// `on_drop` must be dropped first
on_drop: Vec<Box<dyn FnMut() + 'static>>,
_machine: &'a Machine,
}
impl<'a> Drop for ClosureContext<'a> {
fn drop(&mut self) {
for f in &mut self.on_drop {
f();
}
}
}
impl<'a> ClosureContext<'a> {
fn push_closure<F>(&mut self, closure: F)
where
F: 'a + FnMut(),
{
let raw = Box::into_raw(Box::new(closure))
as *mut std::os::raw::c_void;
println!("Closure {:?} goes to C land", raw);
self.on_drop.push(Box::new(move || {
println!("Closure {:?} gets invalidated", raw);
}));
}
}
impl Machine {
fn new_closure_context<'a, O>(&'a self, once: O)
where
for<'b> O: FnOnce(&'b mut ClosureContext<'a>),
{
let mut context = ClosureContext::<'a> {
_machine: self,
on_drop: vec![],
};
once(&mut context);
}
fn run(&self) {
println!("Running (and possibly executing closures, unless invalidated)");
}
}
fn main() {
let machine = Machine {};
let h: i32 = 0;
let mut i: i32 = 0;
let mut j: i32 = 0;
machine.new_closure_context(|ctx| {
ctx.push_closure(|| {
i += 1;
});
ctx.push_closure(|| {
j += 1;
});
// allowed:
drop(h);
// no effect, as ctx is just a mut reference:
std::mem::forget(ctx);
// not allowed:
//drop(i);
machine.run();
});
machine.run();
}
(Playground)
Output:
Closure 0x55affe0b69d0 goes to C land
Closure 0x55affe0b6a60 goes to C land
Running (and possibly executing closures, unless invalidated)
Closure 0x55affe0b69d0 gets invalidated
Closure 0x55affe0b6a60 gets invalidated
Running (and possibly executing closures, unless invalidated)
I had to use an HRTB, as you mentioned too. But I did not use any extra lifetime parameter to new_closure_context
. Instead I just use 'a
from &'a self
and hope there is no problem with that?
I hope I (mostly) understood things right by not handing out ClosureContext
and thus not allowing to mem::forget
it. But I wouldn't be surprised if I got it all wrong .
Perhaps it's more clear to write it this way:
fn new_closure_context<'a, 'b, O>(&'a self, once: O)
where
'a: 'b,
for<'c> O: FnOnce(&'c mut ClosureContext<'b>),
This would indicate that the lifetime of the ClosureContext
does not depend in any way on the lifetime of &self
. But I believe in practice there is no difference to my approach where I used a single lifetime. Yet when I turn it into a trait, implementors of the trait will have to follow whichever signature I choose here, so not sure what's best to pick.
I'm still feeling confused to introduce 'b
(or 'beyond_cleanup
in your example) just to create a lifetime that lasts until the function returned. But it kinda makes sense. I think.
Update:
I finally ended up with the following API interface (for now). Note that in my case, the Machine
isn't a struct but a trait, because I intend to support an interface for multiple (different) scripting languages.
/// Ability of [`Machine`]s to call provided callbacks that live as long as the machine lives
pub trait Callback<'a>: Machine<'a> {
/// Create a [`Machine::Datum`] representing a callback (which invokes the `func` closure)
fn callback<F>(&'a self, func: F) -> Result<Self::Datum, MachineError>
where
F: 'a + FnMut(Vec<Self::Datum>) -> Result<Vec<Self::Datum>, String>;
}
/// Ability of [`Machine`]s to call provided callbacks that have a limited lifetime
pub trait ScopedCallback<'a>: Machine<'a> {
/// Scope handle
type Scope<'b>: CallbackScope<'a, 'b, Self>;
/// Create scope handle that allows creation of callbacks with limited lifetime
///
/// The scope handle will not be returned but a reference to the handle is
/// passed to the given closure `once`.
fn callback_scope<'b, O, R>(&'b self, once: O) -> R
where
O: FnOnce(&Self::Scope<'b>) -> R;
}
/// Scope that allows creation of callbacks with limited lifetime
pub trait CallbackScope<'a, 'b, M: Machine<'a> + ?Sized> {
/// Create a [`Machine::Datum`] representing a callback (which invokes the `func` closure)
///
/// After [`ScopedCallback::Scope`] has been dropped
/// (i.e. after [`ScopedCallback::callback_scope`] returned),
/// calling the callback will result in a runtime error ([`MachineError`]).
fn callback<F>(&self, func: F) -> Result<M::Datum, MachineError>
where
F: 'b + FnMut(Vec<M::Datum>) -> Result<Vec<M::Datum>, String>;
}
Edit: Added lifetime 'a
to self
parameter in Callback::callback
.
The associated type Machine::Datum
is a value to be used in the scripting language, e.g. a Lua value such as a Lua string or Lua function (or closure).
- The
Callback
trait allows converting a long-living Rust closure (i.e. a Rust closure that lives at least as long as the Machine
) into a Machine::Datum
.
- The
ScopedCallback
trait allows creating a scope (some associated type which implements CallbackScope
) which allows converting Rust closures into a Machine::Datum
even if the Rust closures live for a short time only (i.e. if they are only valid until the once
closure, which gets a reference to the scope handle, returns).
Note that I decided against using a dedicated lifetime parameter 'c
, such as:
fn callback_scope<'b, 'c, O, R>(&'b self, once: O) -> R
where
'b: 'c,
O: FnOnce(&Self::Scope<'c>) -> R;
Using an extra 'c
seems to make the interface more complex without any advantages (but please correct me if I'm wrong). Instead, I use a single lifetime 'b
for both &'b self
and &Self::Scope<'b>
. Basically 'b
can be any lifetime that is at least valid until callback_scope
returns. If the reference to self
actually lives longer, that should not be a problem, due to subtyping.
Also note that I ommitted the explicit HRTBs (to make the code a bit more compact). If I understand it right, they are still there (but implicit).
I will have to see if the interface is feasible. I have only implemented Callback
yet, but not ScopedCallback
. I might or might not face further problems when I actually try to provide implementations. Plus, the invalidation of an already created Lua closure (when the Rust closure and/or the scope handle goes out of scope) will add some complexity.
I'd be happy about some feedback regarding my API draft and I hope it's clear now how it's supposed to work.