Skip to main content

Crate future_form

Crate future_form 

Source
Expand description

§future_form

crates.io CI docs.rs License

“This isn’t even my final future form!”

Abstractions over Send and !Send futures in Rust.

§Installation

Add to your Cargo.toml:

[dependencies]
future_form = "0.3.1"

§Motivation

Async Rust has a fragmentation problem. Some runtimes demand Send futures (tokio, async-std), while others are perfectly happy with !Send (single-threaded executors, Wasm). Library authors face an unpleasant choice:

  1. Duplicate async trait implementations for both variants — MyService and MyLocalService, all the way down
  2. Force everyone onto Send futures, leaving !Send use cases out in the cold (or vice versa)
  3. Maintain separate crates or feature flags for each variant

All of these are verbose, error-prone, and a maintenance headache.

Async Rust developers have long struggled with a question as old as time: “Should I Send or !Send?” Finally, you can stop asking and simply… embrace your future form and delay to the final concrete call site.

§Approach

future_form solves this with a simple abstraction: write your async code once, parameterized over the “form” of future you need. The choice between Send and !Send becomes a type parameter that flows through your code.

use future_form::{FutureForm, Sendable, Local};
use futures::future::{BoxFuture, LocalBoxFuture};

// Define your trait once, generic over the future kind
pub trait Service<K: FutureForm> {
    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> {
        Local::from_future(async move { x * 2 })
    }
}

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

Users pick the variant they need:

use future_form::{FutureForm, Sendable, Local};
use futures::future::{BoxFuture, LocalBoxFuture};

trait Service<K: FutureForm> {
    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> {
        Local::from_future(async move { x * 2 })
    }
}

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

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

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

Or thread through the FutureForm parameter and delay the choice to compile time. This is typesafe: if you try to send a Local future between threads, you’ll get a compile error telling you to specialize to Sendable.

use future_form::{FutureForm, Sendable, Local};
use futures::future::{BoxFuture, LocalBoxFuture};

trait Service<K: FutureForm> {
    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> {
        Local::from_future(async move { x * 2 })
    }
}

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

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

§Choose Your Form

§FutureForm Trait

The core abstraction — a trait with an associated future type:

use std::future::Future;

pub trait FutureForm {
    type Future<'a, T: 'a>: Future<Output = T> + 'a;
    // + from_future() and ready() default methods
}

This library ships with Sendable and Local, but you can implement your own forms for other boxing strategies or even unboxed futures.

§Sendable

“Have future, will travel”

Represents Send futures, backed by futures::future::BoxFuture. For multithreaded 1 contexts:

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

§Local

What happens on the thread, stays on the thread.

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

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

§#[future_form] Macro

One impl to rule them all.

Rust’s async { ... } blocks have a concrete Send or !Send type, so you can’t write a single generic impl that works for both, nor can you extract out the body without incurring those bounds. Without this macro, if you want identical behavior, Rust forces you to write and maintain identical implementations for each variant. This macro lets you write your impl once and generates one for each future form you pass in:

use std::marker::PhantomData;
use future_form::{future_form, FutureForm};

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

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

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

You can also generate only specific variants:

#[future_form(Sendable)]        // Only Sendable
#[future_form(Local)]           // Only Local
#[future_form(Sendable, Local)] // Both

Each variant can have its own additional bounds using where:

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

§Threading FutureForm Through Your Code

The trick is structuring your code so the FutureForm parameter flows naturally through your API. Two common patterns:

  1. Return the future directly — the K appears in the return type
  2. Carry K in a structPhantomData<K> lets you thread it through methods

Here’s the struct approach:

use std::marker::PhantomData;
use future_form::{FutureForm, Local, Sendable};
use futures::future::{BoxFuture, LocalBoxFuture};

trait Service<K: FutureForm> {
    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> {
        Local::from_future(async move { x * 2 })
    }
}

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

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

impl<K: FutureForm> 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
    }
}

// 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: FutureForm>(x: u8) -> K::Future<'static, u8>
where
    K::Future<'static, u8>: FromFuture<'static, u8, impl Future<Output = u8>>
{
    K::from_future(async move { x * 2 })
}

The FutureForm choice propagates through your entire call stack — type safety all the way down.

§Ready Values

When you already have a computed value and just need to wrap it as a future, use K::ready() instead of K::from_future(async { value }):

fn cached_count<K: FutureForm>(&self) -> K::Future<'_, u32>
where
    K::Future<'_, u32>: FromReady<'_, u32>,
{
    K::ready(self.count)
}

This avoids creating an unnecessary async state machine for synchronous results.

§Host-Driven Polling (FFI)

No runtime? No problem.

The companion future_form_ffi crate lets FFI hosts (Go, Java, Python, C, Swift) drive Rust async state machines without an async runtime. Your trait impls don’t change — the same #[future_form(Sendable)] implementation works whether a Rust runtime .awaits the future or a foreign host polls it in a loop.

