Hi!
The context of this post is this PR to gtk-rs/glib.
Below is the short backstory and a minimal testcase that shows the problems.
In glib there is a generic Value
type that acts as a container for values of various types, with type checks happening at runtime. Kind of like a Box<std::any::Any>
but using glib's type system.
The types that can be retrieved from these Value
s all implement the FromValue
trait. The trait is responsible for checking the type and then extracting the value if it is allowed. This trait is implemented for e.g. String
, &str
, bool
, i32
, etc..
The main complication of this trait is that it has a lifetime parameter. This is necessary to allow borrowing of the contained value as well as getting owned values. For example the Value
might contain a string: then you can either get a String
from it (an owned copy) or a &str
(borrowing the original value). If this can be achieved differently then that's also acceptable as a solution to the more general problem here.
Now glib has implements a signal/slots system that allows users to connect closures as signal handlers to specific signals. Every specific signal has a specific signature (parameter types) and return type, and those have to match up.
Right now this is achieved via closures of the form Fn(&[Value]) -> Option<Value>
, i.e. it is the users job to retrieve values of the correct types from the Value
s and return a Value
of the correct type or None
in case of ()
. This all works fine, is safe but is not very convenient. Type mismatches are panics at the time the signal handler is called.
The goal now is to allow passing closures that take an arbitrary number of parameters, each of them implementing the FromValue
trait, and having the retrieval of the concrete value types happen automatically. A secondary goal is to allow type-checking at the time the signal handler is connected/added, but that's a trivial aspect and not really of importance for the problem.
This also works fine via a proc macro: glib::closure
. That macro is doing exactly what you would expect, no magic in there.
As proc macros are not very nice (e.g. you don't get rustfmt
to work on the macro's body), the next step now is to implement the same thing statically without macros.
The PR contains a first version of that, but that has the problem that it only works on owned values (i.e. String
but not &str
) because it requires for<'a> FromValue<'a>
for each of the closure parameters. This is of course not optimal and is also not required by the macro I mentioned before.
I tried getting rid of this constraint but wasn't able to match up the lifetimes in a way that it actually compiles.
Below is a minimal testcase of the whole setup, already with some more lifetime parameters than in the PR. However on the connect()
function it still requires a for<'a> SignalClosure<'a>
, which still constrains the whole setup to owned values.
Does anybody have any suggestions how to make the whole thing work with as little constraints as the macro has? It's acceptable to change any of the traits in any way necessary.
/// Generic value container
struct Value;
/// Retrieve/borrow value from container
trait FromValue<'a>: Sized {
fn from_value(value: &'a Value) -> Option<Self>;
}
/// Gets an owned `String` out of the value, i.e. a copy.
impl<'a> FromValue<'a> for String {
fn from_value(value: &'a Value) -> Option<Self> {
todo!();
}
}
/// Borrows a `&str` from the value. Lifetime is the same as the `Value`.
impl<'a> FromValue<'a> for &'a str {
fn from_value(value: &'a Value) -> Option<Self> {
todo!();
}
}
/// Trait to convert return values.
trait ToClosureReturnValue {
fn to_closure_return_value(&self) -> Option<Value>;
}
impl ToClosureReturnValue for () {
fn to_closure_return_value(&self) -> Option<Value> {
None
}
}
impl<'a> ToClosureReturnValue for &'a str {
fn to_closure_return_value(&self) -> Option<Value> {
Some(Value)
}
}
/// Trait implemented on closures that take arguments that are `FromValue` and return a
/// `ToClosureReturnValue`.
trait SignalClosure<'a, A> {
type Output: ToClosureReturnValue;
fn call(&self, args: &'a [Value]) -> Option<Value>;
}
/// Example implementation for one argument.
impl<'a, F: Fn(A1) -> R, A1: FromValue<'a>, R: ToClosureReturnValue> SignalClosure<'a, (A1,)>
for F
{
type Output = R;
fn call(&self, args: &'a [Value]) -> Option<Value> {
let a1 = <A1 as FromValue>::from_value(&args[0]).unwrap();
let res = self(a1);
res.to_closure_return_value()
}
}
struct Object;
impl Object {
fn connect_with_values<F: Fn(&[Value]) -> Option<Value> + 'static>(&self, func: F) {
todo!()
}
fn connect<F: for<'a> SignalClosure<'a, A> + 'static, A>(&self, func: F) {
self.connect_with_values(move |args: &[Value]| func.call(args));
}
}
fn main() {
let obj = Object;
obj.connect(|s: String| {});
// This should compile
obj.connect(|s: &str| {});
// This shouldn't compile
obj.connect(|s: &'static str| {});
// This should compile
obj.connect(|s: &str| s);
// This should compile
obj.connect(|s: &str| "123");
// This shouldn't compile as the `&str` must be bound to the `&Value` it is borrowed from
obj.connect(|s: &'static str| {});
}