Optional Send+Sync implementations through a feature flag

Problem: I want to optionally implement Send+Sync for certain types/traits/function signatures/etc... through a feature flag.

Origin: If you are interested in more details of why I want to do this, you can read more on this PR (WIP: ?Send support).

Detailed Explanation:

Assuming the following code. (in this particular example a Trait)

pub trait TypeName {
    /// Returns a GraphQL type name.
    fn type_name() -> Cow<'static, str>;
}

A send-sync feature flag should result in two possibilities

The first one, the code remain as is

pub trait TypeName {
    /// Returns a GraphQL type name.
    fn type_name() -> Cow<'static, str>;
}

In the second, Send+Sync marker traits are added as constraints to the Trait.

pub trait TypeName: Send + Sync {
    /// Returns a GraphQL type name.
    fn type_name() -> Cow<'static, str>;
}

Solution: There are multiple ways to solve this.

  1. Conditional cfg_if
    Probably the most straighforward and workable solution. However, this has the disavdantage of creating two declarations of TypeName. Now, if you want to make changes to the Trait, you'd need to update two definition instead of one.
  2. Custom Trait: ThreadedModel
    Another solution is to declare a new Trait (ie: ThreadedModel) that is constrained by Send and Sync. This does work for the example above, but not for large bases with multiple traits, structs, etc.. The Send and Sync traits are marker traits and seems to behave very differently from normal Rust traits.
  3. Macros
    Another option is to use macros to implement the Send and Sync traits. Again, this does work for the simple example above but fails in more complex cases. I outline some of them below. The list is not exhaustive.

Challenging cases:

Case 1: This can be considered an edge case, however, and cfg_if can be used instead.

#[async_trait::async_trait]
// vs.
#[async_trait::async_trait(?Send)]

Case 2: The Send+Sync constrained is required for a particular type (Sync for T and Send+Sync for E). A general approach will not have the insight on where to apply these constraints.

impl<T: OutputType + Sync, E: Into<Error> + Send + Sync + Clone> OutputType for Result<T, E> {
// vs.
impl<T: OutputType, E: Into<Error> + Clone> OutputType for Result<T, E> {

Case 3: Similar situation to Case 2.

pub fn on_connection_init<F, R>(self, callback: F) -> WebSocket<S, E, F>
    where
        F: FnOnce(serde_json::Value) -> R + Send + 'static,
        R: Future<Output = Result<Data>> + Send + 'static,
    {
// vs.
pub fn on_connection_init<F, R>(self, callback: F) -> WebSocket<S, E, F>
    where
        F: FnOnce(serde_json::Value) -> R + 'static,
        R: Future<Output = Result<Data>> + 'static,
    {

Case 4: Another similar situation, not all Types have the same constraint. The constraint is also require inside the "where" of a Trait impl.

impl<K, V, S> InputType for HashMap<K, V, S>
where
    K: ToString + FromStr + Eq + Hash + Send + Sync,
    K::Err: Display,
    V: Serialize + DeserializeOwned + Send + Sync,
    S: Default + BuildHasher + Send + Sync,
{
//vs.
impl<K, V, S> InputType for HashMap<K, V, S>
where
    K: ToString + FromStr + Eq + Hash,
    K::Err: Display,
    V: Serialize + DeserializeOwned,
    S: Default + BuildHasher,
{

Preferred implementation:

The feature flag could implement or remove the marker traits. Both are fine and will serve the same purpose. If the Send+Sync constraint could locally be nullified or disabled, that's also a fair solution but am not sure of the feasibility of that in the Rust language.

A perfect implementation will be a macro where Send and Sync are redefined as "Send!" and "Sync!" and these get translated in compile time to either nothing or the relevant marker trait.

Your suggestions?

Before I get to a solution, I would like to point out that neither option (opting in to or out of Send + Sync) adheres to the strict additivity requirement of Cargo feature flags. Thus, you should basically not do this. The reason is:

  • If you make the traits opt-in through the feature, then you will cause breakage by requiring extra bounds. Code that compiled without the feature will not compile anymore if some types implementing TypeName are not Send + Sync.
  • If, on the other hand, you make the traits opt-out, then you will cause breakage by opting your own types out of implementing Send + Sync, which can cause downstream code relying on these bounds to not compile.

With that said:

this seems to be easy to implement using a procedural macro. This is the foundation of one such macro, except that it doesn't yet account for the + in trait bounds (it generates spurious + signs even if the traits are omitted). Fixing that is left to you as an exercise.

1 Like

Hey Carbonic Acid :smiley:

  1. Regarding the additivity of Cargo features, is it about "adding stuff/traits/constraints" or "adding features"? The default (in this case for async-graphql) is to have the Send + Sync bound. The "addition" is the support of the "no Send + Sync constraint". From this perspective, this is a feature for people who are operating in single threaded environment (ie: WASM).

  2. I'll be looking into this code. Thanks for this effort. I am a bit new to this, as of yesterday I was using "cargo expand" to see the generated code :confused:

I don't know in what sense you are asking. The strict additivity of features means that you must not break 3rd-party code by enabling features. That only works with traits if your feature creates additional trait impls. It does not work if it either disables impls or adds bounds.

I see the meaning of additivity now. But that also means that either way, this is going to break this rule. What's the idiomatic way to do it then?

I don't see a straightforward way other than providing two sets of traits/impls. The bulk of the functionality can be in a supertrait, and there could be a separate subtrait and a corresponding blanket impl, as in:

pub trait ThreadedTypeName: TypeName + Send + Sync {}
impl<T: TypeName + Send + Sync> ThreadedTypeName for T {}

Types must then implement TypeName only, and they will get the ThreadedTypeName impl automatically if and only if they are eligible.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.