Code that compiles with the 2021 edition but not with the 2024 edition!

Hello,

When I migrated to the 2024 edition, this code that worked before no longer compiles now!

Code :

...

pub(crate) fn create_html(document: &str, title: &str) -> String {
    SWAGGER_UI_TEMPLATE
        .replace("{:title}", title)
        .replace("{:style}", SWAGGER_UI_CSS)
        .replace("{:script}", SWAGGER_UI_JS)
        .replace("{:spec}", document)
}

pub(crate) fn create_endpoint(document: &str, title: &str) -> impl Endpoint {
    let ui_html = create_html(document, title);
    poem::Route::new()
        .at("/", make_sync(move |_| Html(ui_html.clone())))
        .at("/oauth-receiver.html", make_sync(move |_| Html(OAUTH_RECEIVER_HTML.to_string())))
}

pub fn modern_swagger_ui<T: poem_openapi::OpenApi, W: poem_openapi::Webhook>(oas: &OpenApiService<T, W>, title: &str) -> impl Endpoint {
    create_endpoint(&oas.spec(), title)
}

#[allow(dead_code)]
pub trait ModernSwagger {
    fn modern_swagger_ui(&self) -> impl Endpoint;
}

impl<T: poem_openapi::OpenApi, W: poem_openapi::Webhook> ModernSwagger for OpenApiService<T, W> {
    fn modern_swagger_ui(&self) -> impl Endpoint {
        create_endpoint(&self.spec(), "Swagger-ui")
    }
}

...

The errors :

   |
63 |     create_endpoint(&oas.spec(), title)
   |     -----------------^^^^^^^^^^--------
   |     |                |                |
   |     |                |                temporary value is freed at the end of this statement
   |     |                creates a temporary value which is freed while still in use
   |     argument requires that borrow lasts for `'static`
