Rust Nibbles – Gazebo: AnyLifetime

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

The Rust Gazebo Library contains a collection of proven Rust utilities in the form of stand-alone modules. In this series of blog posts, we’re going to cover some of the modules that make up the Gazebo Library. In today’s blog we cover the AnyLifetime trait. This blog is part of our Rust Nibbles series where we’ll go over the various Rust libraries we’ve made open source to learn more about what motivates their creation and how to use them.

Rust offers the Any feature, which serves as a helper for dynamic typing. With the Any property you can define the following:

fn print_if_string (arg: & dyn Any) {if let Some (string) = arg.downcast_ref ::() {println! (“It’s a string ({}): ‘{}'”, string.len (), string); } else {println! (“No string …”); }}

Here we take a value arg, the type of which is statically unknown, then test at runtime whether it is a string or not, and use it as a string if necessary. This code did not turn Rust into a dynamically typed language, and there is nothing insecure about our code. Under the hood, the Rust compiler generates a different TypeId for each type, and if the TypeId values ​​for any two values ​​match, their types are equivalent and Rust can safely convert between the two.

The Any feature works great, but it does impose the limitation that the type contained in Any is static. Which means no lifetimes in this context. In particular, we can’t use Any methods on a type like the following:

Structural value<'v> {…}

This limitation was particularly problematic for our Starlark language implementation, where most types actually contain a ‘v-Lifetime argument. To get around this limitation, we have defined AnyLifetime. Much like Any, the AnyLifetime feature in Pavillon :: any is defined as:

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

In the case of a value (or just a type) we have to specify the TypeId, which uniquely identifies this type. The only change here is that we define the property to generate the TypeId of the equivalent static type, e.g. B. for Value<'v> above we can use the TypeId of Value use<'static>:

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

Now we have access to the methods we know and love from Any, such as downcast_ref, but without the static limitation. The only drawback is that the implementation definition is uncertain. And it’s really 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 error-prone, methods of defining instances. Starting with the simplest, we can define:

#[derive(AnyLifetime)]
Structural value<'v> {…}

This works for types with no generic types and either null or a lifetime parameter. Next, if we need to define the instance for any type, even type aliases, we can use any_lifetime! Macro:

any_lifetime! (value<'v>)

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

uncertain impl<'v> AnyLifetime<'v> for value<'v> {any_lifetime_body! (value<'static>); }

Of these, the last one is unsafe as you are required to make sure the implementation type is the same as in the main part, but with static arguments for all lifetime arguments.

The biggest problem with this approach is that while Any is built into the compiler and has a rich implementation that provides implementations for each type, AnyLifetime requires specific instances. In addition, it is currently impossible to have an AnyLifetime implementation for Vec. to specify given an AnyLifetime for T, since such instances are not structurally composed (something we couldn’t implement with a nice API, but becomes possible with generic mapped types). Despite these limitations, we considered AnyLifetime to be essential in those cases where we needed it.

We hope this blog helps you understand the AnyLifetime feature, how to use it, and give you a good look at how it works. Look out for our next blog in this series where we discuss the comparisons in Gazebo that utilities provide for operations like comparison chaining.

Check out our previous blogs in the Gazebo series to learn more about the various features the Gazebo Library has to offer –
Pavilion – Prelude
Pavilion – dupe
Pavilion variants

About the Rust Nibbles series

At Facebook, we believe that Rust is a stellar language that shines on critical issues like storage security, performance, and reliability. We have joined the Rust Foundation to contribute to the growth, advancement, and adoption of Rust, and the sustainable development of open source technologies and developer communities around the world.

This blog is part of our Rust Nibbles series where we’ll go over the various Rust libraries we’ve made open source to learn more about what motivates their creation and how to use them. We hope this series helps you create amazing projects 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.

Comments are closed.