2 releases
Uses new Rust 2024
| 0.2.0 |
|
|---|---|
| 0.1.1 | Jan 24, 2026 |
| 0.1.0 | Jan 14, 2026 |
#348 in Asynchronous
723 downloads per month
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:
- Duplicate their async trait implementations for both
Sendand!Sendvariants, including all consumers (e.g.MyServiceandMyLocalService) - Force all users to use
Sendfutures, excluding legitimate!Senduse cases - 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:
- The function returns the future directly (not the awaited result)
- 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:
- Zero-cost abstraction: The trait compiles down to direct use of
BoxFutureorLocalBoxFuture - Compile-time dispatch: All decisions about
Sendvs!Sendhappen at compile time - Minimal API surface: Just one trait and two implementations keep the crate focused and maintainable
- Compatibility: Works seamlessly with the
futurescrate's existing types - 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:
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
Dependencies
~0.7–1.3MB
~27K SLoC