Str.to_owned() lifetime

This question is on lifetimes for arguments to the aws_sdk_s3::Region::new constructor (Region in aws_sdk_s3 - Rust)

I have an async function that has a parameter: region: Option<&str>

This simplified line gives a compiler error: let x = aws_sdk_s3::Region::new(region.unwrap());

error[E0521]: borrowed data escapes outside of function
  --> aws_s3.rs:65:17
   |
37 |     region: Option<&str>,
   |     ------         - let's call the lifetime of this reference `'1`
   |     |
   |     `region` is a reference that is only valid in the function body
...
65 |         let x = aws_sdk_s3::Region::new(region.unwrap());
   |                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |                 |
   |                 `region` escapes the function body here
   |                 argument requires that `'1` must outlive `'static`

For more information about this error, try `rustc --explain E0521`.

First, I don't see how the variable escapes the function body anyway, nothing created in that function is outlasting the function body, and in the simplified example, x was never used again.

Second, can I be right in thinking that the AWS api is expecting only static string references? That would be incredible.

Yet a simple change to use to_owned():
- let x = aws_sdk_s3::Region::new(region.unwrap().to_owned());
+ let x = aws_sdk_s3::Region::new(region.unwrap());
will apparently resolve this error.

The result of to_owned() is surely a heap-allocated string with a slightly shorter lifetime than the original parameter, so why would it fix the problem?

Surely it could not be that the parameter only lasts up till the first async/await call?

The documentation for Region constructor is: Region in aws_sdk_s3 - Rust and shows that it takes region: impl Into<Cow<'static, str>> and I'm not quite sure what one of those is. Is it the str that is required to be 'static ?

In passing a str.to_owned() which I understand emits a heap allocates string, does that become owned by the Region instance or is it destroyed when the Region constructor returns?

I didn't use the from_static constructor because I'm sure my argument is not a region: &'static str

What am I missing?

It's not. It's an owned String (as the name suggests), which is allowed to live however long it needs - unlike &'a str, which must be dropped before the end of 'a.

Cow<'a, T> is an enum which contains either &'a T or <T as ToOwned>::Owned - in this case it's either &'static str or String.

That's not important, since you give the ownership away anyway; but the API is designed as if the passed value is being held owned by Region, and so should be treated as such (deviation from that, if any, are the internal implementation details).

2 Likes

It is not. It's owned, so if you pass it to a function, it gets moved into that function, and is thus kept alive.

It becomes owned. See the definition of the Region struct.

Again, just check the definition. A Cow<'a, T> is just

enum Cow<T: ?Sized> {
    Borrowed(&'a T),
    Owned(T::Owned),
}

and if 'a = 'static and T = str, then it contains either a &'static str or an owned String. This is a common pattern for storing string literals without heap-allocating and copying them unnecessarily, while also providing the option for passing owned data so as to avoid the proliferation of lifetimes.

3 Likes

So are we saying that although region outlived object Region, the strict API explicitly demanded either 'static' or Owned`

But as the owned string is apparently not heap allocated by .to_owned it must be stack allocated in my function scope... surely it ended up heap allocated anyway when the constructor took ownership? (Is that what move trait does?

(But given how async smears what would be stack allocations all over the heap anyway (with the iterator implementation of async) surely it was heap allocated anyway?)

No. The API needs a &'static str or an owned value if it doesn't want to tie itself to another borrow (that's what I meant by preventing the proliferation of lifetime annotations). It's not any stricter than it needs to be.

The buffer, i.e. the actual character/byte array pointed by a String is always heap-allocated. That's what the String type does. However, the handle (the triplet consisting of the pointer to the buffer, its capacity, and its actually used length, i.e. the String value itself) might live anywhere, including the stack or the heap. Regardless of where it lives, it conceptually owns the pointed heap buffer and deallocates it when destroyed. This is why it doesn't need a lifetime annotation – it doesn't borrow anything.

There is no move trait, and moves don't do anything in Rust. They cause the moved-from place to be invalidated, which is a purely compile-time thing. In addition, they might actually copy memory around, bit-by-bit, dumbly – but that's almost always optimized away.

1 Like

The buffer, i.e. the actual character/byte array pointed by a String is always heap-allocated

Sure, that's what I meant when I originally said heap-allocated.

So although in this case the region parameter reference actually happened to live long enough for my use, the API and/or Rust weren't willing/able to track that. I can see why, a simple change elsewhere to re-use the config that the Region was passed would lead to a more confusing dependency error.

thanks for your explanations

I'm sorry that I can't mark both as solution - thank you all the same.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.