[Solved] First issue with lifetimes


#1

Hey everyone !

I am trying to make a function that connects to an ssh server and then returns the sftp session.
The issue here is that the ssh session doesn’t live long enough and I don’t understand how to do any of this, I’m completely new to this type of concept.
Maybe I am not thinking of it the right way ?

fn sftp_connect<'a>(account: String, pass: String) -> ssh2::Sftp<'a> {
    let tcp = TcpStream::connect("127.0.0.1:22").expect("Couldn't connect to ssh server");

    let mut sess: ssh2::Session = ssh2::Session::new().expect("Couldn't create a NEW session");
    sess.handshake(&tcp).expect("No handshake, no like");

    sess.userauth_password(account.as_ref(), pass.as_ref()).expect("Wrong USERNAME/PASSWORD ?!");

    sess.sftp().expect("Could not start sftp session")
}

Library used is ssh2.

I would really like to get better at this language but the book didn’t help me in this case because I’m dealing with an external library with it’s own lifetimes.

Thank you in advance!


#2

The Sftp<'_> struct has a lifetime parameter (as you’ve already discovered), but this lifetime refers to the Session from which you created it via the sftp() method. In other words, the Session is the “root” object here (think of it like a factory) - you need to keep it alive longer than your Sftp value.

In the code you gave, the Session is created inside the function but then dies at the end - nothing keeps it alive. The Sftp would be left with a dangling reference back to its Session if the compiler didn’t rightfully error.

So, you need to create a TcpStream first. Then create a Session using that stream. Both of these need to be kept alive longer than the Sftp you create as a third step.

If you need help understanding how to keep those alive then ask away. It’ll depend on the structure of your app so may want to elaborate a bit on that as well.


#3

Also, as a rule of thumb: if you find yourself with a function signature like fn foo<'a>(<no args with 'a lifetime>) -> <something that uses 'a>), then it’s wrong :slight_smile:. That’s the case with your sftp_connect function.

Unless you’re returning a 'static reference, all returned references must be tied to some input (i.e. arg) reference; otherwise there’s nothing to “anchor” the returned reference against - if you don’t have incoming references, and not returning a 'static reference, it means you have owned values only and cannot return references against them.


#4

Ok, so for now I think I understood this pretty well. It seems logical that my session doesn’t live long enough.
My question is then: How do I make it live long enough ? Do I have to declare it outside my function ? I tried to create the session inside the function with the given lifetime but wasn’t successful.

What I am doing is a simple conversion of my current PHP backend to Rust. I have already learned a lot about the language, but this isn’t something I understand really well for now.

This function will most likely be called once and it’s output will be used inside the scope it has been called in.
I wanted to understand how it works with this example because I know I will want to do this again some time.


#5

It sounds like the simplest solution, based on what you wrote, is to create the Session outside this function and then pass it in as a reference arg.


#6

Note that lifetime parameters don’t change the scope/lifetime of a value - they’re generic args, similar to generic type arguments (e.g. fn<T>). The real lifetime/scope of a value is determined by the compiler based on the code you write. Lifetimes parameters don’t change that, they only enforce lifetime relationships/constraints.

Also, a lifetime parameter means the caller decides the actual lifetime - you cannot change it or influence it in any way inside the function. Again, it’s similar to a generic type parameter: fn foo<T>; caller gives you a concrete T. Similarly, fn<'a> means caller determines what 'a really is.


#7

Ok, I think I get it, I really have to change my mindset to be able to use this language.
Now I have an issue with doing a substring of a Path, but I didn’t try a lot for now so I won’t post anything on that. My plan was to pass the path in a function and return the string (minus the first two characters). I have been able to do this on any other languages with predefined functions. I will try this to see if I can do it.

Thanks again for the help!


#8

If you’re coming from a GC’d language then yeah, it’ll be an adjustment. Rust forces you to think about ownership. There are several ways to set up ownership, but you have to think about it and make conscious decisions in that regard. This is generally not something you think about in GC’d languages.

Yeah, try it and if you can’t make it work post here.


#9

Maybe the simple solution is to change to a with_sftp function that takes a closure and rust it at the end.

An alternate to try is using rental
https://crates.io/crates/rental


#10

Maybe the simple solution is to change to a with_sftp function that takes a closure and rust it at the end.

For now I think this isn’t worth it, I will only use the sftp and ssh connection in a single function that handles every action that the user can send (creating a folder, creating a file, changing a file, getting a list of files and folders, etc.)
I wanted to know how to do this to be able to use the function in different other functions “if” I needed to. If my only options are to send the ssh connection into the function to make it work it kind of defeats the purpose.

