Define variable lifetime in a function


#1

Hi,
For some long time I didn’t have any borrow checker fight but it hit me again.

I have to use struct similar to this:

struct CMConfig<'a> {
  description: Option<&'a str>
}

When I’m creating function that is returning this type with following signature:

pub fn check_cm<'a>(&self, mac: &Mac) -> Result<(bool, CMConfig<'a>), Error> {

When I assign some value to description field in CMConfig struct that will be returned lifetime error kicks in:

attrs` does not live long enough
   --> src/handler.rs:174:43
    |
174 |                     let dhcp_statements = attrs.get("dhcpStatements");
    |                                           ^^^^^ does not live long enough
...
194 |                 }
    |                 - borrowed value only lives until here
    |

This is how dhcp_statements is created:

let attrs: HashMap<String, Vec<String>> = search_entry.attrs;
let dhcp_statements = attrs.get("dhcpStatements");
          match dhcp_statements {
                    Some(dhcp_statements) => {
                        for stmt in dhcp_statements.iter() {
                            let statement: Vec<&str> = stmt.split_whitespace().collect();
                            if statement.len() == 2 {
                                let key = statement[0];
                                let value = statement[1];
                                if key == "description" {
                                    cm.description = Some(statement[1]);
                                }
                            }
                        }
                    },
                    None => {}
                }

How to force lifetime value for attrs so I can return CMConfig with some &str references.
I can’t change CMConfig struct to use String as a field type…


#2

Any of my attempts like this one to work on Strings just move problem somwhere else:

                   for stmt in dhcp_statements.iter() {
                        let statement: Vec<String> = stmt.split_whitespace().map(String::from).collect();
                        if statement.len() == 2 {
                            let key = &statement[0];
                            let value: &'a str = &statement[1];
                            if key == "fixed-address" {
                                // let ip:& 'a str  = value;
                                cm.ip_address = Some(value);
                            }
                        }
                    }

Hower simplified reproduction of my problem like this works like a charm:

#[derive(Debug)]
struct CMConfig<'a> {
    description: Option<&'a str>,
}

fn make_cm<'a>() -> CMConfig<'a> {
    let desc = "Test description";
    CMConfig { description: Some(desc) }
}

fn main() {
    println!("{:?}", make_cm());
}

#3

You moved search_entry.attrs into the local attrs variable (binding). Once the function returns, that HashMap is destroyed (freed). You are, however, trying to store a &str that itself is pointing at one of these Strings contained in the Vec of the HashMap in the CMConfig, which you then return out of the function. That’s why the compiler is saying attrs doesn’t live long enough for that to be valid.

So, if you want to use a CMConfig<'a> where 'a is not 'static, then you need to make sure the String values you’re slicing into are kept alive longer than the CMConfig itself.


#4

That is also my reasoning. Question remains how to achieve that in my function code inside function ?


#5

It’s hard to say without seeing more code. In the abstract, are you able to generate this HashMap<String, Vec<String>> somewhere further up the call stack? It’s not clear where search_entry comes from because you’re showing just a small snippet of code.

And to be clear - you cannot change CMConfig in any way at all?


#6

I will post whole function code later (commuting right now :slight_smile: )
It’s just making LDAP query and this HasMap is part of its result. I can’t move it anywhere else’s since pure job of this function is to make ldap query and return some data from that query.
Changing CMConf struct would require a lot of code refactoring. It’s fields are filled in many places by function calls that returns Option(&str) and would end up to rewrite those functions or to make monstrocities like Some(String::from(fn_call().unwrap()) where now it’s just function with signature like fn_call() -> Option(&str)


#7

Ok, so if I’m understanding correctly, this HashMap and the enclosed Vec<String> is all allocated in this function call. If that’s correct, you cannot return a &str into those String values from this function. It sounds like what you’d be served well by is:

struct CMConfig<'a> {
    description: Option<Cow<'a, str>>,
}

That would allow you to use (borrowed) &str where the lifetimes of CMConfig and the owning String values line up, but would also allow you to create owned String values in cases your CMConfig needs to live longer. Of course this requires refactoring.


#8

Thanks,
You are right regarding HashMap creation.
I will check if this is something that is ok for us.
Despite knowing reasons behind, string management in rust sucks :slight_smile:


#9

I don’t think it sucks, but you do have to plan for how you’re going to do it (if you’re coming from a GC language background, this is simply something you don’t even consider typically) - that obviously applies to other non-Copy values, not just String.

One could argue that Cow<str> should be the preferred way if you don’t know upfront how it’s going to look. Alternatively, you can start with the less-performant approach - take String values. So then you’d either move them or clone them into there. Then, once you have a good grasp on the various use cases, you can potentially refactor to use references (or perhaps Rc/Arc if that’s more appropriate) if that seems like it would offer up performance benefits.


#10

I think you mean fn_call.map(String::from).