rustborrow-checkerownershipborrowing

Why I get "temporary value dropped while borrowed" if I assign, but not when passing via function?


I am quite fresh with Rust. I have experience mainly in C and C++.

This code from lol_html crate example works.

use lol_html::{element, HtmlRewriter, Settings};

let mut output = vec![];

{
    let mut rewriter = HtmlRewriter::try_new(
        Settings {
            element_content_handlers: vec![
                // Rewrite insecure hyperlinks
                element!("a[href]", |el| {
                    let href = el
                        .get_attribute("href")
                        .unwrap()
                        .replace("http:", "https:");

                    el.set_attribute("href", &href).unwrap();

                    Ok(())
                })
            ],
            ..Settings::default()
        },
        |c: &[u8]| output.extend_from_slice(c)
    ).unwrap();

    rewriter.write(b"<div><a href=").unwrap();
    rewriter.write(b"http://example.com>").unwrap();
    rewriter.write(b"</a></div>").unwrap();
    rewriter.end().unwrap();
}

assert_eq!(
    String::from_utf8(output).unwrap(),
    r#"<div><a href="https://example.com"></a></div>"#
);

But if I move element_content_handlers vec outside and assign it, I get

temporary value dropped while borrowed

for the let line:

use lol_html::{element, HtmlRewriter, Settings};

let mut output = vec![];

{
    let handlers = vec![
                // Rewrite insecure hyperlinks
                element!("a[href]", |el| {
                    let href = el
                        .get_attribute("href")
                        .unwrap()
                        .replace("http:", "https:");

                    el.set_attribute("href", &href).unwrap();

                    Ok(())
                }) // this element is deemed temporary
            ];

    let mut rewriter = HtmlRewriter::try_new(
        Settings {
            element_content_handlers: handlers,
            ..Settings::default()
        },
        |c: &[u8]| output.extend_from_slice(c)
    ).unwrap();

    rewriter.write(b"<div><a href=").unwrap();
    rewriter.write(b"http://example.com>").unwrap();
    rewriter.write(b"</a></div>").unwrap();
    rewriter.end().unwrap();
}

assert_eq!(
    String::from_utf8(output).unwrap(),
    r#"<div><a href="https://example.com"></a></div>"#
);

I think that the method takes ownership of the vector, but I don't understand why it does not work with the simple assignment. I don't want to let declare all elements first. I expect that there is a simple idiom to make it own all elements.

EDIT: Compiler proposed to bind the element before the line, but what if I have a lot of elements? I would like to avoid naming 50 elements for example. Is there a way to do this without binding all the elements? Also why the lifetime of the temporary ends there inside of vec! invocation in case of a let binding, but not when I put the vec! inside newly constructed struct passed to a method? The last question is very important to me.


Solution

  • When I first tried to reproduce your issue, I got that try_new didn't exist. It's been removed in the latest version of lol_html. Replacing it with new, your issue didn't reproduce. I was able to reproduce with v0.2.0, though. Since the issue had to do with code generated by macros, I tried cargo expand (something you need to install, see here).

    Here's what let handlers = ... expanded to in v0.2.0:

    let handlers = <[_]>::into_vec(box [(
        &"a[href]".parse::<::lol_html::Selector>().unwrap(),
        ::lol_html::ElementContentHandlers::default().element(|el| {
            let href = el.get_attribute("href").unwrap().replace("http:", "https:");
            el.set_attribute("href", &href).unwrap();
            Ok(())
        }),
    )]);
    

    and here's what it expands to in v0.3.0

    let handlers = <[_]>::into_vec(box [(
        ::std::borrow::Cow::Owned("a[href]".parse::<::lol_html::Selector>().unwrap()),
        ::lol_html::ElementContentHandlers::default().element(|el| {
            let href = el.get_attribute("href").unwrap().replace("http:", "https:");
            el.set_attribute("href", &href).unwrap();
            Ok(())
        }),
    )]);
    

    Ignore the first line, it's how the macro vec! expands. The second line shows the difference in what the versions generate. The first takes a borrow of the result of parse, the second takes a Cow::Owned of it. (Cow stands for copy on write, but it's more generally useful for anything where you want to be generic over either the borrowed or owned version of something.).

    So the short answer is the macro used to expand to something that wasn't owned, and now it does. As for why it worked without a separate assignment, that's because Rust automatically created a temporary variable for you.

    When using a value expression in most place expression contexts, a temporary unnamed memory location is created initialized to that value and the expression evaluates to that location instead, except if promoted to a static

    https://doc.rust-lang.org/reference/expressions.html#tempora...

    Initially rust created multiple temporaries for you, all valid for the same-ish scope, the scope of the call to try_new. When you break out the vector to its own assignment the temporary created for element! is only valid for the scope of the vector assignment.

    I took a look at the git blame for the element! macro in lol_html, and they made the change because someone opened an issue with essentially your problem. So I'd say this is a bug in a leaky abstraction, not an issue with your understanding of rust.