Trait bounds by feature

I have the following situation: My library provides async functionality. Right now, I have some Send bounds flying around, since the runtime might be using different thread. I'd like to offer a version that works with single-threaded runtimes, that therefore relaxes the Send bounds. I've already tried simply removing all Send bounds, and it seems to work. I'm now pondering how to avoid simply duplicating all code.

Is there a way I can put a Send bound on things depending on a selected feature? Maybe something like

impl<Q> Clone for Dummy<Q>
where
  #[cfg(not(feature = "nosend"))] Q: AsyncWrite + Send + Sync + Unpin + 'static,
  #[cfg(feature = "nosend")] Q: AsyncWrite + Sync + Unpin + 'static,
{
  fn clone(&self) -> Self {
    Dummy {
      q: self.q.clone(),
    }
  }
}

I've tried making a macro for trait bounds to no avail, too. The main point here is to not repeat the impl body, because that's really the same, verbatim.

Or should I try a proc-macro? Those feel tough, but if I can pull it off, I'm going to learn them :slight_smile:
Thanks for any pointers!

Note that features must be additive. If I use two crates A and B, both of which depend on your crate C, but only one of them uses the nosend feature, then both will be given the dependency C with the feature enabled. You have to write your code such that anything that uses your crate without the feature, will also compile if it's enabled.

That said, why would you have a Send or Sync requirement on the Clone impl at all? If there is no Send requirement, it works regardless of whether or not the inner type is Send.

Do you have some sort of type erasure with Box<dyn Trait> that requires you to manually handle Send on the wrapper?

Huh, well, that's basically impossible. Fundamentally, it's about async runtimes: Using the library with tokio means one has to use the types from tokio (e.g. the asynchronous versions of Stdin/Stdout), and those are Send, so it's possible to use it single- or multithreaded. If using gio, one has to use the type from that, but the types are not Send, so one can only uses it singlethreaded, which works out since the runtime accepts non-Send futures. See e.g. the Spawner trait which I use so I can put the futures onto the runtime used (like here) which needs to work with Send futures in the one case, and non-Send ones in the other.

Reconciling different async runtimes seems hard :slight_smile: But I want to go as far as possible. It's hard to imagine a scenario where it might sense to depend on two versions of the lib with Send and no-Send.

Oh that was just the shortest code example I could find. I'm in the process of removing bounds as far as possible as a preparation, but I won't get rid of, e.g. those.

I took a look at your code. It seems like you already have support for tokio and async-std but are adding support for a third, namely gio? It can be ok if you only expect the crate to be a top-level dependency, but be aware of the compatibility issues.

Think about introducing a new trait that implements AsyncWrite, Unpin, 'static and sometimes Send to simplify your code. Then you can add cfgs on just that instead of everywhere.

1 Like

Exactly.

Huh, but I thought implied trait bounds don't yet work, so I'd have to write out those anyways? That seems like a very good shot it if works, thanks!

That did work indeed, thanks a lot!

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