What about a new structure ? with a new(String, String) function that creates the ssh connection, stores it in a Struct, creates the sftp connection and stores it here too. Kind of like it already works in the API but here I would only have to do something like :
let sftp_custom_struct: Sftp_custom = Sftp_custom::new("account", "passwd");
This would let me use the connection I want and should work since it’s in the same scope ?

Yeah, try it and if you can’t make it work post here.

I want your advice on this:

fn substring(str_to_sub: String, start: usize, end: Option<usize>) -> Result<String, String> {
    // Declaring char_size and size for later checks
    let char_size: usize = str_to_sub.chars().count();
    let size: usize = match end {
        Some(sz) => {
            sz
        },
        None => char_size,
    };
    // Early return because of an error
    if start >= size {
        return Err("Start size given is greater than end size".to_string());
    }
    if size <= char_size {
        Ok(str_to_sub.chars().skip(start).take(size - start).collect())
    } else {
        Err("String is shorter than the wanted end size given".to_string())
    }
}

Something doesn’t feel right in this function. It works, this is great, but I feel like it could be better ? I tried different methods and this is the one I like the most.
The thing I personally don’t like here is the creation of the Chars from the input string twice. But when I do a count it consumes the iterator :confused:.
Any improvement I can make ?
The other thing is that I don’t know the size of the string that will arrive.
And the third thing is that this will be sent to javascript to handle after. Would it be faster to use javascript’s built in substr() function ?


#11

A few comments/suggestions:

  1. Take &str as input rather than String unless you have a good reason for it.
  2. &str (and String, since you can deref it to get a &str) has a len method to give you the length.
  3. Return a slice (&str) back instead of String
  4. Once you’ve checked that the args are in bounds, use a range indexer to return the slice (eg input[start .. end])
  5. Use Option::unwrap_or instead of match statement for simple things (eg let end = end.unwrap_or(input.len()))

#12
fn substring(in_str: &str, first: usize, last: Option<usize>) -> Result<&str, &str> {
    let end: usize = last.unwrap_or(in_str.len());

    if first < end && end <= in_str.len() {
        Ok(&in_str[first .. end])
    } else {
        Err("Out of bounds")
    }
}

Oh ok, this is not the same at all… Thank you for those suggestions. It’s so much simpler.
I thought I couldn’t use the range indexer because it always told me that it wasn’t a fixed size.
Now I’m certain there are even better ways to do this but for my uses this will be perfect !

Second time I ask a question on this forum and I’m beginning to really like this community ! (I’m not usually the kind of guy to ask on forums when I’m blocked, but I read so much about how the community is great that I wanted to see by myself, needless to say: you’re all amazing!)

this is it for now, I’m sure I’ll have some other things to ask later, and I’m certain that it is not the last time I need help on something.
Nothing is better than having someone that actually know about


#13

This second substring() is using bytes so any incorrect character bound input will panic. eg substring("Löwe 老虎 Léopard", 7 , None);


#14

Very good point @jonh! My suggestion was based on the assumption that all input is single-byte chars. But then it should probably take and return &[u8] to make that clear.


#15

This is exactly what I was afraid of… I already know that I have characters that won’t work…
I already had the issue and solved it by converting it to a lossy utf8 but I lose some characters by doing this.
I will work on that more and make it work.

Do you think the first solution could work? I will tweak it and see how it works


#16

Well, I finally got it to compile how I wanted, and it actually works ! (took me about 2 days to understand how it works and to realize that it didn’t work the first time because I forgot to pass the tcp stream as a reference)

pub enum Type<'a> {
    Sftp {
        session: &'a ssh2::Session,
        sftp: ssh2::Sftp<'a>,
    }
}

impl<'a> Type<'a> {
    pub fn new_sftp(sess: &'a mut ssh2::Session, username: &'a str, password: &'a str, tcp: &TcpStream) -> Type<'a> {
        sess.handshake(tcp).expect("No handshake, no like");
        sess.userauth_password(username, password).expect("Wrong username / password");
        
        Type::Sftp {
            session: sess,
            sftp: sess.sftp().unwrap(),
        }
    }
}

(This is an Enum because there will be more that just Sftp. Ftps will soon be added too. I thought it would be great to have everything at the same place. Then I make private functions to manipulate files and get the list of directories, call them in my action_handler function (that gets a json from the client) and I’m all good.)

This almost works like I want. The only thing is that I need to create the tcp stream and ssh2 session outside.

This will be good enough, I can now say that this issue is solved. Sftp still takes .3 seconds to connect but, since I’ve been working on rust-ftp to make it work, this won’t be an issue.