Borrowing as Any non-'static

I am doing something quite straightforward. Just trying to get a ref to a struct after it was passed around as a trait in a Box. The usual help on as_any does not apply, because the lifetimes are not static. According to the searches I have made, one should first convert to Any, with some borrowing and then downcast. This almost works, but Rust gets insistent about 'static-ness.

One solution would be the following function. But how do I write it? Rust error suggestions are circular, as often is the case. (The lifetime on the reference, 'a, is perhaps not needed, but everything is annotated for debugging.)

pub fn z_to_a<'a, 'b>(input: &'a mut (dyn SomeTrait + 'b)) -> &'a mut (dyn Any + 'b)
where
   'a: 'b,
{
   // Implementation here.
}

The implementation is easy.

input

But you need to remove the where (have it wrong way around and it is implicit the other way.)
Write it so Any is a supertrait of SomeTrait (and make sure your not using an old version of rust.)

Ideally though, you should write code not to be dependent on downcasting.

Any requires 'static, you can't have a non-'static one, at least with the stdlib.

The reason for this is that Any needs to carry some runtime metadata to remember what was the original type: this is the TypeId. However lifetimes are "erased", they don't influence what gets compiled and don't exist at runtime, hence they can't be tracked by any.

You could technically track this lifetime statically, i.e. with some sort of dyn Any<'lifetime> but this is rather tricky: you need to track all the lifetimes of the underlying type and with the proper variance. better_any is a third-party crate that attempts to do (part of) this.

Thanks.

So the whole approach is not workable. This also explains the frustration of fighting the lifetime messaging. I'll post a separate message about how to extract the struct (reference) from a Boxed trait thereof. It cannot involve Any.

I decided to give up.

There remains the outstanding question of "How to get concrete struct ref from Box of trait where static lifetimes are unworkable?"

I have made a rather dangerous early design commitment in order to work around. If it proves not to work later, the wider project will be abandoned, because this commitment is in practice irrevocable.

The real reason for the foregoing needs is that I have to be able to call a function thus:

  function_i_do_not_control(&concrete_from_trait_a, &concrete_from_trait_b);  

You are definitely doing something very-very tricky.

If “lifetimes are not static” then you have a type that exists for a limited time. That's the only reason lifetimes exist: to tell the compiler how long certain references can be used for.

That means that all these lifetimes that you want to pass around should be represented by that trait. Otherwise there are no way to pass them around.

That's where the following requirement comes from:

What does it even mean? Where would concrete_from_trait_a and concrete_from_trait_b get their lifetimes that they need to exist?

Forget Any, for the time being… conceptually: who would pass lifetimes around and how?

Then you can try to invent some kind solution… so far the problem is not yet clear enough…

Unfortunately true. I oversimplified what I wrote on playground.
The code allows you to define the function without 'static. Maybe a clippy lint could spot such code.
At use it only compiles when 'static gets inferred. What would be better is downcast only works with types that are without lifetimes.

What that phrase even means?

How would “types without lifetimes” differ from 'static?

Every type have lifetime in Rust. No exceptions. Although some of these are 'static!

I was thinking along the lines of getting &'a mut (dyn Any + 'b) to work without forcing 'b to be 'static. I was hinting at generic types getting blocked at the downcast stage, since there is only one vtable no matter what what the lifetime was inferred. I failed to see the breaking flaw at the time of writing, in that you can currently create/use dyn Any when you concrete in 'static.

And that's precisely the problem.

What happens if your type is, in reality, more complex and accepts two or three lifetimes? Should they be merged together? If yes then how? If no then what happens when you go from that trait to a concrete type?

It's worse than that. dyn Any + 'b doesn't mean that type under dyn Any + 'b lives for 'b.

It means that it lives at least for 'b.

When you would attempt to go from dyn Any + 'b to a concrete type you would need to decide what to do with all other lifetimes which may hide there.

Not impossible, but very quickly becomes extremely convoluted and complicated.

Rust already seems to take the innermost lifetime. (I see this in my drastically alternative approach, which actually requires up to 3 lifetimes in some places, and definitely 2 for the struct impl the trait.)

