How to correctly read a string value from an outer scope within an async closure for Hyper in Rust

huangapple go评论75阅读模式
英文:

How to correctly read a string value from an outer scope within an async closure for Hyper in Rust

问题

以下是您提供的内容的翻译:

我正在尝试学习Rust,并尝试编写一些非常简单的Web服务器代码来实现这一目标。

我以为我对生命周期和借用的基础知识有一个良好的理解,但我发现要么我漏掉了某个基本的技巧,要么我认为是一个简单情况的情况实际上因某种原因更加复杂。

我本质上要做的是这样的:

use std::env;
use std::convert::Infallible;
use std::net::SocketAddr;
use hyper::{Body, Request, Response, Server};
use hyper::service::{make_service_fn, service_fn};

// 一个演示Web服务器:从命令行接收一条消息,然后将其返回给传入的请求。

#[tokio::main]
pub async fn main() {
    let args: Vec<String> = env::args().collect();
    let message = format!("Arguments were: {:?}", &args[1..]);
    serve_message(message).await;
}

pub async fn serve_message(message: String) {
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));

    let make_svc = make_service_fn(|_conn| {
        async move {
            Ok::<_, Infallible>(service_fn(move |_: Request<Body>| async move {
                Ok::<_, Infallible>(
                    Response::new(Body::from(message))
                )
            }))
        }
    });

    let server = Server::bind(&addr).serve(make_svc);

    if let Err(e) = server.await {
        eprintln!("server error: {}", e);
    }
}

这个代码在编译时失败,出现以下错误:

error[E0507]: 无法从`message`中移出一个在`FnMut`闭包中捕获的变量
  --> src/main.rs:22:68
   |
