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.
- 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. - Custom Trait:
ThreadedModel
Another solution is to declare a new Trait (ie:ThreadedModel
) that is constrained bySend
andSync
. This does work for the example above, but not for large bases with multiple traits, structs, etc.. TheSend
andSync
traits are marker traits and seems to behave very differently from normal Rust traits. - Macros
Another option is to use macros to implement theSend
andSync
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?