Rust Lifetime of a structs self contained reference

I am writing a simple http client using reqwest. In order to to integration tests on it, i want to create a setup() method returning a TestEnv struct with some instantiated values. It does not compile due to a circular reference in the Env struct.

Here is an MCVE playground

Is there a way to properly annotate the &api lifetime of the Api struct which was instantiated here?

Full code is here

This isn’t possible, and your function shows pretty nicely why:

fn setup<'a>() -> TestEnv<'a> {
    let api = Api { base_url: String::from("https://cats.org") };
    let cats_client = CatsClient { api: &api };
    
    TestEnv {
        api: api, 
        cats_client: cats_client
    }
}

A reference is just a pointer (thin or fat), so cats_client.api points to the api in the stack frame of setup. When setup finishes, the TestEnv is moved out into the caller’s stack frame, which would invalidate the pointer.

There is some work ongoing to allow self-references in some cases, such as when you’re referencing content that’s actually stored on the heap (e.g. a String slice) or can’t be moved (there’s a Pin family of traits in unstable Rust).

3 Likes

What is the idiomatic test setup() code then? Static state in the setup method?

Typically, you would instantiate things in multiple calls: hold the referenced data in a broader scope and pass it into setup as an argument. Static state could be used to achieve roughly the same thing, but may be troublesome in an async situation (I.e., it may require redesigning your objects to work with Arcs, Mutexes, and whatnot.) You’ll also have trouble creating an object which owns data and tries to reference it statically, because that is a contradiction.

The OO pattern of making an object construct itself isn’t really idiomatic in Rust. Consider how the lifetime 'a is bound: we declare a struct – defined to be valid for some lifetime parameter 'a – but how does the object know what that lifetime is if you don’t pass any references in? The only option is 'static; the compiler can’t make any assumptions about who is destroying the referenced data, so it requires the data to always be valid.

1 Like