Back to News for Developers

Rust Nibbles - Gazebo: AnyLifetime

July 20, 2021ByNavyata Bawa

This article was written in collaboration with Neil Mitchell, a Software Engineer in the Developer Infrastructure organization at Facebook.

The Rust library Gazebo contains a collection of well-tested Rust utilities in the form of standalone modules. In this series of blog posts, we will cover some of the modules that make up the Gazebo library. In today’s blog, we will cover the trait AnyLifetime. This blog is a part of our Rust Nibbles series, where we go over the various Rust libraries we have open-sourced to learn more about what motivated their creation and how one can use them.

Rust provides the trait Any which serves as its helper for dynamic typing. Using the Any trait you can define:

fn print_if_string(arg: &dyn Any) {
    if let Some(string) = arg.downcast_ref::<String>() {
        println!("It's a string({}): '{}'", string.len(), string);
    } else {
        println!("Not a string...");
    }
}
        

Here, we are taking a value arg whose type is not statically known, then testing at runtime whether it is a String or not, and if it is, using it as a String. This code hasn’t turned Rust into a dynamically typed language, and there is nothing unsafe in our code. Under the hood, the Rust compiler generates a distinct TypeId for each type, and if the TypeId values match for any two values, their types are equivalent and Rust can safely convert between the two.

The Any trait works great, but it does impose a constraint that the type contained in the Any is 'static. Which, in this context, means no lifetimes. In particular, we can’t use Any methods on a type such as:

struct Value<'v> {...}
        

This restriction was particularly problematic for our Starlark language implementation, where most types do indeed contain a 'v lifetime argument. To work around this limitation, we defined AnyLifetime. Very similarly to Any, the AnyLifetime trait is defined in gazebo::any as:

pub unsafe trait AnyLifetime<'a>: 'a {
   fn static_type_id() -> TypeId where Self: Sized;
   fn static_type_of(&self) -> TypeId;
}
        

Given a value (or just a type), we need to provide the TypeId which uniquely identifies this type. The only tweak here is that we define the trait to produce the TypeId of the equivalent static type, e.g. for Value<'v> above we can use the TypeId of Value<'static>:

unsafe impl<'v> AnyLifetime<'v> for Value<'v> {
    fn static_type_id() -> TypeId {
        TypeId::of::<Value<'static>>()
    }
    fn static_type_of(&self) -> TypeId {
        TypeId::of::<Value<'static>>()
    }
}

        

Now we have access to the methods we know and love from Any, such as downcast_ref, but without the 'static constraint. The only downside is that unsafe in the definition of the implementation. And it really is unsafe - if we claim that the TypeId of Value is that of String, we can convert between the two types at runtime, and things will go horribly wrong (with a segfault, most likely).

The solution is a series of increasingly powerful, but increasingly more error prone methods of defining instances. Starting with the simplest, we can define:

#[derive(AnyLifetime)]
struct Value<'v> {...}
  

This works for types with no generic types and either zero or one lifetime parameters. Next, if we need to define the instance for any type, even type aliases, we can use the any_lifetime! macro:

any_lifetime!(Value<'v>)         
        

And finally, if we need ultimate flexibility, we can define the instance head ourselves and just use a macro to define the body:

unsafe impl<'v> AnyLifetime<'v> for Value<'v> {
    any_lifetime_body!(Value<'static>);
}
        

Of these, the final one is unsafe, as you are duty-bound to ensure the implementation type is the same as that in the body, but with static for all lifetime arguments.

The biggest problem with this approach is that while Any is built into the compiler and has a blanket implementation that supplies implementations for every type, AnyLifetime requires specific instances. Furthermore, it’s currently impossible to give an AnyLifetime implementation for Vec<T> given an AnyLifetime for T, because such instances don’t compose structurally (something we’ve failed to implement with a nice API, but may become possible with generic associated types). Despite these limitations, we’ve found AnyLifetime essential in the cases we’ve needed it.

We hope that this blog helps you understand the AnyLifetime trait, how to use it and gives you good insight into what it does. Look out for our next blog in this series, where we discuss the Comparisons in Gazebo, which provides utilities for operations such as comparison chaining.

Be sure to check out our previous blogs in the Gazebo series to learn more about the various features the Gazebo library has to offer -
Gazebo - Prelude
Gazebo - Dupe
Gazebo - Variants

About the Rust Nibbles series

We at Facebook believe that Rust is an outstanding language that shines in critical issues such as memory safety, performance and reliability. We joined the Rust Foundation to help contribute towards the growth, advancement and adoption of Rust, and towards sustainable development of open source technologies and developer communities across the world.

This blog is a part of our Rust Nibbles series, where we go over the various Rust libraries we have open-sourced to learn more about what motivated their creation and how one can use them. We hope that this series helps you create amazing projects by using these libraries and encourages you to try them out.

To learn more about Facebook Open Source, visit our open source site, subscribe to our YouTube channel, or follow us on Twitter and Facebook.