Rust in Everything
基于 async-stream 0.3.6

编译报错

主线位置:宏展开一段的出口。这四条报错都不是 async-stream 自己发出的,是编译器在看宏改写之后的代码时报的,所以报错里提到的东西(async 块、yield 语法、一个奇怪的标签、AsyncStream<..>)都要对回到改写规则才看得懂。

flowchart TB
  src["stream! { ... }"] --> scrub["Scrub 改写"]
  scrub -->|"yield 不在字面位置,没改写"| e0658["E0658 yield syntax is experimental"]
  scrub -->|"yield 改写了,但落进别的 async 块"| e0767["E0767 unreachable label"]
  scrub -->|"? 改写成 send(Err(e.into()))"| e0282["E0282 type annotations needed"]
  scrub --> out["AsyncStream::new(rx, async move { .. })"]
  out -->|"直接 .next()"| e0277["E0277 cannot be unpinned"]

依赖:

async-stream = "=0.3.6"
futures = "0.3"
tokio = { version = "=1.54.1", features = ["full"] }

{async block@...} cannot be unpinned(E0277)

use async_stream::stream;
use futures::StreamExt;

#[tokio::main]
async fn main() {
  let mut s = stream! {
    yield 1;
    yield 2;
  };
  while let Some(v) = s.next().await {
    println!("{v}");
  }
}
error[E0277]: `{async block@lib.rs:193:9}` cannot be unpinned
    --> src/bin/unpin.rs:10:25
     |
  10 |   while let Some(v) = s.next().await {
     |                         ^^^^ within `__Origin<'_, {integer}, {async block@lib.rs:193:9}>`, the trait `Unpin` is not implemented for `{async block@lib.rs:193:9}`
     |
     = note: consider using the `pin!` macro
             consider using `Box::pin` if you need to access the pinned value outside of the current scope
...
     = note: required for `AsyncStream<{integer}, {async block@lib.rs:193:9}>` to implement `Unpin`
note: required by a bound in `futures::StreamExt::next`
...
 275 |         Self: Unpin,
     |               ^^^^^ required by this bound in `StreamExt::next`

同样的报错对 .await 再报一次。读这条报错要知道三件事:

  • lib.rs:193:9 不是你的文件。 它是 async-stream 的 lib.rs 第 193 行,stream! 这个 macro_rules! 调用 stream_inner! 的那一行。宏生成的 async 块借的是这个位置的 span,所以报错里的 async 块「定义在」async-stream 里。
  • __Origin 是 pin-project-lite 生成的。 AsyncStream 用 pin_project! 声明,generator 字段标了 #[pin],生成的 Unpin 实现要求这个字段 Unpin;async 块不是 Unpin,AsyncStream 也就不是(poll_next 的「!Unpin、Send、Sync 与 Debug」)。
  • 要求 Unpin 的是 StreamExt::next。 next(&mut self) 拿的是普通的 &mut,要在里面 poll 就得能安全地造出 Pin<&mut Self>,只有 Self: Unpin 时才行。

改法是先 pin。async-stream 自己的文档示例在每个循环前都写了 pin_mut!(s); // needed for iteration(lib.rs 第 41 行),这是 futures_util::pin_mut!;Rust 1.68 起标准库有 std::pin::pin!。三种写法:

// 写法一:pin 在栈上
let mut s = pin!(stream! {
  yield 1;
  yield 2;
});
while let Some(v) = s.next().await {
  println!("pin!: {v}");
}
// 写法二:装箱,可以存进结构体、跨函数返回
let mut b = Box::pin(stream! {
  yield 3;
});
while let Some(v) = b.next().await {
  println!("Box::pin: {v}");
}
// 写法三:消费方式自己会 pin
let all: Vec<i32> = stream! { yield 4; yield 5; }.collect().await;
println!("collect: {all:?}");
pin!: 1
pin!: 2
Box::pin: 3
collect: [4, 5]

collect、for_each、fold 这些拿 self 的组合子把流存进自己的 future 里,由外层 pin,不需要你操心;stream! 块里的 for await 也一样(for await)。只有 next()、try_next() 这类拿 &mut self 的才要先 pin。

yield syntax is experimental(E0658)

想用迭代器的 for_each 产出每一项:

let s = stream! {
  (0..3).for_each(|i| {
    yield i;
  });
};
let items: Vec<i32> = s.collect().await;
error[E0658]: yield syntax is experimental
 --> src/bin/closure_yield.rs:8:7
  |
8 |       yield i;
  |       ^^^^^^^
  |
  = note: see issue #43122 <https://github.com/rust-lang/rust/issues/43122> for more information
error: `yield` can only be used in `#[coroutine]` closures, or `gen` blocks
 --> src/bin/closure_yield.rs:8:7
  |
8 |       yield i;
  |       ^^^^^^^
  |
help: use `#[coroutine]` to make this closure a coroutine
  |
7 |     (0..3).for_each(#[coroutine] |i| {
  |                     ++++++++++++
error[E0277]: expected an `FnMut({integer})` closure, found `{coroutine@src/bin/closure_yield.rs:7:21: 7:24}`
...
error[E0277]: the trait bound `Vec<i32>: Extend<()>` is not satisfied

四条报错,没有一条提到 stream!。原因在 Scrub:闭包分支原样跳过,yield i 没被改写,留给了编译器。编译器在 stable 上不认 yield 语法,于是报 E0658;它还把这个闭包当成了 coroutine,于是有第二、三条,建议你加 #[coroutine](这条建议在 stable 上没用)。最后一条更绕:块里一个被改写的 yield 都没有,宏插入了 if false { __yield_tx.send(()).await; }(dummy_yield),流的 Item 被推成了 (),collect 进 Vec<i32> 当然不行。

仓库的 tests/ui 里还有另外两个位置,本机 rustc 1.99.0 上的第一条报错相同,后面跟的不一样:

位置用例本机的其余报错
闭包里yield_in_closure.rs`yield` can only be used in `#[coroutine]` closures, or `gen` blocks;E0277 闭包类型不对
async 块里yield_in_async.rsE0727 `async` coroutines are not yet supported
嵌套 fn 里yield_in_nested_fn.rs`yield` can only be used in `#[coroutine]` closures, or `gen` blocks;E0627 yield expression outside of coroutine literal

看到 yield syntax is experimental,就去找那个 yield 和 stream! 块之间隔着什么:闭包、async { }、fn。改法都是把 yield 搬回块的字面位置:

  • 闭包:换成 for 循环。

    let s = stream! {
      // 闭包换成 for 循环,yield 回到 stream! 块的字面位置
      for i in 0..3 {
        yield i;
      }
    };
    
    [0, 1, 2]
    
  • async 块:让 async 块只算出值,在外面 yield;要消费另一个流,用 for await 或 while let Some(v) = s.next().await。

  • 嵌套 fn:让函数返回值(或返回一个 Vec / 一个 stream),在块里 yield 它的结果。

use of unreachable label '__async_stream_private_check_scope(E0767)

想让两段逻辑并发执行、各自产出值,用了 tokio::join!:

use std::time::Duration;

use async_stream::stream;
use futures::StreamExt;
use tokio::time::sleep;

#[tokio::main]
async fn main() {
  // 想让两路并发执行、各自 yield
  let s = stream! {
    tokio::join!(
      async {
        sleep(Duration::from_millis(1)).await;
        yield "a";
      },
      async {
        yield "b";
      },
    );
  };
  let items: Vec<&str> = s.collect().await;
  println!("{items:?}");
}
error[E0767]: use of unreachable label `'__async_stream_private_check_scope`
  --> src/bin/join_yield.rs:10:11
   |
10 |     let s = stream! {
   |  ___________^
11 | |     tokio::join!(
12 | |       async {
13 | |         sleep(Duration::from_millis(1)).await;
...  |
19 | |     );
20 | |   };
   | |   ^
   | |   |
   | |___unreachable label `'__async_stream_private_check_scope`
   |     unreachable label defined here
   |
   = note: labels are unreachable through functions, closures, async blocks and modules

error[E0267]: `break` inside `async` block
  --> src/bin/join_yield.rs:10:11
...
   | |       async {
   | |       ----- enclosing `async` block

E0267 对两个 async 块各报一次。和上一节对比:这里的 yield 写在 join! 的参数里,join! 是宏调用,Scrub 会进宏调用的 token 找 yield(为了支持 select!,Scrub 的「宏调用」一节);token 层看不出 async { } 的边界,于是两个 yield 都被改写了。改写结果里那句 if false { break '__async_stream_private_check_scope (loop {}); } 落进了 async { },标签穿不过 async 块,编译失败。

这条报错是 0.3.6 加的防线(取舍与对比的 #106 一节),不是宏坏了。它挡住的是「yield 在别的 async 块里执行」这一类代码,不管那个 async 块在运行时会不会真的在别的 stream 里被 poll。仓库自己的回归用例 unsoundness_issue_106.rs 报的是同样两条。

改法:并发的部分只算值,yield 放回块的字面位置:

let s = stream! {
  let (a, b) = tokio::join!(
    async {
      sleep(Duration::from_millis(1)).await;
      "a"
    },
    async { "b" },
  );
  yield a;
  yield b;
};
["a", "b"]

区别是两项要等两路都完成才一起产出。想要「谁先完成谁先产出」,用 tokio::select! 把 yield 写在分支的处理部分(处理部分在外层 async 块里执行,标签可达,见 Scrub 的例子),或者把两路做成两个 stream,用 tokio-stream 的 merge 合并。

type annotations needed for AsyncStream<Result<i32, _>, ...>(E0282)

use async_stream::try_stream;
use futures::StreamExt;

fn parse(s: &str) -> Result<i32, std::num::ParseIntError> {
  s.parse()
}

#[tokio::main]
async fn main() {
  let s = try_stream! {
    let v = parse("1")?;
    yield v;
  };
  let items: Vec<_> = s.collect().await;
  println!("{items:?}");
}
error[E0282]: type annotations needed for `AsyncStream<Result<i32, _>, {async block@lib.rs:229:9}>`
  --> src/bin/infer.rs:10:7
   |
10 |   let s = try_stream! {
   |       ^
...
14 |   let items: Vec<_> = s.collect().await;
   |                       - type must be known at this point
   |
help: consider giving `s` an explicit type, where the placeholders `_` are specified
   |
10 |   let s: async_stream::__private::AsyncStream<Result<_, _>, {async block@.../async-stream-0.3.6/src/lib.rs:229:9: 229:63}> = try_stream! {

parse 的错误类型明明是 ParseIntError,为什么推不出来?因为 ? 被改写成了 send(Err(e.into())).await(Scrub 的 ? 分支):e 是 ParseIntError,into() 能转成任何实现了 From<ParseIntError> 的类型,流的错误类型只能从别处得知。这里的 Vec<_> 没给出任何信息。Ok 那一边是 i32,来自 yield v,所以报错里是 Result<i32, _>。

编译器给的建议没法照做:它让你写出 AsyncStream<.., {async block@..}>,async 块的类型写不出名字,AsyncStream 也在 #[doc(hidden)] 的 __private 里。改法是在别处把错误类型钉死:

// 返回类型把错误类型钉死,`?` 里的 e.into() 就有了目标
fn numbers(input: Vec<&'static str>) -> impl Stream<Item = Result<i32, ParseIntError>> {
  try_stream! {
    for t in input {
      let v = parse(t)?;
      yield v;
    }
  }
}

#[tokio::main]
async fn main() {
  let items: Vec<_> = numbers(vec!["1", "2"]).collect().await;
  println!("fn 返回类型: {items:?}");
  // 不想单独写函数时,给变量标类型也行
  let s: std::pin::Pin<Box<dyn Stream<Item = Result<i32, ParseIntError>>>> = Box::pin(try_stream! {
    let v = parse("3")?;
    yield v;
  });
  let items: Vec<_> = s.collect().await;
  println!("变量标注: {items:?}");
}
fn 返回类型: [Ok(1), Ok(2)]
变量标注: [Ok(3)]

两种写法在依赖里都见得到:sea-orm 的 into_stream 返回 PinBoxStream<'db, Result<Vec<_>, DbErr>>,dioxus-fullstack 的 SSE 客户端把流标成 Pin<Box<dyn Stream<Item = Result<ServerSentEvent, ServerFnError>>>>,见 05 在本站中。消费端写成 Vec<Result<i32, ParseIntError>> 也能让推断通过,但把类型信息放在消费端,换一个消费者就又要写一遍,不如放在返回类型上。

小结

  • E0277 cannot be unpinned:StreamExt::next 要求 Unpin,AsyncStream 里是 async 块,不是 Unpin。先 pin! / Box::pin,或者用 collect 这类拿 self 的组合子。报错里的 lib.rs:193:9 是 async-stream 的宏定义位置。
  • E0658 yield syntax is experimental:yield 隔着闭包、async 块或嵌套 fn,Scrub 没改写。把 yield 搬回块的字面位置。闭包的情况还会附带 Extend<()> 之类的报错,因为一个 yield 都没改写时 Item 被推成 ()。
  • E0767 unreachable label:yield 写在别的宏的参数里,被改写后落进了那个宏的 async 块,0.3.6 的标签检查挡住了它。让 async 块只算值,在外面 yield。
  • E0282 type annotations needed:? 改写成了 e.into(),错误类型推不出。给返回值写 impl Stream<Item = Result<T, E>>,或者给变量标上装箱后的类型。

💬 关联讨论

发起讨论
加载中...