See the future_form_ffi docs and the FFI examples for complete Go, Java, and Python hosts driving the same Rust counter through C ABI and JNI.

§Use Cases

Use CaseDescription
Cross-platform librariesWrite async traits once, support both native and Wasm targets
Runtime flexibilityAllow users to choose their async runtime without forcing Send constraints
TestingUse Local futures in single-threaded test environments while production uses Sendable
Gradual migrationSupport both variants during migration between runtimes

§Design Philosophy

For deeper architectural discussion, see the design/ notes.

future_form is deliberately minimal:

  1. Near-zero-cost abstraction — same overhead as async-trait or tokio::spawn: one heap allocation per async call
  2. Compile-time dispatchSend vs !Send is resolved statically, no runtime overhead
  3. Small API surface — one core trait, two marker types, one macro. That’s it.
  4. Plays well with others — builds on the futures crate’s existing types
  5. Extensible — implement FutureForm for your own types if the builtins don’t fit
  6. FFI-ready — the PollOnce trait lets foreign hosts drive any boxed future without an async runtime

§Comparison with async-trait

async-trait and future_form are complementary — they solve different problems:

Aspectasync-traitfuture_form
Problem solvedAsync methods in traits (pre-RPITIT)Abstracting over Send vs !Send
Send/!Send choicePer-trait (#[async_trait(?Send)])Per-usage site (K: FutureForm)
When to chooseAt trait definition timeAt impl usage time
Post-RPITIT (Rust 1.75+)Less necessary for basic casesStill useful for Send/!Send flexibility

§When to use async-trait

  • You need async trait methods on older Rust versions (pre-1.75)
  • You want the simplest possible syntax for async traits
  • All users of your trait will use the same Send/!Send variant

§When to use future_form

  • You want consumers to choose Send vs !Send at usage time
  • You’re building a library that should work in both multi-threaded and single-threaded contexts
  • You need both variants available simultaneously (e.g., for different runtime configurations)

§Using both together

Nothing stops you from using both. A common pattern: async-trait for internal convenience, future_form at your public API boundary:

use async_trait::async_trait;
use future_form::{FutureForm, future_form};

// Internal trait using async-trait for convenience
#[async_trait]
trait InternalService {
    async fn fetch(&self) -> Vec<u8>;
}

// Public trait using future_form for flexibility
trait PublicService<K: FutureForm> {
    fn fetch(&self) -> K::Future<'_, Vec<u8>>;
}

§Comparison with trait-variant

trait-variant is the Rust lang team’s approach: generate a second trait with Send bounds from your base trait. Different philosophy, different tradeoffs:

Aspecttrait-variantfuture_form
ApproachGenerates two separate traitsSingle trait with type parameter
SyntaxNative async fnBoxed futures via K::Future
Trait countTwo traits (Local* and Send variant)One trait generic over K
Impl patternSeparate impl block per variantSingle impl with #[future_form] generates both
Middleware patternLimited (can’t conditionally impl Send)Supported via generic K propagation

§trait-variant example

#[trait_variant::make(HttpService: Send)]
pub trait LocalHttpService {
    async fn fetch(&self, url: &str) -> Vec<u8>;
}

// Implementors write separate impl blocks:
impl LocalHttpService for MyClient { ... }
impl HttpService for MyClient { ... }  // must duplicate impl body

§future_form example

pub trait HttpService<K: FutureForm> {
    fn fetch(&self, url: &str) -> K::Future<'_, Vec<u8>>;
}

// Implementors can support BOTH via #[future_form]:
#[future_form(Sendable, Local)]
impl<K: FutureForm> HttpService<K> for MyClient {
    fn fetch(&self, url: &str) -> K::Future<'_, Vec<u8>> {
        K::from_future(async move { /* ... */ })
    }
}

§When to use trait-variant

  • You want native async fn syntax (can avoid boxing in some contexts)
  • You’re okay with separate impl blocks for each variant
  • You prefer the Rust lang team’s officially recommended approach

§When to use future_form

  • You want a single trait to support both Send and !Send usage
  • You’re building middleware/wrappers that should preserve the caller’s Send choice
  • You need the K: FutureForm parameter to propagate through your type hierarchy

§License

Licensed under either of:

at your option.


  1. Thread level over 9000?! 

Modules§

extensions
Guidance on extending FutureForm with custom implementations.

Enums§

Local
Abstraction over !Send futures.
Sendable
Abstraction over Send futures.

Traits§

FromFuture
A trait for constructing FutureForm-specific types from raw futures.
FromReady
A trait for constructing FutureForm-specific types from ready values.
FutureForm
An abstraction over Futures.

Attribute Macros§

future_form
Generate implementations of a trait for Sendable and/or Local FutureForms.