17 |   pub async fn serve_message(message: String) {
   |                              ------- 外部捕获的变量
...
22 |               Ok::<_, Infallible>(service_fn(move |_: Request<Body>| async move {
   |  ____________________________________________-----------------------^
   | |                                            |
   | |                                            被此`FnMut`闭包捕获
23 | |                 Ok::<_, Infallible>(
24 | |                     Response::new(Body::from(message))
   | |                                              -------
   | |                                              |
   | |                                              变量由于在生成器中使用而被移出
   | |                                              移动发生因为`message`具有不实现`Copy`特性的`String`类型
25 | |                 )
26 | |             }))
   | |_____________^ 移出`message`发生在这里

error[E0507]: 无法从`message`中移出一个在`FnMut`闭包中捕获的变量
  --> src/main.rs:21:9
   |
17 |   pub async fn serve_message(message: String) {
   |                              ------- 外部捕获的变量
...
20 |       let make_svc = make_service_fn(|_conn| {
   |                                      ------- 被此`FnMut`闭包捕获
21 | /         async move {
22 | |             Ok::<_, Infallible>(service_fn(move |_: Request<Body>| async move {
23 | |                 Ok::<_, Infallible>(
24 | |                     Response::new(Body::from(message))
   | |                                              -------
   | |                                              |
   | |                                              变量由于在生成器中使用而被移出
   | |                                              移动发生因为`message`具有不实现`Copy`特性的`String`类型
25 | |                 )
26 | |             }))
27 | |         }
   | |_________^ 移出`message`发生在这里

我尝试了许多更复杂的修改,包括克隆、ARCs、将状态放入具有句柄impl的结构体以及许多其他方法,但我一直在挣扎,每一种方法似乎都让我回到了上面的同一个根本问题。显然,我在异步、闭包和所有权交互方面漏掉了一些基本的东西,以及管理它们的工具。我看过 https://stackoverflow.com/questions/67860385/how-to-re-use-a-value-from-the-outer-scope-inside-a-closure-in-rust ,它类似,但唯一的答案示例是一个更简单的演示,不明显适用于更大的问题 - 就像随处建议的添加 .clone() 对于这种情况似乎不足够。

我发现最令我困惑的部分是,这与Hyper自己的一个示例非常相似:https://docs.rs/hyper/latest/hyper/service/fn.make_service_fn.html#example。但这个示例似乎没有遇到任何问题,而这个示例却遇到了问题。

正确和惯用的方法是什么,为什么它起作用,以及这与Hyper示例案例有何不同?非常感谢初学者级别的解释。

英文:

I'm trying to learn Rust, and trying to write some extremely simple web server code to do so.

I thought I had a good idea of the basics of lifetimes & borrowing in simple code, but I'm finding that either I'm missing a basic technique somewhere, or what I thought was a simple case is actually much more complicated for some reason.

What I'm essentially trying to do is this:

use std::env;
use std::convert::Infallible;
use std::net::SocketAddr;
use hyper::{Body, Request, Response, Server};
use hyper::service::{make_service_fn, service_fn};

// A demo web server: takes a message on the command-line, then
// serves it back to incoming requests.

#[tokio::main]
pub async fn main() {
    let args: Vec&lt;String&gt; = env::args().collect();
    let message = format!(&quot;Arguments were: {:?}&quot;, &amp;args[1..]);
    serve_message(message).await;
}

pub async fn serve_message(message: String) {
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));

    let make_svc = make_service_fn(|_conn| {
        async move {
            Ok::&lt;_, Infallible&gt;(service_fn(move |_: Request&lt;Body&gt;| async move {
                Ok::&lt;_, Infallible&gt;(
                    Response::new(Body::from(message))
                )
            }))
        }
    });

    let server = Server::bind(&amp;addr).serve(make_svc);

    if let Err(e) = server.await {
        eprintln!(&quot;server error: {}&quot;, e);
    }
}

This fails to compile with:

error[E0507]: cannot move out of `message`, a captured variable in an `FnMut` closure
  --&gt; src/main.rs:22:68
   |
17 |   pub async fn serve_message(message: String) {
   |                              ------- captured outer variable
...
22 |               Ok::&lt;_, Infallible&gt;(service_fn(move |_: Request&lt;Body&gt;| async move {
   |  ____________________________________________-----------------------_^
   | |                                            |
   | |                                            captured by this `FnMut` closure
23 | |                 Ok::&lt;_, Infallible&gt;(
24 | |                     Response::new(Body::from(message))
   | |                                              -------
   | |                                              |
   | |                                              variable moved due to use in generator
   | |                                              move occurs because `message` has type `String`, which does not implement the `Copy` trait
25 | |                 )
26 | |             }))
   | |_____________^ move out of `message` occurs here

error[E0507]: cannot move out of `message`, a captured variable in an `FnMut` closure
  --&gt; src/main.rs:21:9
   |
17 |   pub async fn serve_message(message: String) {
   |                              ------- captured outer variable
...
20 |       let make_svc = make_service_fn(|_conn| {
   |                                      ------- captured by this `FnMut` closure
21 | /         async move {
22 | |             Ok::&lt;_, Infallible&gt;(service_fn(move |_: Request&lt;Body&gt;| async move {
23 | |                 Ok::&lt;_, Infallible&gt;(
24 | |                     Response::new(Body::from(message))
   | |                                              -------
   | |                                              |
   | |                                              variable moved due to use in generator
   | |                                              move occurs because `message` has type `String`, which does not implement the `Copy` trait
25 | |                 )
26 | |             }))
27 | |         }
   | |_________^ move out of `message` occurs here

I've tried a wide variety of more complex modifications of this, with cloning, ARCs, state into a struct with a handle impl, and lots of other approaches, but I'm struggling and each of them seems to bring me back to the same fundamental problem above. I'm clearly missing something essential about how async, closures & ownership interact, and the tools to manage that. I have seen https://stackoverflow.com/questions/67860385/how-to-re-use-a-value-from-the-outer-scope-inside-a-closure-in-rust which is similar, but the only answer's example is a simpler demo that doesn't clearly translate to the larger problem - just adding .clone() as suggested everywhere doesn't seem sufficient for this case.

The part I find most confusing is that this is extremely similar to one of Hyper's own examples: https://docs.rs/hyper/latest/hyper/service/fn.make_service_fn.html#example. But that example doesn't seem to hit any issues, while this does.

What's the correct & idiomatic way to do this, why does it work, and what's the difference between this and that Hyper example case? Beginner-level explanations much appreciated.

答案1

得分: 1

以下是代码部分的翻译:

use bytes::Bytes;
use hyper::{
    service::{make_service_fn, service_fn},
    Body, Error as HyperError, Request, Response, Server,
};
use std::convert::Infallible;
use std::net::SocketAddr;
use std::sync::Arc;

pub async fn serve_message(message: String) {
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));

    let message = Arc::new(Bytes::from(message));

    let make_svc = make_service_fn(move |_conn| {
        let message = Arc::clone(&message);
        async {
            Ok::<_, Infallible>(service_fn(move |_: Request<Body>| {
                let message = Bytes::copy_from_slice(&*Arc::clone(&message));
                async move { Ok::<_, HyperError>(Response::new(Body::from(message))) }
            }))
        }
    });

    let server = Server::bind(&addr).serve(make_svc);

    if let Err(e) = server.await {
        eprintln!("server error: {}", e);
    }
}

希望这对你有所帮助。如果需要更多信息或有其他问题,请随时提出。

英文:

The reason that you are getting this error is that the message string is being moved into the closure that you pass to service_fn. In Rust, each value has a unique owner, and moving a value transfers its ownership. Once a value has been moved, it can no longer be used from the original location. See "Ownership and moves"

However, in your case, you want to use the message string in multiple responses, which means you need to share it between multiple closures. This is where Arc (Atomic Reference Counting) comes in handy.
An Arc&lt;T&gt; is a thread-safe reference-counted pointer that allows shared read access to a value of type T. It can be cloned to create a new pointer to the same value, increasing the reference count.

You can update your serve_message function to wrap the message in an Arc and then clone it for each request like this (playground):

use bytes::Bytes;
use hyper::{
    service::{make_service_fn, service_fn},
    Body, Error as HyperError, Request, Response, Server,
};
use std::convert::Infallible;
use std::net::SocketAddr;
use std::sync::Arc;

pub async fn serve_message(message: String) {
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));

    let message = Arc::new(Bytes::from(message));

    let make_svc = make_service_fn(move |_conn| {
        let message = Arc::clone(&amp;message);
        async {
            Ok::&lt;_, Infallible&gt;(service_fn(move |_: Request&lt;Body&gt;| {
                let message = Bytes::copy_from_slice(&amp;*Arc::clone(&amp;message));
                async move { Ok::&lt;_, HyperError&gt;(Response::new(Body::from(message))) }
            }))
        }
    });

    let server = Server::bind(&amp;addr).serve(make_svc);

    if let Err(e) = server.await {
        eprintln!(&quot;server error: {}&quot;, e);
    }
}

The way it works is:

  • Arc::new(message) creates a new Arc that owns the message. This is the only Arc that directly owns the message.
  • Each time make_service_fn is called, it clones the Arc (not the message itself), which increases the reference count but does not move the message.
  • Inside service_fn, we again clone the Arc for each request. This allows each request to have a reference to the message without taking ownership.
  • Finally, Response::new(Body::from((*Arc::clone(&amp;message)).clone())) clones the Arc one more time to use the message in the response. This does not move the message and allows it to be used in subsequent responses.

Regarding the last point:

The code Response::new(Body::from(Arc::clone(&amp;message).to_string())) would work as well, but there is a key difference to keep in mind.

When you use Arc::clone(&amp;message).to_string(), you are creating a new String for every request. This can be inefficient if the message is large or if there are a lot of requests, because it involves allocating memory for a new String each time.

Instead, let message = Bytes::copy_from_slice(&amp;*Arc::clone(&amp;message)) is of type Bytes.

In this code, we're using Bytes::copy_from_slice(&amp;*Arc::clone(&amp;message)) to create a new Bytes instance from the shared Bytes for each request.
This is less efficient than sharing the Bytes directly (as we would ideally like to do), but it avoids the borrowing errors you were encountering earlier.

The breakdown is:

  • Arc::clone(&amp;message) creates a new Arc that points to the same Bytes value that message points to. The type of Arc::clone(&amp;message) is Arc&lt;Bytes&gt;.
  • &amp;*Arc::clone(&amp;message) dereferences the Arc&lt;Bytes&gt; to get a reference to the Bytes value it points to. The type of &amp;*Arc::clone(&amp;message) is &amp;Bytes.
  • Bytes::copy_from_slice(&amp;*Arc::clone(&amp;message)) creates a new Bytes value that contains the same byte sequence as the Bytes value message points to. The type of Bytes::copy_from_slice(&amp;*Arc::clone(&amp;message)) is Bytes.

The Bytes::copy_from_slice function expects a reference to a byte slice (&amp;[u8]), and &amp;Bytes can be used as &amp;[u8] because Bytes implements Deref&lt;Target=[u8]&gt;.

So the overall effect of Bytes::copy_from_slice(&amp;*Arc::clone(&amp;message)) is to create a new Bytes value that contains a copy of the bytes from the original Bytes value, allowing it to be used independently in the response body.


Dereferencing Arc&lt;Bytes&gt; to get a Bytes reference (&amp;Bytes) doesn't work is because Body::from doesn't accept a &amp;Bytes reference as a parameter.
While Rust's From trait often does work with references, in this case, the From trait is only implemented for the owned Bytes type and not for a reference to Bytes.

  • Body::from consumes its argument. It takes ownership of the provided value.
  • When you dereference Arc&lt;Bytes&gt;, you get a &amp;Bytes (a reference to Bytes), not an owned Bytes.
  • A &amp;Bytes is not the same thing as Bytes. They are different types. The former is a reference to a Bytes value, the latter is an owned Bytes value.
  • Because Body::from is not implemented for &amp;Bytes, you can't pass a &amp;Bytes to Body::from.

The Hyper example you linked creates an HTTP response with a body of type hyper::Body::empty(), which does not involve any borrowing from the outer scope.

In your code, you are trying to use a String (message) from the outer scope inside your service function, which leads to the borrowing issues you are encountering.

Your use case is different from the Hyper example because you want to share an owned String between multiple closures, which requires the use of Arc as explained above. The Arc allows multiple closures to have a read-only reference to the same String without taking ownership of it.

huangapple
  • 本文由 发表于 2023年5月20日 23:47:22
  • 转载请务必保留本文链接:https://go.coder-hub.com/76296058.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定