Own Arc implementation outside of standard library?

Hello,
I'm still pretty new to Rust. Coming from C++ I assumed a good beginner problem to learn would be to write my own shared pointer implementation. Also I wanted to build something more similar to C++'s boost::intrusive_ptr.

I started off with the example given in the nomicon but ran into problems as soon as I wanted to use trait objects. The following line does not work with the implementation given there.

let x: Arc<dyn MyTrait> = Arc::new(MyStruct { x: 0 });

A little bit of searching brought me to the standard library implementation and I guess my problem has to do with core::ops::CoerceUnsized that I can't use outside of the standard library. Somehow related I found https://github.com/rust-lang/rust/issues/27732.

So my practical questions:

  • Am I right with my assessment?
  • Is there any possibility to write a fully qualified Rc / Arc Implementation in my own code?

Thanks.

1 Like

It's possible in nightly, but not on stable. The std lib uses lots of nightly features internally. In many cases it's for performance, but also to provide things like unsized coercions. So providng a fully featured implementation may require nightly. (As it does in this case)

5 Likes

Thank you.

So I can experiment on nightly which is sufficient for what I want to do right now.
(But as soon as I want to use it in any public "production crate" I'd have to reevaluate the situation and look for other solutions...?)

Yes, at least for the time being you will need to find other ways or ask your users to use nightly.

So, I have copy-pasted (and tweaked) the implementation there reaching

  1. Initial state;

  2. Then, I have loosened the <T> generics present in that code to <T : ?Sized>: Playground;

  3. Finally, unless you have nightly, you will need to use helper crates that use a bit of unsafe to feature the coercions in stable Rust. So here is one which is being released as we speak:

  • Right now supporting arbitrary custom traits is a bit unergonomic in that first you need to write a dummy function pointer fattener: fn fatten_ptr<'lt> (p: *const (impl 'lt + Trait)) -> *const (dyn 'lt + Trait) { p } and then use a bit of unsafe to assert that this function you provide is indeed as dummy as it looks.

    I'll soon make a PR to offer a non-unsafe (macro-based) interface for ease of use :slight_smile:

1 Like

But as soon as I want to use it in any public "production crate" I'd have to reevaluate the situation and look for other solutions

Not necessarily. You can release a crate which only compiles on nightly, but that will mean that it can only be consumed by crates or users which are also on nightly. That may reduce your audience, but it's not necessarily a deal-breaker; rocket is a very popular crate, probably the most popular web framework, which targets nightly.

Keep in mind though that if you enable a feature gate (which requires nightly), then that feature may be unstable, and is subject to backwards-incompatible changes. This is in contrast to the rest of the standard library and language which maintains backwards-compatibility. So this may mean that a future rust update may break your code, and require you to update your code to compile again.

Well, its master branch can be compiled on stable since long ago. It's just they've not made the breaking change version release yet.

1 Like

By the way, I think your initial code had concurrency bugs:

let inner = unsafe { self.ptr.as_ref() };
if inner.rc.fetch_sub(1, Ordering::Relaxed) != 1 {
    atomic::fence(Ordering::Release);
    return;
}
atomic::fence(Ordering::Acquire);
unsafe {
    drop::<Box<ArcInner<T>>>(Box::from_raw(
        self.ptr.as_ptr()
    ));
}

In this code, because the fetch_sub is Relaxed it is allowed to be ordered before previous uses of the Arc, which can cause use after frees if another thread comes along and frees the Arc just after you fetch_sub. The only place a Release fence could go in that code is before the fetch_sub, to show to the compiler that the fetch_sub must occur after you finish using the Arc:

let inner = unsafe { self.ptr.as_ref() };
atomic::fence(Ordering::Release);
if inner.rc.fetch_sub(1, Ordering::Relaxed) != 1 {
    return;
}
atomic::fence(Ordering::Acquire);
unsafe {
    drop::<Box<ArcInner<T>>>(Box::from_raw(
        self.ptr.as_ptr()
    ));
}

But at this point you should just use a Release fetch_sub like ::alloc.

1 Like

You're completely right, I applied the load-acquire to store-release symmetry incorrectly, good catch! Thanks! Now I have an actual example to remember to avoid making this mistake in the future :slightly_smiling_face: (I'll edit my playground link to fix it).

1 Like