If we set aside the Any+downcast aspects and suppose we were able to directly go from Box of (dyn SomeTrait + 'a) to borrow &'a mut MultiLifetimeStruct: would that be allowable, at least in robust handling of lifetimes?

[And I'll admit that this isn't quite "doing something quite straightforward", just mostly straightforward, :stuck_out_tongue: .]

That works fine when you convert to a trait. We are talking about the opposite process.

This would depend on details of “restoration process”: where would you take these “extra” lifetimes and what would you do with them.

You would need to define what “robust handling of lifetimes” means, too.

I'm not sure any process that involves bazillion subtle definitions and possibilities can be named “straightforward”.

It's only “straightforward” in a world of tracing GC where you may ignore lifetimes entirely and assume that GC would solve the lifetime issues.

&'static str has a lifetime, even though that's 'static.

i32 has no lifetime.

The difference is that if you coinsider the type with all lifetimes erased (as they are known at runtime), then &'?? str is no longer guaranteed to be 'static, but i32 is, meaning it would be safe to downcast a non-'static dyn Any to i32 but not to &'static str, even though both are 'static.

2 Likes

The type would be well formed (if you got the type of MultiLifetimeStruct actually correct, e.g. exactly matched what was coerced to dyn SomeTrait + 'a).

You probably don't want to actually preserve 'a exactly as you'd have to leak the box; you want something shorter.


If the outer lifetimes of your &'outer NonErasedTypes are ephemeral, and if you can type erase a function pointer or such, there may be an unsafe way forward. You would be doing something analogous to dyn Trait vtable calls, where you don't know the underlying type but you can create a valid reference to one anyway. I don't know all the safety considerations off-hand; you may need some trampoline for API reason, etc.

If I were going to attempt it I'd look into non-std but well-tested vtable implementations, like in tokio or such.

Care to write code that shows what you mean? This works fine:

fn foo<'a>(x: Box<dyn Any>) -> &'static str {
    *x.downcast::<&'static str>().unwrap()
}

fn bar() -> &'static str {
    let boxed: Box<dyn Any> = Box::new("Hello");
    foo(boxed)
}

pub fn main() {
    let test = bar();
    println!("{test}");
}

Is Any here not enough non-'static to show the difference?

Interesting problem, let me think it out loud (imagining there is no trait Any: 'static requirement):

To unsize T to dyn Any + 'b it has to be T: Any (trivial) and T: 'b. To downcast from dyn Any + 'b to U we need:

For any T such that type_id(T) == type_id(U) and T: 'b, T is a subtype of U.

Here I assume type_id ignores lifetimes, so the equality means that T and U have the same structure, they only differ in lifetimes.

Since the T: 'b is defined syntactically, the lifetimes in T have to outlive 'b (and can be as long as 'static).

So to allow the downcast, every lifetime in U in "covariant"[1] position has to be 'b and every lifetime in U in "contravariant"[2] position has to be 'static and U may not contain lifetimes in "invariant" positions (unless 'b = 'static maybe).

Is there any flaw in this reasoning?

I don't know how hard would it be to implement it nor how useful that would be, but it seems possible.


  1. like &'_ V ↩︎

  2. like fn(&'_ V) ↩︎

What you wrote uses the classic Any, which requires 'static content. However what I wrote is:

Ah. So you are not talking about Rust as it exists today, but about some other, hypothetical, language that may also exist.

Fair enough, but how is that hypothetical language is relevant to our discussion here?

Is there an RFC or some discussion that seriously propose to extend Rust in that direction?

You assume that you may have your cake and eat it too… but why are you even sure that may work?

Today, in a Rust as it exists today typeid can ignore lifetimes precisely because Any have that : 'static bound.

Internally Any uses typeid to distinguish types, but because of that bound we can be sure that types that are different stay different when compared via typeid.

If you remove : 'static bound then the question of whether having typeid that's no longer distinguishes between lifetimes is enough is immediately raised.

But that's the central question of the whole exercise: when language is extended, in some way, the most important part is sequence that connects that extension to the “business need”, something requirement that one may want or need, that's expressible in a layman terms – and then series of decisions that connect your obscure language extensions to these.

Otherwise it's way too easy to invent some busy work with no real benefit. Your proposal, essentially, would make shared references permitted in such Any type but uniique references illegal. And closures with references would all be receiving 'static references (which definitely reduces their usability). Is it really the good solution? Is it useful?

We don't know till we would see some concrete examples. Because we start with the requirement that Any: 'static doesn't work for us and then, immediately, turn around and slap extra : 'static requirements in a different place… have we actually solved anything? Have we actually made something worthwhile possible… or have we just made language more complicated for no good reason?

It's basically what OP wanted, I think that's quite relevant, no?

The point in general is that typeid must ignore lifetimes, and the : 'static bound on Any (and on downcast! both are required for safety with the current approach) is the way we current deal with it.

That's not completely correct, it would make lifetimes behind a mutable reference illegal, e.g. &'a mut i32 would be ok because it's covariant in 'a, but &'a mut &'b i32 would not be ok because it's invariant in 'b, not to mention that both lifetimes would be the same which is an even bigger issue.

Given the download count of the better_any crate I think there are some that find it useful.

Even within the stdlib there are some places where a non-'static Any looks very useful, for example the std::error::Error::provide API is effectively emulating a non-'static Any, and the std::task::Context::ext API is severely limited by allowing to hold only a reference to a 'static type.

1 Like