Support both Rc and Arc in a library

Hello,

I have created a library that uses Rc under the hood.
Now, I would additionally like to provide a thread-safe version of my library, using Arc.
My strategy was to introduce a feature (threadsafe) which sets the implementation as follows:

#[cfg(not(threadsafe))]
type RcS = Rc<String>;
#[cfg(threadsafe)]
type RcS = Arc<String>;

Now, I would like to use both Rc and Arc versions of my library in the same binary, so users can choose at runtime whether to use concurrency.
However, this seems to imply that I would need to depend twice on my library with different features, which seems to be prohibited by cargo:

error: the crate `my-binary` depends on crate `my-library` multiple times with different names

What would be a good strategy to support both Rc and Arc in a library and allow a program to use both versions?

There’s no good solution here. I suggest doing the same as im-rs: https://github.com/bodil/im-rs.

They publish two different crates form the same source, im and im-rc and, with some buid.rs hackery enable a cfg option based on the package name.

3 Likes

First, it is not recommended to use feature flag for swapping incompatible types.
Feature flags of a library can be enabled by indirect dependencies, and then all dependencies are linked to the library with the flag enabled, even if some of them did not enable the flag.

Second, it is difficult to change generic type (Rc<_> and Arc<_>) using type parameters.
For example, defining RcS to make RcS<ThreadLocal> == Rc<String> and RcS<ThreadSafe> == Arc<String> is hard, but maybe possible.
If you want to do this, archery crate would be useful.

2 Likes

Although I love ::im's approach to the problem (thanks @matklad for pointing that one up), if you want a runtime switch, you need to have your collections have both alternatives available simultaneously. So this requires, once you split you crate between lib and app, using ::im's trick to depend on both libs, or what I think is more simple here, use the generic approach @nop_thread has mentioned:

fn main ()
{
    fn main<P : ::archery::SharedPointerKind> ()
    {
        // actual `main` logic *after* knowing if multithreaded
        // use YourCollection<P>
    }

    if multithreaded() {
        main::<::archery::ArcK>()
    } else {
        main::<::archery::RcK>()
    }
}
1 Like

Thank you all for your answers! I really appreciate it!

I decided to adopt an approach similar to what im is doing:
I created two modules called arc and rc inside my crate, which contain the code for the data structures using Arc and Rc, respectively. However, most of the files from the arc module are actually symlinks to their counterparts in the rc module, with the exception of one file (sharing.rs) which defines the actual type to be Rc or Arc.
You can see this at: https://github.com/01mf02/kontroli-rs.

1 Like

Rather than symlinks, you can use the following "trick" that uses only Rust-specific constructs, and is thus OS-agnostic (any platform will be able to clone the repo and make it work):

tree

foo
├── Cargo.toml
└── src
    ├── common
    │   ├── mod.rs
    │   └── some_submodule.rs
    └── lib.rs

src/lib.rs

pub
mod multi_threaded {
    use ::std::sync::{
        Arc, Weak,
        RwLock,
    };

    include!("common/mod.rs");
    /* or:
    #[path = "../common/mod.rs"]
    mod common;
    pub use common::*;
    // but it requires that there be a `src/multi_threaded` dir */
}

pub
mod single_threaded {
    mod private {
        use ::core::cell::{RefCell, Ref, RefMut};

        pub
        struct RwLock<T> (
            RefCell<T>,
        );

        // Mimic `RwLock`'s API
        impl<T> RwLock<T> {
            pub
            fn new (value: T)
              -> Self
            {
                Self(RefCell::new(value))
            }

            pub
            fn read (self: &'_ Self)
              -> Result<Ref<'_, T>, ::core::convert::Infallible>
            {
                Ok(self.0.borrow())
            }

            pub
            fn write (self: &'_ Self)
              -> Result<RefMut<'_, T>, ::core::convert::Infallible>
            {
                Ok(self.0.borrow_mut())
            }

            // etc.
        }
    }
    use ::std::rc::{Rc as Arc, Weak};
    use private::RwLock;

    include!("common/mod.rs");
}

src/common/mod.rs

// magically `Arc`, `Weak` and `RwLock` are in scope here

/* If using the `mod common;` technique, then you have to:
use super::{Arc, Weak, RwLock};
// so that it is explicit that such items are in scope */

pub
struct MyShared<T> (
    Arc<RwLock<T>>,
);

mod some_submodule; // etc.

4 Likes

Thanks for your follow-up answer, Yandros.
I implemented it in this commit and it works perfectly. :slight_smile:

For people who read this thread after us:
The solution of Yandros and my original symlink solution both suffer from the fact that doctests from the common module are being executed twice.
This can be prevented using an approach as follows:

#[cfg(not(doctest))]
pub mod arc {
    use alloc::sync::Arc as Rc;
    include!("common/mod.rs");
}
pub mod rc {
    use alloc::rc::Rc;
    include!("common/mod.rs");
}
1 Like

Why is that a bad thing? Wouldn't you want to test both versions?

1 Like

It would be actually good to be able to test both versions, but I do not know how to do that.
The problem is that in each doctest, I have to choose which implementation I want to test, and I do not know how to choose that in dependency of which module I'm currently in.

An example might be due. Suppose that the following doctest is in the common module:

/// # use mycrate::rc::MyShared;

How can I make this test use rc when run in the rc module, and arc when in the arc module?

If this was C++, you could "just" add a template template parameter that is either Rc or Arc (without the actual type!).
Last time I checked Rust did not have higher order typing capabilities, has that changed or will that change soon?

Nope and probably nope.

GATs (Generic Associated Types) are coming, and it's been pointed out that they're as powerful as HKTs in some important ways. Hence "probably nope": It's going to take a while for GATs to get implemented, and it's a little subjective whether GATs count as "higher order typing capabilities", but they can probably solve the Rc/Arc problem.

What will almost certainly not happen (because I've never seen anyone even propose this, much less get support for it) is Rust adding completely blind/unrestricted/??? type parameters that mimic the default behavior of C++'s type parameters. It's probably worth emphasizing here that C++ only ever supported HKTs by accident, and they've never been "first-class".

cargo fmt does not format modules imported with the include! macro. On the other hand, using #[path = "../common/mod.rs"] makes cargo fmt consider the files.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.