Я начал делать свой executor/reactor и застрял:
Первая попытка:
use std::future::Future;
struct Runtime;
fn start_with_runtime<C, F>(closure: C)
where
C: for<'a> FnOnce(&'a Runtime) -> F,
F: Future
{
let rt = Runtime;
let _future = closure(&rt);
// block_on(future)
}
async fn async_main(_rt: &Runtime) {}
fn main() {
start_with_runtime(|rt| { async_main(rt) });
}
Идея проста, executor/reactor запускаются с помощью функции start_with_runtime(). Однако здесь я хочу попробовать идею когда API для работы с асинхронным кодом передается в качестве параметра, поэтому start_with_runtime() создает runtime на стеке и на вход просит функцию/замыкание которое получит ссылку на этот runtime. Запускаем:
error: lifetime may not live long enough
--> src/main.rs:17:31
|
17 | start_with_runtime(|rt| { async_main(rt) });
| --- ^^^^^^^^^^^^^^ returning this value requires that `'1` must outlive `'2`
| | |
| | return type of closure is impl std::future::Future
| has type `&'1 Runtime`
Что за ерунда? Рассахариваем async просто для упрощения:
struct Runtime;
trait Shmutura {}
struct ShmuturaImpl<'a> {
rt: &'a Runtime
}
impl<'a> Shmutura for ShmuturaImpl<'a> {
}
fn start_with_runtime<'b, C, F>(closure: C)
where
C: for<'a> FnOnce(&'a Runtime) -> F,
F: Shmutura + 'b
{
let rt = Runtime;
let _future = closure(&rt);
// block_on(future)
}
fn desugared_async<'a>(rt: &'a Runtime) -> impl Shmutura + 'a {
ShmuturaImpl { rt }
}
fn main() {
start_with_runtime(|rt| { desugared_async(rt) });
}
Ошибка не изменилась, но теперь я вижу что с async
и Future
это вообще никак не связано.
Итак, я ожидаю что замыкание, которое мы передаем в start_with_runtime()
имеет вид |&'a Runtime| -> impl Future + 'a
. Однако несколько часов спустя выясняется что это не так. Я нашел такое обсуждение:
https://github.com/rust-lang/rust/issues/58052 :
fn main() {
let f = |x: &i32| x;
let i = &3;
let j = f(i);
}
error: lifetime may not live long enough
--> src/main.rs:2:23
|
2 | let f = |x: &i32| x;
| - - ^ returning this value requires that `'1` must outlive `'2`
| | |
| | return type of closure is &'2 i32
| let's call the lifetime of this reference `'1`
Для замыканий придумали простой синтаксис, я в своем коде не указывал ни тип параметров, ни тип результата в надежде что компилятор сам выведет все типы. Видимо в этом случае он делает что-то такое что трудно от него ожидать, что-то вроде |&'a Runtime| -> impl Future + 'b
и тикет #58052 про то что с этим что-то не так.
Начинаешь гулять по тикетам (#56537, #55526 и дальше по ссылкам) чтобы понять как что работает и попадаешь в какую-то кроличью нору, у которой сразу несколько измерений.
Измерение первое: Выглядит это таким образом что команда Rust находит какой-то случай, который компилируется с ошибкой и тогда они начинают обсуждать правильно это или нет. Рассуждения там такие, ну наверное должно компилироваться, почему бы и нет, давайте я поправлю. Следующее сообщение, “я поправил но поломалось в других местах, давайте оставим как есть и потом что-нибудь придумаем”. С одной стороны выглядит так что компетентные люди решают проблемы языка по мере возможностей, с другой стороны какой-то спецификации у языка нет. Таким образом скорей всего нет какой-то возможности сделать вторую реализацию компилятора Rust, потому что как и что должно компилироваться решается по ходу дела.
Измерение второе: чтобы как-то понимать что можно было писать, а что нет. С одной стороны вывод типов и lifetime elision нужны для того чтобы все как бы работало само собой, сложность скрывается и вроде писать код становится концептуально проще. Однако когда все это ломается, приходится со всем этим разбираться чтобы получить понимаю что вообще происходит. Учитывая что нет какого-то стандарта или спецификации, трудно понять где вообще можно искать.
Ладно, вытираем слезки и пытаемся как-то исправить проблему. В кроличьей норке я узнал что сигнатура у замыканий выводится оглядываясь на требуемую сигнатуру, так что может быть можно как-то указать правильную сигнатуру здесь:
fn start_with_runtime<'b, C, F>(closure: C)
where
C: for<'a> FnOnce(&'a Runtime) -> F,
F: Shmutura + 'b
В реализации выше явно указано гонево в виде'b
, но это от безысходности, на этом месте должно быть 'a
от for<'a>
что не компилируется или что что-то вроде такого:
fn start_with_runtime<C>(closure: C)
where
C: for<'a> FnOnce(&'a Runtime) -> (impl Shmutura +'a),
Что тоже не компилируется:
error[E0562]: `impl Trait` not allowed outside of function and inherent method return types
--> src/main.rs:12:40
|
12 | C: for<'a> FnOnce(&'a Runtime) -> (impl Shmutura + 'a),
| ^^^^^^^^^^^^^^^^^^
Нельзя использовать impl Trait
для возвращаемого параметра FnOnce. Если же я в своих функциях укажу ShmuturaImpl как возвращаемый тип, то все компилируется:
(ссылку на playground не могу поставить из-за лимита)
struct Runtime;
trait Shmutura {}
struct ShmuturaImpl<'a> {
rt: &'a Runtime,
}
impl<'a> Shmutura for ShmuturaImpl<'a> {}
fn start_with_runtime<C>(closure: C)
where
C: for<'a> FnOnce(&'a Runtime) -> ShmuturaImpl<'a>,
{
let rt = Runtime;
let _future = closure(&rt);
// block_on(future)
}
//fn desugared_async<'a>(rt: &'a Runtime) -> impl Shmutura + 'a {
fn desugared_async<'a>(rt: &'a Runtime) -> ShmuturaImpl<'a> {
ShmuturaImpl { rt }
}
fn associate<C>(closure: C) -> C
where
C: for<'a> FnOnce(&'a Runtime) -> ShmuturaImpl<'a>
{
return closure;
}
fn main() {
start_with_runtime(associate(|rt| desugared_async(rt)));
}
Значит гипотеза о том чтобы можно было указать правильный expected signature работает и lifetime у замыкания в этом случае корректно выводится. В случае с шмутурами это может работать, но в случае с async и Future мне не известен тип Future, так что надо думать дальше.
Проверим кстати что HRTB нельзя заменить на lifetime привязанный к start_with_runtime:
fn start_with_runtime<'b>(closure: impl FnOnce(&'b Runtime) -> ShmuturaImpl<'b>)
{
let rt = Runtime;
let _future = closure(&rt);
// block_on(future)
}
Не собирается:
Compiling playground v0.0.1 (/playground)
error[E0597]: `rt` does not live long enough
--> src/main.rs:14:27
|
11 | fn start_with_runtime<'b>(closure: impl FnOnce(&'b Runtime) -> ShmuturaImpl<'b>)
| -- lifetime `'b` defined here
...
14 | let _future = closure(&rt);
| --------^^^-
| | |
| | borrowed value does not live long enough
| argument requires that `rt` is borrowed for `'b`
15 | // block_on(future)
16 | }
| - `rt` dropped here while still borrowed
Все, я в тупике.