Is the following proposition true?
Proposition 1: Assume a type T<'l> parameterized on a lifetime. If T<'a>: Send for some lifetime 'a, then T<'l>: Send for all lifetimes 'l.
How about the following weaker version?
Proposition 2: Assume a type F: AsyncFnMut. If F::CallRefFuture<'a>: Send for some lifetime 'a, then F::CallRefFuture<'l>: Send for all lifetimes 'l.
Or the following even weaker version?
Proposition 3: Although proposition 2 may not be true in general, e.g. for hand-rolled implementations of AsyncFnMut that do weird things, it is true of all types F that are associated with an actual syntactic closure (i.e. something spelled as async (move)? |...| { ... }).
In all cases I am interested in counterexamples, ideally non-contrived ones, but contrived too if that's all that exists.
If you're interested in why I want to know this, I'm looking for a workaround for this issue, where it's difficult/impossible for an API to require the caller to provide a thread-safe AsyncFnMut without restricting to only 'static closures. I believe any of the propositions above would allow me to do the following:
-
Define my own trait for "is AsyncFnMut, and the returned future is Send for all lifetimes with which self can be borrowed".
I believe this trait could be used as a bound without triggering the issue I'm working around, because no HRTB is required, as the trait builds in its own quantification over lifetimes.
-
Let users safely access an implementation of this trait by calling a macro that checks that the returned future is Send for one particular lifetime.
It is possible to have a type that has a weird Send implementation like this.
struct Thing<'a>(&'a i32, *mut i32);
unsafe impl Send for Thing<'static> {}
This can also affect closures, since closures inherit auto traits from whatever variables are captured.
See also Array and Vec's Clone specialization is maybe unsound with conditionally Copy types. · Issue #132442 · rust-lang/rust · GitHub.
1 Like
Thanks, yes, makes sense. Is it possible that 'static is the only possible problem, since you can't name other lifetimes ahead of time? If so I wonder if the following even weaker (?) proposition holds, which also would be sufficient for me:
Proposition 4: Assume a type F: AsyncFnMut that comes from an actual syntactic closure. If F::CallOnceFuture: Send, then F::CallRefFuture<'l>: Send for all lifetimes 'l.
The reason I think this might work is because I imagine the generated async_call_once implementation looks like something like this:
async fn async_call_once(self, args: Args) {
<Self as AsyncFnMut>::async_call_mut(&mut self, args).await
}
That borrow has an anonymous lifetime, so it shouldn't be possible to write a tricky type like the one in your example that is Send for that lifetime but not for others.
(I guess more generally I could modify proposition 3 to say "for some non-'static lifetime" for the same effect.)
Proposition 4 is false, even in non-contrived cases. For example: Rust Playground
The "non-'static lifetime" idea doesn't work, due to it being possible for an impl to be conditional on two lifetimes being equal:
struct Thing<'a, 'b>(&'a i32, &'b i32, *const i32);
unsafe impl<'a> Send for Thing<'a, 'a> {}
1 Like
I kind lost what you meant by "truly anonymous lifetimes". Lifetimes are just constraint checking. And one can easily write this:
unsafe impl<'a, 'b: 'a> Send for Thing<'a, 'b> {}
So you don't really need to "name" anything.
I think you can't make any assumption about whether lifetime affecting sendness or not.
Specifically in the macro in my previous reply nothing about the lifetime parameter to CallRefFuture can effect whether your Thing is send, right? (Perhaps this requires the closure to be a true closure, not a manual implementation of AsyncFnMut.)
Do you see a counter-example that would defeat that macro?
Sorry, I accidentally deleted my post by clicking on the wrong button when trying to edit it to fix a bug. :-/ I've posted the corrected macro and the question about counter-examples here.
1 Like