Skip to content

Cyclic Arc and Rc Builder #90

Closed
Closed
@eholk

Description

@eholk

Proposal

Problem statement

It can be helpful to build cyclic ARC structures. Currently there is Arc::new_cyclic, but this does not work if the supplied data_fn returns a Result or is an async fn.

Motivation, use-cases

Consider the following example that creates a cyclic Arc structure using Arc::new_cyclic:

use std::sync::{Arc, Weak};

struct Gadget {
    me: Weak<Gadget>,
}

impl Gadget {
    fn new(me: Weak<Gadget>) -> Self {
        Gadget { me }
    }
}

fn create_gadget() -> Arc<Gadget> {
    Arc::new_cyclic(|gadget| Gadget::new(gadget.clone()))
}

We may want to make Gadget::new be an async function that returns a Result indicating it might fail. We might be tempted to replace the Arc::new_cyclic(|gadget| Gadget::new(gadget.clone())) with Arc::new_cyclic(|gadget| async { Gadget::new(gadget.clone()).await? }), but doing so gives the following errors:

error[E0277]: the trait bound `Gadget: FromResidual<Result<Infallible, std::io::Error>>` is not satisfied
  --> src/lib.rs:15:36
   |
15 |     Arc::new_cyclic(|gadget| async { Gadget::new(gadget.clone()).await? })
   |                                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |                                    |
   |                                    the trait `FromResidual<Result<Infallible, std::io::Error>>` is not implemented for `Gadget`
   |                                    required by a bound introduced by this call

error[E0308]: mismatched types
  --> src/lib.rs:15:30
   |
15 |     Arc::new_cyclic(|gadget| async { Gadget::new(gadget.clone()).await? })
   |                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected struct `Gadget`, found opaque type
   |
   = note:   expected struct `Gadget`
           found opaque type `impl Future<Output = Gadget>`

Some errors have detailed explanations: E0277, E0308.
For more information about an error, try `rustc --explain E0277`.
error: could not compile `playground` due to 2 previous errors

There is currently no obvious way to make this work using Rust, and many if not all of the non-obvious ways involve unsafely depending on Arc implementation details.

Solution sketches

We propose adding a new CyclicArcBuilder and corresponding CyclicRcBuilder with roughly the following API:

pub struct CyclicArcBuilder<T> { /* details omitted */ }

impl<T> CyclicArcBuilder<T> {
    pub fn new() -> Self { /* details omitted */ }
    
    /// Obtain a `Weak<T>` to the allocation. Attempting to
    // `upgrade` the weak reference prior to invoking `build`
    // will fail and result in a `None` value.
    pub fn weak(&self) -> Weak<T> { /* details omitted */ }
    
    // Finish construction of the `Arc<T>`.
    pub fn build(self, data: T) -> Arc<T> { /* details omitted */ }
}

As an alternative (or in addition) to CyclicArcBuilder::new, we could also add a new_cyclic_builder() method to Arc.

Using this API would allow us to solve the problem in the previous section as follows:

use std::io;
use std::sync::{Arc, Weak};

struct Gadget {
    me: Weak<Gadget>,
}

impl Gadget {
    async fn new(me: Weak<Gadget>) -> io::Result<Self> {
        Ok(Gadget { me })
    }
}

async fn create_gadget() -> io::Result<Arc<Gadget>> {
    let builder = Arc::new_cyclic_builder();
    let gadget = Gadget::new(builder.weak()).await?;
    Ok(builder.build(gadget))
}

Alternative: More cyclic Constructors

An alternative we considered was adding a collection of new_cyclic variants, like try_new_cyclic, async_new_cyclic, and async_try_new_cyclic. This approach adds a lot of API surface while CyclicArcBuilder subsumes all of these use cases and more (for example, the original arc_new_cyclic issue mentioned multiple arity variants).

Alternative: Keyword Generics

The Keyword Generics Initiative would make it possible to use the existing Arc::new_cyclic constructor in fallible and async contexts by sharing a single implementation. The initiative is still in the early stages though, while adding CyclicArcBuilder would allow making incremental progress sooner. CyclicArcBuilder is also strictly more general, since it can enable the multi-arity use case mentioned above while keyword generics would not.

One advantage of both of these alternatives is that they are likely to present a simpler API. CyclicArcBuilder is quite general, but arguably not as ergonomic. However, the kind of code that requires something like new_cyclic would likely already in a situation that would benefit from an even more flexible API.

Alternative: Multi-phase Construction

One option is to split the construction of the object into a fallible and infallible portion, like this example take from this comment:

let mut obj = try_obj()?;
let rc = Rc::new_cyclic(move |this| {
    obj.this = Weak::clone(this);
    obj
});

This approach does work, but there are some downsides. It requires constructing the object in an invalid state and then patching it up afterwards, which is likely more error-prone than being able to make the object correct-by-construction using the builder API.

One issue this this approach is that it can either expose implementation details that should be private, or require factoring the code differently. Suppose we wanted to have a ServiceManager that managed multiple objects that implement the Service trait. Using CyclicArcBuilder, we might write something like this:

struct ServiceManager {
    foo_service: Arc<dyn Service>,
    bar_service: Arc<dyn Service>,
}

impl ServiceManager {
    fn new() -> Arc<Self> {
        let this = Arc::builder();
        this.build(Self {
            foo_service: FooService::create(this.weak()),
            bar_service: BarService::create(this.weak()),
        })
    }
    
    fn get_foo_service(&self) -> Arc<dyn Service> { ... },
    fn get_bar_service(&self) -> Arc<dyn Service> { ... },
}

Without CyclicArcBuilder, ServiceManager::new would either need access to the service internals, or we'd need to add a method to the Service trait to finalize construction. The first option would look like this:

impl ServiceManager {
    fn new() -> Arc<Self> {
        let mut obj = Self {
            foo_service: FooService::create(),
            bar_service: BarService::create(),
        };
        Arc::new_cyclic(move |this| {
            obj.foo_service.service_manager = this.clone();
            obj.bar_service.service_manager = this.clone();
            obj
        })
    }
}

We can work around needing direct access to the service_manager field by using something like an activate method:

impl ServiceManager {
    fn new() -> Arc<Self> {
        let mut obj = Self {
            foo_service: FooService::create(),
            bar_service: BarService::create(),
        };
        Arc::new_cyclic(move |this| {
            obj.foo_service.activate(this.clone());
            obj.bar_service.activate(this.clone());
            obj
        })
    }
}

This avoids leaking internal details of the service implementation, but it still less than ideal. One, it requires complicating the Service trait with an activate method. More significantly though, this approach is a maintenance hazard. If in the future we added baz_service to ServiceManager, the compiler would guarantee that we call BazService::create() but it would be easy to forget to call obj.baz_service.activate(this.clone()). On the other hand, the builder pattern makes this mistake impossible.

Links and related work

This proposal builds upon and generalizes some of the alternatives discussed in Tracking Issue for arc_new_cyclic (#75861). Some specific comments have been linked above, but the whole thread is relevant.

What happens now?

This issue is part of the libs-api team API change proposal process. Once this issue is filed the libs-api team will review open proposals in its weekly meeting. You should receive feedback within a week or two.

Metadata

Metadata

Assignees

No one assigned

    Labels

    T-libs-apiapi-change-proposalA proposal to add or alter unstable APIs in the standard libraries

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions