#send-future #send #boxed-future

no-std futures_kind

Abstractions over Send and !Send futures

2 releases

Uses new Rust 2024

0.2.0 Jan 26, 2026
0.1.1 Jan 24, 2026
0.1.0 Jan 14, 2026

#348 in Asynchronous

Download history 3/week @ 2026-01-08 327/week @ 2026-01-15 393/week @ 2026-01-22

723 downloads per month

MIT/Apache

22KB
83 lines

futures_kind

Abstractions over Send and !Send futures in Rust.

Motivation

Async Rust has a fragmentation problem: some runtimes require Send futures (like tokio and async-std), while others work with !Send futures (like single-threaded executors or Wasm environments). This forces library authors to either:

  1. Duplicate their async trait implementations for both Send and !Send variants, including all consumers (e.g. MyService and MyLocalService)
  2. Force all users to use Send futures, excluding legitimate !Send use cases
  3. Create separate crates or feature flags for each variant

This duplication is verbose, error-prone, and increases maintenance burden.

Approach

futures_kind provides an abstraction that allows you to write async code once and support both Send and !Send futures through generic implementations.

use futures_kind::{FutureKind, Sendable, Local};
use futures::future::{BoxFuture, LocalBoxFuture, FutureExt};

// Define your trait once, generic over the future kind
pub trait Service<K: FutureKind> {
    fn handle<'a>(&'a self, x: u8) -> K::Future<'a, u8>;
}

struct MyService;

// Implement for both Send and !Send with minimal boilerplate
impl Service<Local> for MyService {
    fn handle<'a>(&'a self, x: u8) -> LocalBoxFuture<'a, u8> {
        async move { x * 2 }.boxed_local()
    }
}

impl Service<Sendable> for MyService {
    fn handle<'a>(&'a self, x: u8) -> BoxFuture<'a, u8> {
        async move { x * 2 }.boxed()
    }
}

Now users can choose which variant they need:

use futures_kind::{FutureKind, Sendable, Local};
use futures::future::{BoxFuture, LocalBoxFuture, FutureExt};

trait Service<K: FutureKind> {
    fn handle<'a>(&'a self, x: u8) -> K::Future<'a, u8>;
}

struct MyService;

impl Service<Local> for MyService {
    fn handle<'a>(&'a self, x: u8) -> LocalBoxFuture<'a, u8> {
        async move { x * 2 }.boxed_local()
    }
}

impl Service<Sendable> for MyService {
    fn handle<'a>(&'a self, x: u8) -> BoxFuture<'a, u8> {
        async move { x * 2 }.boxed()
    }
}

// For Send-required runtimes like tokio
async fn use_sendable(service: &impl Service<Sendable>) {
    let result = service.handle(42).await;
}

// For !Send runtimes like Wasm or single-threaded executors
async fn use_local(service: &impl Service<Local>) {
    let result = service.handle(42).await;
}

Or thread through the FutureKind parameter and delay to compile time. This is typesafe, and will complain if you try to send between threads at which point you'll know that you need to specialize to Sendable.

use futures_kind::{FutureKind, Sendable, Local};
use futures::future::{BoxFuture, LocalBoxFuture, FutureExt};

trait Service<K: FutureKind> {
    fn handle<'a>(&'a self, x: u8) -> K::Future<'a, u8>;
}

struct MyService;

impl Service<Local> for MyService {
    fn handle<'a>(&'a self, x: u8) -> LocalBoxFuture<'a, u8> {
        async move { x * 2 }.boxed_local()
    }
}

impl Service<Sendable> for MyService {
    fn handle<'a>(&'a self, x: u8) -> BoxFuture<'a, u8> {
        async move { x * 2 }.boxed()
    }
}

async fn use_unknown<K: FutureKind>(service: &impl Service<K>) {
    let result = service.handle(42).await;
}

Core Types

FutureKind Trait

The central abstraction that defines an associated type for futures:

use std::future::Future;

pub trait FutureKind {
    type Future<'a, T: 'a>: Future<Output = T> + 'a;
}

Sendable

Represents Send futures, backed by futures::future::BoxFuture:

impl FutureKind for Sendable {
    type Future<'a, T: 'a> = BoxFuture<'a, T>;
}

Local

Represents !Send futures, backed by futures::future::LocalBoxFuture:

impl FutureKind for Local {
    type Future<'a, T: 'a> = LocalBoxFuture<'a, T>;
}

#[kinds] Macro

While you can define traits generic over FutureKind, Rust cannot verify that a single async block satisfies both Send and !Send bounds for an arbitrary K: FutureKind. The #[kinds] macro solves this by generating separate implementations for each variant, allowing the compiler to verify each one independently:

use std::marker::PhantomData;
use futures_kind::{kinds, FutureKind};

trait Counter<K: FutureKind> {
    fn next(&self) -> K::Future<'_, u32>;
}

struct Memory<K> {
    val: u32,
    _marker: PhantomData<K>,
}

// Generates impl Counter<Sendable> and impl Counter<Local>
#[kinds]
impl<K: FutureKind> Counter<K> for Memory<K> {
    fn next(&self) -> K::Future<'_, u32> {
        let val = self.val;
        Box::pin(async move { val + 1 })
    }
}

You can also generate only specific variants:

#[kinds(Sendable)]        // Only Sendable
#[kinds(Local)]           // Only Local
#[kinds(Sendable, Local)] // Both (default)

Each variant can have its own additional bounds using where:

#[kinds(Sendable where T: Send, Local where T: Debug)]
impl<K: FutureKind, T: Clone> Processor<K> for Container<T> {
    fn process(&self) -> K::Future<'_, T> {
        let value = self.value.clone();
        Box::pin(async move { value })
    }
}
// Generates:
//   impl<T: Clone + Send> Processor<Sendable> for Container<T>
//   impl<T: Clone + Debug> Processor<Local> for Container<T>

Threading FutureKind Through Your Code

The simplest pattern is to structure your code so the FutureKind type parameter appears naturally in your API. This typically happens when:

  1. The function returns the future directly (not the awaited result)
  2. You use a struct to carry the type parameter

Here's an example using a struct:

use std::marker::PhantomData;
use futures_kind::{FutureKind, Local, Sendable};
use futures::future::{BoxFuture, LocalBoxFuture, FutureExt};

trait Service<K: FutureKind> {
    fn handle<'a>(&'a self, x: u8) -> K::Future<'a, u8>;
}

struct MyService;

impl Service<Local> for MyService {
    fn handle<'a>(&'a self, x: u8) -> LocalBoxFuture<'a, u8> {
        async move { x * 2 }.boxed_local()
    }
}

impl Service<Sendable> for MyService {
    fn handle<'a>(&'a self, x: u8) -> BoxFuture<'a, u8> {
        async move { x * 2 }.boxed()
    }
}

pub struct Handler<K: FutureKind> {
    service: MyService,
    _marker: PhantomData<K>,
}

impl<K: FutureKind> Handler<K>
where
    MyService: Service<K>
{
    pub fn new(service: MyService) -> Self {
        Self {
            service,
            _marker: PhantomData,
        }
    }

    // K is part of Self, so methods can use it naturally
    pub async fn process(&self, x: u8) -> u8 {
        Service::<K>::handle(&self.service, x).await
    }
}

# async fn example() {
// Usage is clean and type-safe
let my_service = MyService;
let handler = Handler::<Sendable>::new(my_service);
let result = handler.process(42).await;
# }

Or when returning futures directly:

// K appears in the return type
pub fn create_task<K: FutureKind>(x: u8) -> K::Future<'static, u8>
where
    MyService: Service<K>
{
    // Return the boxed future, don't await it
    async move { x * 2 }.boxed() // or .boxed_local() for Local
}

This pattern allows the FutureKind choice to propagate through your entire call stack while maintaining type safety.

Use Cases

Use Case Description
Cross-platform libraries Write async traits once, support both native and Wasm targets
Runtime flexibility Allow users to choose their async runtime without forcing Send constraints
Testing Use Local futures in single-threaded test environments while production uses Sendable
Gradual migration Support both variants during migration between runtimes

Design Philosophy

futures_kind embraces the following principles:

  1. Zero-cost abstraction: The trait compiles down to direct use of BoxFuture or LocalBoxFuture
  2. Compile-time dispatch: All decisions about Send vs !Send happen at compile time
  3. Minimal API surface: Just one trait and two implementations keep the crate focused and maintainable
  4. Compatibility: Works seamlessly with the futures crate's existing types
  5. Extensability: Other boxings or non-boxed types can be implemented directly.

Installation

Add this to your Cargo.toml:

[dependencies]
futures_kind = "0.1.0"
futures = "0.3.31"

License

Licensed under either of:

at your option.

Dependencies

~0.7–1.3MB
~27K SLoC