future_form
"This isn't even my
finalfuture form!"
Abstractions over Send and !Send futures in Rust.
Installation
Add to your Cargo.toml:
[]
= "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:
- Duplicate async trait implementations for both variants —
MyServiceandMyLocalService, all the way down - Force everyone onto
Sendfutures, leaving!Senduse cases out in the cold (or vice versa) - 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 ;
use ;
// Define your trait once, generic over the future kind
;
// Implement for both Send and !Send with minimal boilerplate
Users pick the variant they need:
use ;
use ;
;
// For Send-required runtimes like tokio
async
// For !Send runtimes like Wasm or single-threaded executors
async
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 ;
use ;
;
async
Choose Your Form
FutureForm Trait
The core abstraction — a trait with an associated future type:
use Future;
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 [^over9k] contexts:
[^over9k]: Thread level over 9000?!
Local
What happens on the thread, stays on the thread.
Represents !Send futures, backed by futures::future::LocalBoxFuture:
#[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 PhantomData;
use ;
// Generates impl Counter<Sendable> and impl Counter<Local>
You can also generate only specific variants:
// Only Sendable
// Only Local
// Both
Each variant can have its own additional bounds using where:
// 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:
- Return the future directly — the
Kappears in the return type - Carry
Kin a struct —PhantomData<K>lets you thread it through methods
Here's the struct approach:
use PhantomData;
use ;
use ;
;
# async
Or when returning futures directly:
// K appears in the return type
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 }):
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 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
For deeper architectural discussion, see the design/ notes.
future_form is deliberately minimal:
- Near-zero-cost abstraction — same overhead as
async-traitortokio::spawn: one heap allocation per async call - Compile-time dispatch —
Sendvs!Sendis resolved statically, no runtime overhead - Small API surface — one core trait, two marker types, one macro. That's it.
- Plays well with others — builds on the
futurescrate's existing types - Extensible — implement
FutureFormfor your own types if the builtins don't fit - FFI-ready — the
PollOncetrait 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:
| Aspect | async-trait |
future_form |
|---|---|---|
| Problem solved | Async methods in traits (pre-RPITIT) | Abstracting over Send vs !Send |
| Send/!Send choice | Per-trait (#[async_trait(?Send)]) |
Per-usage site (K: FutureForm) |
| When to choose | At trait definition time | At impl usage time |
| Post-RPITIT (Rust 1.75+) | Less necessary for basic cases | Still 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;
use ;
// Internal trait using async-trait for convenience
// Public trait using future_form for flexibility
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:
| Aspect | trait-variant |
future_form |
|---|---|---|
| Approach | Generates two separate traits | Single trait with type parameter |
| Syntax | Native async fn |
Boxed futures via K::Future |
| Trait count | Two traits (Local* and Send variant) |
One trait generic over K |
| Impl pattern | Separate impl block per variant | Single impl with #[future_form] generates both |
| Middleware pattern | Limited (can't conditionally impl Send) | Supported via generic K propagation |
trait-variant example
// Implementors write separate impl blocks:
// must duplicate impl body
future_form example
// Implementors can support BOTH via #[future_form]:
When to use trait-variant
- You want native
async fnsyntax (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
Sendand!Sendusage - You're building middleware/wrappers that should preserve the caller's Send choice
- You need the
K: FutureFormparameter to propagate through your type hierarchy
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.