72 |     fn modern_swagger_ui(&self) -> impl Endpoint {
   |                          - let's call the lifetime of this reference `'1`
73 |         create_endpoint(&self.spec(), "Swagger-ui")
   |         -----------------^^^^^^^^^^^---------------
   |         |                |                        |
   |         |                |                        temporary value is freed at the end of this statement
   |         |                creates a temporary value which is freed while still in use
   |         argument requires that borrow lasts for `'1`
107 |     let mut api_service = OpenApiService::new((rest::AuthorizationRestCtl, rest::CityRestCtl, rest::AuthRestCtl, rest::SystemRestCtl, rest::MarketRestCtl), "APIs", "1.0")
    |         --------------- binding `api_service` declared here

138 |     let ui = swagger_ui::modern_swagger_ui(&api_service, "API DOC");
    |              ------------------------------^^^^^^^^^^^^------------
    |              |                             |
    |              |                             borrowed value does not live long enough
    |              argument requires that `api_service` is borrowed for `'static`

I fixed the issue, of course, but I'm wondering what changed so much that it no longer accepts this reference passing? Or was the compiler previously blind and failed to detect this error?

1 Like

Have you looked on the Rust Edition Guide?

This looks like a textbook example of the very first change listed there.

This books kinda-sorta exist to be read in the situations where previously correct code stops compiling… and the whole “Rust Edition” idea is to facilitate such changes thus it's hard to see why are you surprised…

1 Like

I tried using "use" use<'a, 'b, T, W>, but it doesn't work. I feel like Rust is getting more and more complicated. I'll try to understand this new concept.

pub fn modern_swagger_ui<'a, 'b, T: poem_openapi::OpenApi, W: poem_openapi::Webhook>(oas: &'a OpenApiService<T, W>, title: &'b str) -> impl Endpoint + use<'a, 'b, T, W> {

You need to replace the use with use<>

This is because the impl Endpoint you return does not capture any of your input parameters.

In 2021 this was the default, but now by default all lifetimes are captured in RPIT

7 Likes

Thank you for your suggestion,
two "use" statements were needed, like this:

pub(crate) fn create_endpoint(document: &str, title: &str) -> impl Endpoint + use<> {
    let ui_html = create_html(document, title);
    poem::Route::new()
        .at("/", make_sync(move |_| Html(ui_html.clone())))
        .at("/oauth-receiver.html", make_sync(move |_| Html(OAUTH_RECEIVER_HTML.to_string())))
}

pub fn modern_swagger_ui<T: poem_openapi::OpenApi, W: poem_openapi::Webhook>(oas: &OpenApiService<T, W>, title: &str) -> impl Endpoint + use<T, W> {
    create_endpoint(&oas.spec(), title)
}
1 Like

You didn’t really have to try anything, I believe: The code in question, in particular the

that you present in your last reply, should both able to work automatically with the existing migration lints & --fix corrections. Are you aware of the official guide/procedure for migrating?

Transitioning an existing project to a new edition - The Rust Edition Guide

1 Like

I intentionally avoid using "cargo fix"; I resolved the errors manually to better understand and learn.

What misled me was the linter, which suggested adding + use<'_, '_, T, W>. However, with cargo fix, it did indeed solve the problem the right way.

1 Like

In that case, it’s probably more effective to work with the pre-migration lints that are explicitly designed for explaining the migration steps. You don’t need to auto-apply them, just enable rust-2024-compatibility warnings, e.g. in the terminal with

RUSTFLAGS="-Wrust-2024-compatibility" cargo check

(or in code with #![warn(rust_2024_compatibility)][1])

and you can enjoy information such as e.g.

warning: `impl Endpoint` will capture more lifetimes than possibly intended in edition 2024
 --> src/main.rs:8:66
  |
8 | pub(crate) fn create_endpoint<T>(document: &str, title: &str) -> impl Endpoint {
  |                                                                  ^^^^^^^^^^^^^
  |
  = warning: this changes meaning in Rust 2024
  = note: for more information, see <https://doc.rust-lang.org/nightly/edition-guide/rust-2024/rpit-lifetime-capture.html>
note: specifically, these lifetimes are in scope but not mentioned in the type's bounds
 --> src/main.rs:8:44
  |
8 | pub(crate) fn create_endpoint<T>(document: &str, title: &str) -> impl Endpoint {
  |                                            ^            ^
  = note: all lifetimes in scope will be captured by `impl Trait`s in edition 2024
  = note: `-W impl-trait-overcaptures` implied by `-W rust-2024-compatibility`
  = help: to override `-W rust-2024-compatibility` add `#[allow(impl_trait_overcaptures)]`
help: use the precise capturing `use<...>` syntax to make the captures explicit
  |
8 | pub(crate) fn create_endpoint<T>(document: &str, title: &str) -> impl Endpoint + use<T> {
  |                                                                                ++++++++

  1. which might actually be preferable, since changing RUSTFLAGS will make all your dependencies recompile ↩︎

13 Likes

Thank you @steffahn for this suggestion, I will apply it next time. :pray:

Feel free to try it out on this code, too, if you still have the pre-migration version in version control.

Technically, in some corner cases the new edition can actually change program behavior. I imagine the change in temporary drop orders could have such corner cases; those would be particularly relevant if unsafe code is involved. E.g. this and this lint which are part of the rust-2024-compatibility group, can detect some points of interest for this change.

Though to be realistic, if your program already compiles again, and appears to work, then it’s probably fine; this is still Rust, after all; and the edition doesn’t change that much anyway.

2 Likes

Life has taught me to always make backups; I’ve already paid a heavy price for forgetting it once. :sweat_smile:

I think the new temporary drop orders solve more problems than they cause, as they release the resource earlier. However, there’s still a risk that remains.

That's what I love about Rust: when it compiles, there's a 99% chance it will work (unless it's a functional bug).

Re: "backups" - if you're not already familiar with source control, do take a day or so to get familiar with one, even working alone it's incredibly useful to eg. be able to just rip into some piece of code to try out an idea and quickly find out if it works, then go back and properly add the new functionality from a known good base, or to find out how long ago you changed some line and how, etc.

Git is the overwhelmingly popular one, which is a bit unfortunate as it's a very confusing interface in comparison to others (though getting slowly better, eg checkout getting split into switch and restore)

3 Likes

I use Git, but only locally, so I still need to back up my disk.

When I work alone on a project, I use Git more like a checkpoint (to roll back in case of bugs) and to number releases.

I often end up with huge commits affecting multiple parts of the code. :sweat_smile:

But when it's a project with multiple people, it turns out to be very useful.
By the way, I use Git + Gitea (which I highly recommend).

It's like GitHub, but open-source and can be installed locally :

Personally, I'm in favor of the Forgejo fork of Gitea for issue tracking and browsing, + Gerrit for code review. :slight_smile:

1 Like

I didn't know about this solution, I'll have a look and try it out, thanks for the recommendation.