๐Ÿฆ€๐Ÿš€ Abstract over Send and !Send traits crates.io/crates/future_form
0

Configure Feed

Select the types of activity you want to include in your feed.

Add `#[kinds]` macro and IntoFutureKind trait (#1)

Proc macro generates Sendable and Local trait impls from a single
generic definition. Supports conditional bounds per variant.
IntoFutureKind abstracts over boxing for extensibility.

+1829 -40
+68 -1
Cargo.lock
··· 65 65 66 66 [[package]] 67 67 name = "futures_kind" 68 - version = "0.1.0" 68 + version = "0.1.1" 69 69 dependencies = [ 70 70 "futures", 71 + "futures_kind_macros", 72 + "tokio", 73 + ] 74 + 75 + [[package]] 76 + name = "futures_kind_macros" 77 + version = "0.1.1" 78 + dependencies = [ 79 + "proc-macro2", 80 + "quote", 81 + "syn", 71 82 ] 72 83 73 84 [[package]] ··· 81 92 version = "0.1.0" 82 93 source = "registry+https://github.com/rust-lang/crates.io-index" 83 94 checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 95 + 96 + [[package]] 97 + name = "proc-macro2" 98 + version = "1.0.106" 99 + source = "registry+https://github.com/rust-lang/crates.io-index" 100 + checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" 101 + dependencies = [ 102 + "unicode-ident", 103 + ] 104 + 105 + [[package]] 106 + name = "quote" 107 + version = "1.0.44" 108 + source = "registry+https://github.com/rust-lang/crates.io-index" 109 + checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" 110 + dependencies = [ 111 + "proc-macro2", 112 + ] 113 + 114 + [[package]] 115 + name = "syn" 116 + version = "2.0.114" 117 + source = "registry+https://github.com/rust-lang/crates.io-index" 118 + checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" 119 + dependencies = [ 120 + "proc-macro2", 121 + "quote", 122 + "unicode-ident", 123 + ] 124 + 125 + [[package]] 126 + name = "tokio" 127 + version = "1.49.0" 128 + source = "registry+https://github.com/rust-lang/crates.io-index" 129 + checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" 130 + dependencies = [ 131 + "pin-project-lite", 132 + "tokio-macros", 133 + ] 134 + 135 + [[package]] 136 + name = "tokio-macros" 137 + version = "2.6.0" 138 + source = "registry+https://github.com/rust-lang/crates.io-index" 139 + checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" 140 + dependencies = [ 141 + "proc-macro2", 142 + "quote", 143 + "syn", 144 + ] 145 + 146 + [[package]] 147 + name = "unicode-ident" 148 + version = "1.0.22" 149 + source = "registry+https://github.com/rust-lang/crates.io-index" 150 + checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
+16 -11
Cargo.toml
··· 1 - [package] 2 - name = "futures_kind" 3 - version = "0.1.0" 1 + [workspace] 2 + resolver = "3" 3 + members = ["futures_kind", "futures_kind_macros"] 4 + 5 + [workspace.package] 6 + version = "0.1.1" 4 7 edition = "2024" 5 8 rust-version = "1.90" 6 9 license = "MIT OR Apache-2.0" 7 10 repository = "https://codeberg.org/expede/futures_kind" 8 11 authors = ["Brooklyn Zelenka <hello@brooklynzelenka.com>"] 9 - description = "Abstractions over Send and !Send futures" 10 - keywords = ["futures", "async"] 11 - categories = ["asynchronous", "no-std"] 12 12 13 - [dependencies] 14 - futures = { version = "0.3.31", default-features = false, features = ["alloc"] } 15 - 16 - [lints.rust] 13 + [workspace.lints.rust] 17 14 future_incompatible = { level = "warn", priority = -1 } 18 15 let_underscore = { level = "warn", priority = -1 } 19 16 missing_copy_implementations = "warn" ··· 28 25 29 26 unsafe_code = "forbid" 30 27 31 - [lints.clippy] 28 + [workspace.lints.clippy] 32 29 dbg_macro = "warn" 33 30 expect_used = "warn" 34 31 fallible_impl_from = "warn" ··· 45 42 all = { level = "deny", priority = -1 } 46 43 cargo = { level = "deny", priority = -1 } 47 44 pedantic = { level = "deny", priority = -1 } 45 + 46 + [workspace.dependencies] 47 + futures = { version = "0.3.31", default-features = false, features = ["alloc"] } 48 + futures_kind_macros = { version = "0.1.0", path = "futures_kind_macros" } 49 + proc-macro2 = "1" 50 + quote = "1" 51 + syn = { version = "2", features = ["full"] } 52 + tokio = { version = "1", features = ["rt", "macros"] }
+41
DESIGN.md
··· 1 + # Design Notes 2 + 3 + ## Performance Impact of Boxing Futures 4 + 5 + `futures_kind` uses `BoxFuture` and `LocalBoxFuture` under the hood. This section discusses the performance implications of that choice. 6 + 7 + ### Costs 8 + 9 + **Allocation** 10 + - One heap allocation per boxed future (typically small, but adds up in hot paths) 11 + - Deallocation when the future completes 12 + 13 + **Indirection** 14 + - Virtual dispatch through the `dyn Future` trait object on each poll 15 + - Pointer chasing can hurt CPU cache locality 16 + 17 + ### In Practice 18 + 19 + The impact is often negligible because: 20 + 21 + 1. Futures are usually polled relatively few times compared to the work they represent 22 + 2. A single network call or disk read dwarfs the cost of one allocation 23 + 3. The allocator is highly optimized for small, short-lived allocations 24 + 25 + ### When It Matters 26 + 27 + - Tight loops spawning thousands of tiny futures 28 + - Latency-critical code paths (microsecond-level) 29 + - Embedded/`no_std` environments with constrained allocators 30 + 31 + ### Alternatives 32 + 33 + | Approach | Tradeoff | 34 + |---------------------------------|-----------------------------------------------------------------------------------| 35 + | `async-trait` | Same cost (also boxes) | 36 + | `impl Future` (static dispatch) | Avoids boxing but can't be used in trait objects or with this kind of abstraction | 37 + | Enum-based dispatch | Works for a fixed set of variants, but doesn't generalize | 38 + 39 + ### Recommendation 40 + 41 + For most async code doing I/O, the boxing cost is noise. If profiling shows it's a bottleneck, you'd typically avoid the abstraction entirely in that hot path rather than try to optimize the boxing.
+216
EXTENSIONS.md
··· 1 + # Extending `FutureKind` 2 + 3 + The `FutureKind` trait is open for extension. This document sketches out possible implementations beyond the built-in `Sendable` and `Local`. 4 + 5 + ## The Constraint 6 + 7 + ```rust 8 + pub trait FutureKind { 9 + type Future<'a, T: 'a>: Future<Output = T> + 'a; 10 + } 11 + ``` 12 + 13 + Any implementation must provide a `Future` type that: 14 + 1. Works for *any* output type `T` 15 + 2. Works for *any* lifetime `'a` 16 + 3. Implements `Future<Output = T>` 17 + 18 + This rules out concrete async block types (which have fixed output types), but allows any generic future wrapper. 19 + 20 + ## SmallBoxed Futures 21 + 22 + Using stack allocation with heap fallback via the `smallbox` crate: 23 + 24 + ```rust 25 + use smallbox::{SmallBox, space::S16}; 26 + use std::future::Future; 27 + use std::pin::Pin; 28 + use std::task::{Context, Poll}; 29 + 30 + /// Stack-allocated futures with heap fallback for large futures. 31 + pub struct SmallBoxed; 32 + 33 + /// A future that lives on the stack if small enough, otherwise heap. 34 + pub struct SmallBoxFuture<'a, T>( 35 + Pin<SmallBox<dyn Future<Output = T> + Send + 'a, S16>> 36 + ); 37 + 38 + impl<'a, T> Future for SmallBoxFuture<'a, T> { 39 + type Output = T; 40 + 41 + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<T> { 42 + self.0.as_mut().poll(cx) 43 + } 44 + } 45 + 46 + impl FutureKind for SmallBoxed { 47 + type Future<'a, T: 'a> = SmallBoxFuture<'a, T>; 48 + } 49 + 50 + // Constructor helper 51 + impl<'a, T> SmallBoxFuture<'a, T> { 52 + pub fn new<F>(future: F) -> Self 53 + where 54 + F: Future<Output = T> + Send + 'a, 55 + { 56 + Self(SmallBox::new_pin(future)) 57 + } 58 + } 59 + ``` 60 + 61 + Usage: 62 + ```rust 63 + #[kinds(SmallBoxed)] 64 + impl<K: FutureKind> MyTrait<K> for MyType { 65 + fn operation(&self) -> K::Future<'_, u32> { 66 + SmallBoxFuture::new(async { 42 }) 67 + } 68 + } 69 + ``` 70 + 71 + ## Result-Wrapped Futures 72 + 73 + For operations that might fail at construction time: 74 + 75 + ```rust 76 + use std::future::{Future, Ready, ready}; 77 + use std::pin::Pin; 78 + use std::task::{Context, Poll}; 79 + use futures::future::BoxFuture; 80 + 81 + /// Futures that capture a construction-time error. 82 + pub struct Fallible; 83 + 84 + pub enum FallibleFuture<'a, T> { 85 + Ok(BoxFuture<'a, T>), 86 + Err(Ready<T>), // For error cases with default/error value 87 + } 88 + 89 + impl<'a, T> Future for FallibleFuture<'a, T> { 90 + type Output = T; 91 + 92 + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<T> { 93 + match self.get_mut() { 94 + Self::Ok(f) => Pin::new(f).poll(cx), 95 + Self::Err(f) => Pin::new(f).poll(cx), 96 + } 97 + } 98 + } 99 + 100 + impl FutureKind for Fallible { 101 + type Future<'a, T: 'a> = FallibleFuture<'a, T>; 102 + } 103 + ``` 104 + 105 + ## Static Dispatch (Limited) 106 + 107 + True static dispatch without boxing is possible but limited. The future type must be nameable and generic: 108 + 109 + ```rust 110 + use std::future::Ready; 111 + 112 + /// For operations that complete immediately. 113 + pub struct Immediate; 114 + 115 + impl FutureKind for Immediate { 116 + type Future<'a, T: 'a> = Ready<T>; 117 + } 118 + ``` 119 + 120 + Usage: 121 + ```rust 122 + impl MyTrait<Immediate> for MyType { 123 + fn get_value(&self) -> Ready<u32> { 124 + std::future::ready(42) 125 + } 126 + } 127 + ``` 128 + 129 + This works for `Ready`, `Pending`, or any nameable future type, but *not* for async blocks (whose types are anonymous and unnameable). 130 + 131 + ## Why Not Fully Unboxed? 132 + 133 + You might want: 134 + ```rust 135 + impl FutureKind for Unboxed { 136 + type Future<'a, T: 'a> = impl Future<Output = T>; // Not valid 137 + } 138 + ``` 139 + 140 + This doesn't work because: 141 + 1. Associated types can't use `impl Trait` 142 + 2. Async blocks have anonymous, unnameable types 143 + 3. Each async block creates a *different* type, but `FutureKind::Future` must be a single type 144 + 145 + The boxing in `Sendable`/`Local` is precisely what enables type erasure across different async implementations. 146 + 147 + ## The `IntoFutureKind` Trait 148 + 149 + The crate provides an `IntoFutureKind` trait that abstracts over lifting a future into a kind's future type: 150 + 151 + ```rust 152 + pub trait IntoFutureKind<'a, T, F> 153 + where 154 + T: 'a, 155 + F: Future<Output = T> + 'a, 156 + { 157 + fn into_kind(f: F) -> Self; 158 + } 159 + ``` 160 + 161 + This is implemented for `BoxFuture` (requiring `F: Send`) and `LocalBoxFuture` (no `Send` requirement). Custom future types can implement this trait to enable uniform construction: 162 + 163 + ```rust 164 + impl<'a, T, F> IntoFutureKind<'a, T, F> for SmallBoxFuture<'a, T> 165 + where 166 + T: 'a, 167 + F: Future<Output = T> + Send + 'a, 168 + { 169 + fn into_kind(f: F) -> Self { 170 + SmallBoxFuture::new(f) 171 + } 172 + } 173 + ``` 174 + 175 + Usage is then uniform across all kinds via [`FutureKind::into_kind`]: 176 + 177 + ```rust 178 + impl Counter<Sendable> for Memory { 179 + fn next(&self) -> BoxFuture<'_, u32> { 180 + Sendable::into_kind(async { self.val + 1 }) 181 + } 182 + } 183 + 184 + impl Counter<Local> for Memory { 185 + fn next(&self) -> LocalBoxFuture<'_, u32> { 186 + Local::into_kind(async { self.val + 1 }) 187 + } 188 + } 189 + 190 + impl Counter<SmallBoxed> for Memory { 191 + fn next(&self) -> SmallBoxFuture<'_, u32> { 192 + SmallBoxed::into_kind(async { self.val + 1 }) 193 + } 194 + } 195 + ``` 196 + 197 + ## Registering Custom Kinds with `#[kinds]` 198 + 199 + The `#[kinds]` macro currently only recognizes `Sendable` and `Local`. To use custom kinds, implement traits manually: 200 + 201 + ```rust 202 + // Won't work (macro doesn't know about SmallBoxed): 203 + // #[kinds(SmallBoxed)] 204 + 205 + // Instead, implement directly: 206 + impl MyTrait<SmallBoxed> for MyType { 207 + fn operation(&self) -> SmallBoxFuture<'_, u32> { 208 + SmallBoxed::into_kind(async { self.value }) 209 + } 210 + } 211 + ``` 212 + 213 + Extending the macro to support custom kinds is possible and would involve: 214 + 1. Accepting arbitrary type paths in the attribute 215 + 2. Generating `<Path as FutureKind>::Future<'a, T>` for return types 216 + 3. Relying on `IntoFutureKind::into_kind(...)` in method bodies to work for any kind
+64 -12
README.md
··· 4 4 5 5 ## Motivation 6 6 7 - 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: 7 + 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: 8 8 9 9 1. Duplicate their async trait implementations for both `Send` and `!Send` variants, including all consumers (e.g. `MyService` and `MyLocalService`) 10 10 2. Force all users to use `Send` futures, excluding legitimate `!Send` use cases ··· 70 70 let result = service.handle(42).await; 71 71 } 72 72 73 - // For !Send runtimes like WASM or single-threaded executors 73 + // For !Send runtimes like Wasm or single-threaded executors 74 74 async fn use_local(service: &impl Service<Local>) { 75 75 let result = service.handle(42).await; 76 76 } ··· 113 113 114 114 The central abstraction that defines an associated type for futures: 115 115 116 - ```rust,ignore 116 + ```rust,no_run 117 117 pub trait FutureKind { 118 118 type Future<'a, T: 'a>: Future<Output = T> + 'a; 119 119 } ··· 123 123 124 124 Represents `Send` futures, backed by `futures::future::BoxFuture`: 125 125 126 - ```rust,ignore 126 + ```rust,no_run 127 127 impl FutureKind for Sendable { 128 128 type Future<'a, T: 'a> = BoxFuture<'a, T>; 129 129 } ··· 133 133 134 134 Represents `!Send` futures, backed by `futures::future::LocalBoxFuture`: 135 135 136 - ```rust,ignore 136 + ```rust,no_run 137 137 impl FutureKind for Local { 138 138 type Future<'a, T: 'a> = LocalBoxFuture<'a, T>; 139 139 } 140 140 ``` 141 141 142 + ### `#[kinds]` Macro 143 + 144 + 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: 145 + 146 + ```rust,no_run 147 + use std::marker::PhantomData; 148 + use futures_kind::{kinds, FutureKind}; 149 + 150 + trait Counter<K: FutureKind> { 151 + fn next(&self) -> K::Future<'_, u32>; 152 + } 153 + 154 + struct Memory<K> { 155 + val: u32, 156 + _marker: PhantomData<K>, 157 + } 158 + 159 + // Generates impl Counter<Sendable> and impl Counter<Local> 160 + #[kinds] 161 + impl<K: FutureKind> Counter<K> for Memory<K> { 162 + fn next(&self) -> K::Future<'_, u32> { 163 + let val = self.val; 164 + Box::pin(async move { val + 1 }) 165 + } 166 + } 167 + ``` 168 + 169 + You can also generate only specific variants: 170 + 171 + ```rust,no_run 172 + #[kinds(Sendable)] // Only Sendable 173 + #[kinds(Local)] // Only Local 174 + #[kinds(Sendable, Local)] // Both (default) 175 + ``` 176 + 177 + Each variant can have its own additional bounds using `where`: 178 + 179 + ```rust,no_run 180 + #[kinds(Sendable where T: Send, Local where T: Debug)] 181 + impl<K: FutureKind, T: Clone> Processor<K> for Container<T> { 182 + fn process(&self) -> K::Future<'_, T> { 183 + let value = self.value.clone(); 184 + Box::pin(async move { value }) 185 + } 186 + } 187 + // Generates: 188 + // impl<T: Clone + Send> Processor<Sendable> for Container<T> 189 + // impl<T: Clone + Debug> Processor<Local> for Container<T> 190 + ``` 191 + 142 192 ## Threading `FutureKind` Through Your Code 143 193 144 194 The simplest pattern is to structure your code so the `FutureKind` type parameter appears naturally in your API. This typically happens when: ··· 203 253 204 254 Or when returning futures directly: 205 255 206 - ```rust,ignore 256 + ```rust,no_run 207 257 // K appears in the return type 208 258 pub fn create_task<K: FutureKind>(x: u8) -> K::Future<'static, u8> 209 259 where ··· 218 268 219 269 ## Use Cases 220 270 221 - - **Cross-platform libraries**: Write async traits once, support both native and WASM targets 222 - - **Runtime flexibility**: Allow users to choose their async runtime without forcing `Send` constraints 223 - - **Testing**: Use `Local` futures in single-threaded test environments while production uses `Sendable` 224 - - **Gradual migration**: Support both variants during migration between runtimes 271 + | Use Case | Description | 272 + |----------|-------------| 273 + | Cross-platform libraries | Write async traits once, support both native and Wasm targets | 274 + | Runtime flexibility | Allow users to choose their async runtime without forcing `Send` constraints | 275 + | Testing | Use `Local` futures in single-threaded test environments while production uses `Sendable` | 276 + | Gradual migration | Support both variants during migration between runtimes | 225 277 226 278 ## Design Philosophy 227 279 ··· 247 299 248 300 Licensed under either of: 249 301 250 - - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 251 - - MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 302 + - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or <http://www.apache.org/licenses/LICENSE-2.0>) 303 + - MIT license ([LICENSE-MIT](LICENSE-MIT) or <http://opensource.org/licenses/MIT>) 252 304 253 305 at your option.
+75 -8
flake.lock
··· 1 1 { 2 2 "nodes": { 3 + "command-utils": { 4 + "inputs": { 5 + "flake-utils": "flake-utils", 6 + "nixpkgs": "nixpkgs" 7 + }, 8 + "locked": { 9 + "lastModified": 1769213192, 10 + "narHash": "sha256-Lab3bFspz1ZTWZFwRWjMsDqTv8dXXNCQtQix+drJngM=", 11 + "ref": "refs/heads/main", 12 + "rev": "80de3c66b7810667fc932dc3a16537db16d66a2f", 13 + "revCount": 5, 14 + "type": "git", 15 + "url": "https://codeberg.org/expede/nix-command-utils" 16 + }, 17 + "original": { 18 + "type": "git", 19 + "url": "https://codeberg.org/expede/nix-command-utils" 20 + } 21 + }, 3 22 "flake-utils": { 4 23 "inputs": { 5 24 "systems": "systems" 6 25 }, 7 26 "locked": { 27 + "lastModified": 1709126324, 28 + "narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=", 29 + "owner": "numtide", 30 + "repo": "flake-utils", 31 + "rev": "d465f4819400de7c8d874d50b982301f28a84605", 32 + "type": "github" 33 + }, 34 + "original": { 35 + "id": "flake-utils", 36 + "type": "indirect" 37 + } 38 + }, 39 + "flake-utils_2": { 40 + "inputs": { 41 + "systems": "systems_2" 42 + }, 43 + "locked": { 8 44 "lastModified": 1731533236, 9 45 "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 10 46 "owner": "numtide", ··· 20 56 }, 21 57 "nixpkgs": { 22 58 "locked": { 23 - "lastModified": 1768323494, 24 - "narHash": "sha256-yBXJLE6WCtrGo7LKiB6NOt6nisBEEkguC/lq/rP3zRQ=", 59 + "lastModified": 1769089682, 60 + "narHash": "sha256-9yA/LIuAVQq0lXelrZPjLuLVuZdm03p8tfmHhnDIkms=", 61 + "owner": "NixOS", 62 + "repo": "nixpkgs", 63 + "rev": "078d69f03934859a181e81ba987c2bb033eebfc5", 64 + "type": "github" 65 + }, 66 + "original": { 67 + "id": "nixpkgs", 68 + "ref": "nixos-25.11", 69 + "type": "indirect" 70 + } 71 + }, 72 + "nixpkgs_2": { 73 + "locked": { 74 + "lastModified": 1769089682, 75 + "narHash": "sha256-9yA/LIuAVQq0lXelrZPjLuLVuZdm03p8tfmHhnDIkms=", 25 76 "owner": "NixOS", 26 77 "repo": "nixpkgs", 27 - "rev": "2c3e5ec5df46d3aeee2a1da0bfedd74e21f4bf3a", 78 + "rev": "078d69f03934859a181e81ba987c2bb033eebfc5", 28 79 "type": "github" 29 80 }, 30 81 "original": { ··· 35 86 }, 36 87 "root": { 37 88 "inputs": { 38 - "flake-utils": "flake-utils", 39 - "nixpkgs": "nixpkgs", 89 + "command-utils": "command-utils", 90 + "flake-utils": "flake-utils_2", 91 + "nixpkgs": "nixpkgs_2", 40 92 "rust-overlay": "rust-overlay" 41 93 } 42 94 }, ··· 47 99 ] 48 100 }, 49 101 "locked": { 50 - "lastModified": 1768359079, 51 - "narHash": "sha256-a016mOfKconYrYo3fZLN6c2cnmqYYd44g2bUrBZAsQc=", 102 + "lastModified": 1769136478, 103 + "narHash": "sha256-8UNd5lmGf8phCr/aKxagJ4kNsF0pCHLish2G4ZKCFFY=", 52 104 "owner": "oxalica", 53 105 "repo": "rust-overlay", 54 - "rev": "0357d1826057686637e41147545402cbbda420ce", 106 + "rev": "470ee44393bb19887056b557ea2c03fc5230bd5a", 55 107 "type": "github" 56 108 }, 57 109 "original": { ··· 61 113 } 62 114 }, 63 115 "systems": { 116 + "locked": { 117 + "lastModified": 1681028828, 118 + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 119 + "owner": "nix-systems", 120 + "repo": "default", 121 + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 122 + "type": "github" 123 + }, 124 + "original": { 125 + "owner": "nix-systems", 126 + "repo": "default", 127 + "type": "github" 128 + } 129 + }, 130 + "systems_2": { 64 131 "locked": { 65 132 "lastModified": 1681028828, 66 133 "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+29 -5
flake.nix
··· 8 8 url = "github:oxalica/rust-overlay"; 9 9 inputs.nixpkgs.follows = "nixpkgs"; 10 10 }; 11 + command-utils.url = "git+https://codeberg.org/expede/nix-command-utils"; 11 12 }; 12 13 13 14 outputs = { ··· 15 16 flake-utils, 16 17 nixpkgs, 17 18 rust-overlay, 19 + command-utils, 18 20 } @ inputs: 19 21 flake-utils.lib.eachDefaultSystem ( 20 22 system: let ··· 58 60 ]; 59 61 60 62 cargo-installs = with pkgs; [ 61 - cargo-criterion 62 63 cargo-deny 63 64 cargo-expand 64 65 cargo-outdated 65 66 cargo-sort 66 67 cargo-udeps 67 - cargo-watch 68 68 cargo-component 69 69 ]; 70 70 71 + # Command binary paths 72 + cargo' = "${rust-toolchain}/bin/cargo"; 73 + cargo-audit' = "${pkgs.cargo-audit}/bin/cargo-audit"; 74 + cargo-semver-checks' = "${pkgs.cargo-semver-checks}/bin/cargo-semver-checks"; 75 + cargo-watch' = "${pkgs.cargo-watch}/bin/cargo-watch"; 76 + 77 + # Command utils 78 + rust = command-utils.rust.${system}; 79 + 80 + menu = command-utils.commands.${system} [ 81 + (rust.test { cargo = cargo'; cargo-watch = cargo-watch'; }) 82 + (rust.lint { cargo = cargo'; }) 83 + (rust.fmt { cargo = cargo'; }) 84 + (rust.build { cargo = cargo'; }) 85 + (rust.doc { cargo = cargo'; }) 86 + (rust.watch { cargo-watch = cargo-watch'; }) 87 + (rust.audit { cargo-audit = cargo-audit'; }) 88 + (rust.semver { cargo-semver-checks = cargo-semver-checks'; }) 89 + (rust.ci { cargo = cargo'; }) 90 + ]; 91 + 71 92 in rec { 72 93 devShells.default = pkgs.mkShell { 73 94 name = "futures_kind shell"; ··· 75 96 nativeBuildInputs = with pkgs; 76 97 [ 77 98 rust-toolchain 78 - pkgs.cargo 79 - pkgs.rust-analyzer 99 + rust-analyzer 100 + cargo-watch 101 + cargo-audit 102 + cargo-semver-checks 80 103 ] 104 + ++ (builtins.filter builtins.isAttrs menu) 81 105 ++ format-pkgs 82 106 ++ cargo-installs; 83 107 84 - shellHook = '' 108 + shellHook = '' 85 109 unset SOURCE_DATE_EPOCH 86 110 export WORKSPACE_ROOT="$(pwd)" 87 111 menu
+23
futures_kind/Cargo.toml
··· 1 + [package] 2 + name = "futures_kind" 3 + description = "Abstractions over Send and !Send futures" 4 + readme = "README.md" 5 + keywords = ["futures", "async", "send", "boxed-future"] 6 + categories = ["asynchronous", "no-std"] 7 + 8 + version.workspace = true 9 + edition.workspace = true 10 + rust-version.workspace = true 11 + license.workspace = true 12 + repository.workspace = true 13 + authors.workspace = true 14 + 15 + [dependencies] 16 + futures = { workspace = true } 17 + futures_kind_macros = { workspace = true } 18 + 19 + [dev-dependencies] 20 + tokio = { workspace = true } 21 + 22 + [lints] 23 + workspace = true
+307
futures_kind/README.md
··· 1 + # `futures_kind` 2 + 3 + Abstractions over `Send` and `!Send` futures in Rust. 4 + 5 + ## Motivation 6 + 7 + 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: 8 + 9 + 1. Duplicate their async trait implementations for both `Send` and `!Send` variants, including all consumers (e.g. `MyService` and `MyLocalService`) 10 + 2. Force all users to use `Send` futures, excluding legitimate `!Send` use cases 11 + 3. Create separate crates or feature flags for each variant 12 + 13 + This duplication is verbose, error-prone, and increases maintenance burden. 14 + 15 + ## Approach 16 + 17 + `futures_kind` provides an abstraction that allows you to write async code once and support both `Send` and `!Send` futures through generic implementations. 18 + 19 + ```rust 20 + use futures_kind::{FutureKind, Sendable, Local}; 21 + use futures::future::{BoxFuture, LocalBoxFuture, FutureExt}; 22 + 23 + // Define your trait once, generic over the future kind 24 + pub trait Service<K: FutureKind> { 25 + fn handle<'a>(&'a self, x: u8) -> K::Future<'a, u8>; 26 + } 27 + 28 + struct MyService; 29 + 30 + // Implement for both Send and !Send with minimal boilerplate 31 + impl Service<Local> for MyService { 32 + fn handle<'a>(&'a self, x: u8) -> LocalBoxFuture<'a, u8> { 33 + async move { x * 2 }.boxed_local() 34 + } 35 + } 36 + 37 + impl Service<Sendable> for MyService { 38 + fn handle<'a>(&'a self, x: u8) -> BoxFuture<'a, u8> { 39 + async move { x * 2 }.boxed() 40 + } 41 + } 42 + ``` 43 + 44 + Now users can choose which variant they need: 45 + 46 + ```rust 47 + use futures_kind::{FutureKind, Sendable, Local}; 48 + use futures::future::{BoxFuture, LocalBoxFuture, FutureExt}; 49 + 50 + trait Service<K: FutureKind> { 51 + fn handle<'a>(&'a self, x: u8) -> K::Future<'a, u8>; 52 + } 53 + 54 + struct MyService; 55 + 56 + impl Service<Local> for MyService { 57 + fn handle<'a>(&'a self, x: u8) -> LocalBoxFuture<'a, u8> { 58 + async move { x * 2 }.boxed_local() 59 + } 60 + } 61 + 62 + impl Service<Sendable> for MyService { 63 + fn handle<'a>(&'a self, x: u8) -> BoxFuture<'a, u8> { 64 + async move { x * 2 }.boxed() 65 + } 66 + } 67 + 68 + // For Send-required runtimes like tokio 69 + async fn use_sendable(service: &impl Service<Sendable>) { 70 + let result = service.handle(42).await; 71 + } 72 + 73 + // For !Send runtimes like Wasm or single-threaded executors 74 + async fn use_local(service: &impl Service<Local>) { 75 + let result = service.handle(42).await; 76 + } 77 + ``` 78 + 79 + Or thread through the `FutureKind` parameter and delay to compile time. 80 + This is typesafe, and will complain if you try to send between threads 81 + at which point you'll know that you need to specialize to `Sendable`. 82 + 83 + ```rust 84 + use futures_kind::{FutureKind, Sendable, Local}; 85 + use futures::future::{BoxFuture, LocalBoxFuture, FutureExt}; 86 + 87 + trait Service<K: FutureKind> { 88 + fn handle<'a>(&'a self, x: u8) -> K::Future<'a, u8>; 89 + } 90 + 91 + struct MyService; 92 + 93 + impl Service<Local> for MyService { 94 + fn handle<'a>(&'a self, x: u8) -> LocalBoxFuture<'a, u8> { 95 + async move { x * 2 }.boxed_local() 96 + } 97 + } 98 + 99 + impl Service<Sendable> for MyService { 100 + fn handle<'a>(&'a self, x: u8) -> BoxFuture<'a, u8> { 101 + async move { x * 2 }.boxed() 102 + } 103 + } 104 + 105 + async fn use_unknown<K: FutureKind>(service: &impl Service<K>) { 106 + let result = service.handle(42).await; 107 + } 108 + ``` 109 + 110 + ## Core Types 111 + 112 + ### `FutureKind` Trait 113 + 114 + The central abstraction that defines an associated type for futures: 115 + 116 + ```rust,no_run 117 + use std::future::Future; 118 + 119 + pub trait FutureKind { 120 + type Future<'a, T: 'a>: Future<Output = T> + 'a; 121 + } 122 + ``` 123 + 124 + ### `Sendable` 125 + 126 + Represents `Send` futures, backed by `futures::future::BoxFuture`: 127 + 128 + ```rust,ignore 129 + impl FutureKind for Sendable { 130 + type Future<'a, T: 'a> = BoxFuture<'a, T>; 131 + } 132 + ``` 133 + 134 + ### `Local` 135 + 136 + Represents `!Send` futures, backed by `futures::future::LocalBoxFuture`: 137 + 138 + ```rust,ignore 139 + impl FutureKind for Local { 140 + type Future<'a, T: 'a> = LocalBoxFuture<'a, T>; 141 + } 142 + ``` 143 + 144 + ### `#[kinds]` Macro 145 + 146 + 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: 147 + 148 + ```rust,no_run 149 + use std::marker::PhantomData; 150 + use futures_kind::{kinds, FutureKind}; 151 + 152 + trait Counter<K: FutureKind> { 153 + fn next(&self) -> K::Future<'_, u32>; 154 + } 155 + 156 + struct Memory<K> { 157 + val: u32, 158 + _marker: PhantomData<K>, 159 + } 160 + 161 + // Generates impl Counter<Sendable> and impl Counter<Local> 162 + #[kinds] 163 + impl<K: FutureKind> Counter<K> for Memory<K> { 164 + fn next(&self) -> K::Future<'_, u32> { 165 + let val = self.val; 166 + Box::pin(async move { val + 1 }) 167 + } 168 + } 169 + ``` 170 + 171 + You can also generate only specific variants: 172 + 173 + ```rust,ignore 174 + #[kinds(Sendable)] // Only Sendable 175 + #[kinds(Local)] // Only Local 176 + #[kinds(Sendable, Local)] // Both (default) 177 + ``` 178 + 179 + Each variant can have its own additional bounds using `where`: 180 + 181 + ```rust,ignore 182 + #[kinds(Sendable where T: Send, Local where T: Debug)] 183 + impl<K: FutureKind, T: Clone> Processor<K> for Container<T> { 184 + fn process(&self) -> K::Future<'_, T> { 185 + let value = self.value.clone(); 186 + Box::pin(async move { value }) 187 + } 188 + } 189 + // Generates: 190 + // impl<T: Clone + Send> Processor<Sendable> for Container<T> 191 + // impl<T: Clone + Debug> Processor<Local> for Container<T> 192 + ``` 193 + 194 + ## Threading `FutureKind` Through Your Code 195 + 196 + The simplest pattern is to structure your code so the `FutureKind` type parameter appears naturally in your API. This typically happens when: 197 + 198 + 1. **The function returns the future directly** (not the awaited result) 199 + 2. **You use a struct to carry the type parameter** 200 + 201 + Here's an example using a struct: 202 + 203 + ```rust 204 + use std::marker::PhantomData; 205 + use futures_kind::{FutureKind, Local, Sendable}; 206 + use futures::future::{BoxFuture, LocalBoxFuture, FutureExt}; 207 + 208 + trait Service<K: FutureKind> { 209 + fn handle<'a>(&'a self, x: u8) -> K::Future<'a, u8>; 210 + } 211 + 212 + struct MyService; 213 + 214 + impl Service<Local> for MyService { 215 + fn handle<'a>(&'a self, x: u8) -> LocalBoxFuture<'a, u8> { 216 + async move { x * 2 }.boxed_local() 217 + } 218 + } 219 + 220 + impl Service<Sendable> for MyService { 221 + fn handle<'a>(&'a self, x: u8) -> BoxFuture<'a, u8> { 222 + async move { x * 2 }.boxed() 223 + } 224 + } 225 + 226 + pub struct Handler<K: FutureKind> { 227 + service: MyService, 228 + _marker: PhantomData<K>, 229 + } 230 + 231 + impl<K: FutureKind> Handler<K> 232 + where 233 + MyService: Service<K> 234 + { 235 + pub fn new(service: MyService) -> Self { 236 + Self { 237 + service, 238 + _marker: PhantomData, 239 + } 240 + } 241 + 242 + // K is part of Self, so methods can use it naturally 243 + pub async fn process(&self, x: u8) -> u8 { 244 + Service::<K>::handle(&self.service, x).await 245 + } 246 + } 247 + 248 + # async fn example() { 249 + // Usage is clean and type-safe 250 + let my_service = MyService; 251 + let handler = Handler::<Sendable>::new(my_service); 252 + let result = handler.process(42).await; 253 + # } 254 + ``` 255 + 256 + Or when returning futures directly: 257 + 258 + ```rust,ignore 259 + // K appears in the return type 260 + pub fn create_task<K: FutureKind>(x: u8) -> K::Future<'static, u8> 261 + where 262 + MyService: Service<K> 263 + { 264 + // Return the boxed future, don't await it 265 + async move { x * 2 }.boxed() // or .boxed_local() for Local 266 + } 267 + ``` 268 + 269 + This pattern allows the `FutureKind` choice to propagate through your entire call stack while maintaining type safety. 270 + 271 + ## Use Cases 272 + 273 + | Use Case | Description | 274 + |----------|-------------| 275 + | Cross-platform libraries | Write async traits once, support both native and Wasm targets | 276 + | Runtime flexibility | Allow users to choose their async runtime without forcing `Send` constraints | 277 + | Testing | Use `Local` futures in single-threaded test environments while production uses `Sendable` | 278 + | Gradual migration | Support both variants during migration between runtimes | 279 + 280 + ## Design Philosophy 281 + 282 + `futures_kind` embraces the following principles: 283 + 284 + 1. **Zero-cost abstraction**: The trait compiles down to direct use of `BoxFuture` or `LocalBoxFuture` 285 + 2. **Compile-time dispatch**: All decisions about `Send` vs `!Send` happen at compile time 286 + 3. **Minimal API surface**: Just one trait and two implementations keep the crate focused and maintainable 287 + 4. **Compatibility**: Works seamlessly with the `futures` crate's existing types 288 + 5. **Extensability**: Other boxings or non-boxed types can be implemented directly. 289 + 290 + ## Installation 291 + 292 + Add this to your `Cargo.toml`: 293 + 294 + ```toml 295 + [dependencies] 296 + futures_kind = "0.1.0" 297 + futures = "0.3.31" 298 + ``` 299 + 300 + ## License 301 + 302 + Licensed under either of: 303 + 304 + - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or <http://www.apache.org/licenses/LICENSE-2.0>) 305 + - MIT license ([LICENSE-MIT](LICENSE-MIT) or <http://opensource.org/licenses/MIT>) 306 + 307 + at your option.
+360
futures_kind/tests/macro_tests.rs
··· 1 + #![allow(missing_docs, clippy::missing_const_for_fn, clippy::future_not_send)] 2 + 3 + use core::marker::PhantomData; 4 + use futures::future::{BoxFuture, LocalBoxFuture}; 5 + use futures_kind::{FutureKind, IntoFutureKind, Local, Sendable, kinds}; 6 + 7 + trait Counter<K: FutureKind> { 8 + fn next(&self) -> K::Future<'_, u32>; 9 + } 10 + 11 + struct Memory<K> { 12 + val: u32, 13 + _marker: PhantomData<K>, 14 + } 15 + 16 + impl<K> Memory<K> { 17 + fn new(val: u32) -> Self { 18 + Self { 19 + val, 20 + _marker: PhantomData, 21 + } 22 + } 23 + } 24 + 25 + // Single proc macro generates both Sendable and Local impls 26 + #[kinds] 27 + impl<K: FutureKind> Counter<K> for Memory<K> { 28 + fn next(&self) -> K::Future<'_, u32> { 29 + let val = self.val; 30 + K::into_kind(async move { val + 1 }) 31 + } 32 + } 33 + 34 + #[tokio::test] 35 + async fn test_macro_impl_with_sendable() { 36 + let memory: Memory<Sendable> = Memory::new(41); 37 + let result = <Memory<Sendable> as Counter<Sendable>>::next(&memory).await; 38 + assert_eq!(result, 42); 39 + } 40 + 41 + #[tokio::test] 42 + async fn test_macro_impl_with_local() { 43 + let memory: Memory<Local> = Memory::new(41); 44 + let result = <Memory<Local> as Counter<Local>>::next(&memory).await; 45 + assert_eq!(result, 42); 46 + } 47 + 48 + // Test with multiple methods and arguments 49 + trait Database<K: FutureKind> { 50 + fn get(&self, key: &str) -> K::Future<'_, Option<String>>; 51 + fn count(&self) -> K::Future<'_, usize>; 52 + } 53 + 54 + struct InMemoryDb<K> { 55 + data: std::collections::HashMap<String, String>, 56 + _marker: PhantomData<K>, 57 + } 58 + 59 + #[kinds] 60 + impl<K: FutureKind> Database<K> for InMemoryDb<K> { 61 + fn get(&self, key: &str) -> K::Future<'_, Option<String>> { 62 + let key = key.to_string(); 63 + K::into_kind(async move { self.data.get(&key).cloned() }) 64 + } 65 + 66 + fn count(&self) -> K::Future<'_, usize> { 67 + K::into_kind(async move { self.data.len() }) 68 + } 69 + } 70 + 71 + #[tokio::test] 72 + async fn test_database_sendable() { 73 + let mut data = std::collections::HashMap::new(); 74 + data.insert("foo".to_string(), "bar".to_string()); 75 + let db: InMemoryDb<Sendable> = InMemoryDb { 76 + data, 77 + _marker: PhantomData, 78 + }; 79 + 80 + assert_eq!( 81 + <InMemoryDb<Sendable> as Database<Sendable>>::get(&db, "foo").await, 82 + Some("bar".to_string()) 83 + ); 84 + assert_eq!( 85 + <InMemoryDb<Sendable> as Database<Sendable>>::get(&db, "missing").await, 86 + None 87 + ); 88 + assert_eq!( 89 + <InMemoryDb<Sendable> as Database<Sendable>>::count(&db).await, 90 + 1 91 + ); 92 + } 93 + 94 + #[tokio::test] 95 + async fn test_database_local() { 96 + let mut data = std::collections::HashMap::new(); 97 + data.insert("foo".to_string(), "bar".to_string()); 98 + let db: InMemoryDb<Local> = InMemoryDb { 99 + data, 100 + _marker: PhantomData, 101 + }; 102 + 103 + assert_eq!( 104 + <InMemoryDb<Local> as Database<Local>>::get(&db, "foo").await, 105 + Some("bar".to_string()) 106 + ); 107 + assert_eq!( 108 + <InMemoryDb<Local> as Database<Local>>::get(&db, "missing").await, 109 + None 110 + ); 111 + assert_eq!(<InMemoryDb<Local> as Database<Local>>::count(&db).await, 1); 112 + } 113 + 114 + // Test selective generation - only Sendable 115 + trait SendableOnly<K: FutureKind> { 116 + fn send_only(&self) -> K::Future<'_, u32>; 117 + } 118 + 119 + struct SendOnlyType { 120 + val: u32, 121 + } 122 + 123 + #[kinds(Sendable)] 124 + impl<K: FutureKind> SendableOnly<K> for SendOnlyType { 125 + fn send_only(&self) -> K::Future<'_, u32> { 126 + let val = self.val; 127 + K::into_kind(async move { val * 2 }) 128 + } 129 + } 130 + 131 + #[tokio::test] 132 + async fn test_sendable_only() { 133 + let t = SendOnlyType { val: 21 }; 134 + let result = <SendOnlyType as SendableOnly<Sendable>>::send_only(&t).await; 135 + assert_eq!(result, 42); 136 + } 137 + 138 + // Test selective generation - only Local 139 + trait LocalOnly<K: FutureKind> { 140 + fn local_only(&self) -> K::Future<'_, u32>; 141 + } 142 + 143 + struct LocalOnlyType { 144 + val: u32, 145 + } 146 + 147 + #[kinds(Local)] 148 + impl<K: FutureKind> LocalOnly<K> for LocalOnlyType { 149 + fn local_only(&self) -> K::Future<'_, u32> { 150 + let val = self.val; 151 + K::into_kind(async move { val * 2 }) 152 + } 153 + } 154 + 155 + #[tokio::test] 156 + async fn test_local_only() { 157 + let t = LocalOnlyType { val: 21 }; 158 + let result = <LocalOnlyType as LocalOnly<Local>>::local_only(&t).await; 159 + assert_eq!(result, 42); 160 + } 161 + 162 + // Test explicit both 163 + trait ExplicitBoth<K: FutureKind> { 164 + fn explicit(&self) -> K::Future<'_, u32>; 165 + } 166 + 167 + struct ExplicitBothType { 168 + val: u32, 169 + } 170 + 171 + #[kinds(Sendable, Local)] 172 + impl<K: FutureKind> ExplicitBoth<K> for ExplicitBothType { 173 + fn explicit(&self) -> K::Future<'_, u32> { 174 + let val = self.val; 175 + K::into_kind(async move { val + 1 }) 176 + } 177 + } 178 + 179 + #[tokio::test] 180 + async fn test_explicit_both_sendable() { 181 + let t = ExplicitBothType { val: 41 }; 182 + let result = <ExplicitBothType as ExplicitBoth<Sendable>>::explicit(&t).await; 183 + assert_eq!(result, 42); 184 + } 185 + 186 + #[tokio::test] 187 + async fn test_explicit_both_local() { 188 + let t = ExplicitBothType { val: 41 }; 189 + let result = <ExplicitBothType as ExplicitBoth<Local>>::explicit(&t).await; 190 + assert_eq!(result, 42); 191 + } 192 + 193 + // Test trait-in-trait: consuming a FutureKind-generic trait through another generic context 194 + async fn use_counter_generic<K: FutureKind>(counter: &impl Counter<K>) -> u32 { 195 + counter.next().await 196 + } 197 + 198 + #[tokio::test] 199 + async fn test_trait_in_trait_sendable() { 200 + let memory: Memory<Sendable> = Memory::new(41); 201 + let result = use_counter_generic::<Sendable>(&memory).await; 202 + assert_eq!(result, 42); 203 + } 204 + 205 + #[tokio::test] 206 + async fn test_trait_in_trait_local() { 207 + let memory: Memory<Local> = Memory::new(41); 208 + let result = use_counter_generic::<Local>(&memory).await; 209 + assert_eq!(result, 42); 210 + } 211 + 212 + // Test conditional bounds: T: Send only required for Sendable 213 + trait Processor<K: FutureKind> { 214 + fn process(&self) -> K::Future<'_, String>; 215 + } 216 + 217 + struct Container<T> { 218 + value: T, 219 + } 220 + 221 + #[kinds(Sendable where T: Send, Local)] 222 + impl<K: FutureKind, T: Clone + ToString> Processor<K> for Container<T> { 223 + fn process(&self) -> K::Future<'_, String> { 224 + let value = self.value.clone(); 225 + K::into_kind(async move { value.to_string() }) 226 + } 227 + } 228 + 229 + #[tokio::test] 230 + async fn test_conditional_bounds_sendable() { 231 + // String is Send + Clone + ToString 232 + let container = Container { 233 + value: "hello".to_string(), 234 + }; 235 + let result = <Container<String> as Processor<Sendable>>::process(&container).await; 236 + assert_eq!(result, "hello"); 237 + } 238 + 239 + #[tokio::test] 240 + async fn test_conditional_bounds_local() { 241 + // Rc<str> is !Send but Clone + ToString - only works with Local 242 + use std::rc::Rc; 243 + let container = Container { 244 + value: Rc::<str>::from("world"), 245 + }; 246 + let result = <Container<Rc<str>> as Processor<Local>>::process(&container).await; 247 + assert_eq!(result, "world"); 248 + } 249 + 250 + // Test conditional bounds on Local variant 251 + trait Debugger<K: FutureKind> { 252 + fn debug(&self) -> K::Future<'_, String>; 253 + } 254 + 255 + struct Wrapper<T>(T); 256 + 257 + // Local requires Debug, Sendable requires Send 258 + #[kinds(Sendable where T: Send, Local where T: core::fmt::Debug)] 259 + impl<K: FutureKind, T: Clone> Debugger<K> for Wrapper<T> { 260 + fn debug(&self) -> K::Future<'_, String> { 261 + K::into_kind(async move { "debugged".to_string() }) 262 + } 263 + } 264 + 265 + #[tokio::test] 266 + async fn test_local_conditional_bounds() { 267 + // i32 is Debug + Clone, works with Local 268 + let wrapper = Wrapper(42i32); 269 + let result = <Wrapper<i32> as Debugger<Local>>::debug(&wrapper).await; 270 + assert_eq!(result, "debugged"); 271 + } 272 + 273 + #[tokio::test] 274 + async fn test_sendable_conditional_bounds_separate() { 275 + // i32 is Send + Clone, works with Sendable 276 + let wrapper = Wrapper(42i32); 277 + let result = <Wrapper<i32> as Debugger<Sendable>>::debug(&wrapper).await; 278 + assert_eq!(result, "debugged"); 279 + } 280 + 281 + // ============================================================ 282 + // IntoFutureKind and FutureKind::into_kind tests 283 + // ============================================================ 284 + 285 + // Test IntoFutureKind trait directly 286 + #[tokio::test] 287 + async fn test_into_future_kind_sendable() { 288 + let future: BoxFuture<'_, u32> = IntoFutureKind::into_kind(async { 42 }); 289 + assert_eq!(future.await, 42); 290 + } 291 + 292 + #[tokio::test] 293 + async fn test_into_future_kind_local() { 294 + let future: LocalBoxFuture<'_, u32> = IntoFutureKind::into_kind(async { 42 }); 295 + assert_eq!(future.await, 42); 296 + } 297 + 298 + // Test FutureKind::into_kind method with concrete types 299 + #[tokio::test] 300 + async fn test_sendable_into_kind() { 301 + let future = Sendable::into_kind(async { 42 }); 302 + assert_eq!(future.await, 42); 303 + } 304 + 305 + #[tokio::test] 306 + async fn test_local_into_kind() { 307 + let future = Local::into_kind(async { 42 }); 308 + assert_eq!(future.await, 42); 309 + } 310 + 311 + // Test K::into_kind in a generic context 312 + trait Calculator<K: FutureKind> { 313 + fn add(&self, a: u32, b: u32) -> K::Future<'_, u32>; 314 + } 315 + 316 + struct SimpleCalculator; 317 + 318 + impl Calculator<Sendable> for SimpleCalculator { 319 + fn add(&self, a: u32, b: u32) -> BoxFuture<'_, u32> { 320 + Sendable::into_kind(async move { a + b }) 321 + } 322 + } 323 + 324 + impl Calculator<Local> for SimpleCalculator { 325 + fn add(&self, a: u32, b: u32) -> LocalBoxFuture<'_, u32> { 326 + Local::into_kind(async move { a + b }) 327 + } 328 + } 329 + 330 + #[tokio::test] 331 + async fn test_into_kind_in_trait_sendable() { 332 + let calc = SimpleCalculator; 333 + let result = <SimpleCalculator as Calculator<Sendable>>::add(&calc, 20, 22).await; 334 + assert_eq!(result, 42); 335 + } 336 + 337 + #[tokio::test] 338 + async fn test_into_kind_in_trait_local() { 339 + let calc = SimpleCalculator; 340 + let result = <SimpleCalculator as Calculator<Local>>::add(&calc, 20, 22).await; 341 + assert_eq!(result, 42); 342 + } 343 + 344 + // Test that Sendable::into_kind enforces Send bound 345 + // (This is a compile-time check - if it compiles, it works) 346 + #[tokio::test] 347 + async fn test_sendable_into_kind_with_send_data() { 348 + let data = String::from("hello"); 349 + let future = Sendable::into_kind(async move { if data.is_empty() { 0u32 } else { 42u32 } }); 350 + assert_eq!(future.await, 42); 351 + } 352 + 353 + // Test that Local::into_kind works with !Send data 354 + #[tokio::test] 355 + async fn test_local_into_kind_with_non_send_data() { 356 + use std::rc::Rc; 357 + let data = Rc::new(42u32); 358 + let future = Local::into_kind(async move { *data }); 359 + assert_eq!(future.await, 42); 360 + }
+24
futures_kind_macros/Cargo.toml
··· 1 + [package] 2 + name = "futures_kind_macros" 3 + description = "Proc macros for futures_kind" 4 + readme = "README.md" 5 + keywords = ["futures", "async", "proc-macro"] 6 + categories = ["development-tools::procedural-macro-helpers"] 7 + 8 + version.workspace = true 9 + edition.workspace = true 10 + rust-version.workspace = true 11 + license.workspace = true 12 + repository.workspace = true 13 + authors.workspace = true 14 + 15 + [lib] 16 + proc-macro = true 17 + 18 + [dependencies] 19 + proc-macro2 = { workspace = true } 20 + quote = { workspace = true } 21 + syn = { workspace = true } 22 + 23 + [lints] 24 + workspace = true
+35
futures_kind_macros/README.md
··· 1 + # `futures_kind_macros` 2 + 3 + Proc macros for [`futures_kind`](https://crates.io/crates/futures_kind). 4 + 5 + This crate provides the `#[kinds]` attribute macro for generating trait implementations for both `Sendable` and `Local` future kinds. 6 + 7 + ## Usage 8 + 9 + ```rust,ignore 10 + use futures_kind::{kinds, FutureKind}; 11 + 12 + // Generate both Sendable and Local impls (default) 13 + #[kinds] 14 + impl<K: FutureKind> MyTrait<K> for MyType<K> { ... } 15 + 16 + // Generate only specific variants 17 + #[kinds(Sendable)] // Only Sendable 18 + #[kinds(Local)] // Only Local 19 + #[kinds(Sendable, Local)] // Both (default) 20 + 21 + // Add bounds only for specific variants 22 + #[kinds(Sendable where T: Send, Local)] 23 + impl<K: FutureKind, T: Clone> Processor<K> for Container<T> { ... } 24 + ``` 25 + 26 + See the [`futures_kind`](https://crates.io/crates/futures_kind) crate for full documentation. 27 + 28 + ## License 29 + 30 + Licensed under either of: 31 + 32 + - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or <http://www.apache.org/licenses/LICENSE-2.0>) 33 + - MIT license ([LICENSE-MIT](LICENSE-MIT) or <http://opensource.org/licenses/MIT>) 34 + 35 + at your option.
+478
futures_kind_macros/src/lib.rs
··· 1 + //! Proc macros for `futures_kind`. 2 + //! 3 + //! This crate provides the `#[kinds]` attribute macro. 4 + 5 + use proc_macro::TokenStream; 6 + use proc_macro2::TokenStream as TokenStream2; 7 + use quote::{ToTokens, quote}; 8 + use syn::{ 9 + GenericParam, Ident, ImplItem, ImplItemFn, ItemImpl, ReturnType, Type, TypePath, 10 + WherePredicate, parse_macro_input, parse_quote, 11 + }; 12 + 13 + /// Generate implementations of a trait for `Sendable` and/or `Local` `FutureKind`s. 14 + /// 15 + /// This attribute macro allows you to write a single implementation that works for both 16 + /// `Send` and `!Send` futures, avoiding code duplication. 17 + /// 18 + /// # Usage 19 + /// 20 + /// ```rust,ignore 21 + /// // Generate both Sendable and Local impls (default) 22 + /// #[kinds] 23 + /// impl<K: FutureKind> MyTrait<K> for MyType<K> { ... } 24 + /// 25 + /// // Explicitly specify which kinds to generate 26 + /// #[kinds(Sendable, Local)] 27 + /// impl<K: FutureKind> MyTrait<K> for MyType<K> { ... } 28 + /// 29 + /// // Generate only Sendable impl 30 + /// #[kinds(Sendable)] 31 + /// impl<K: FutureKind> MyTrait<K> for MyType<K> { ... } 32 + /// 33 + /// // Generate only Local impl 34 + /// #[kinds(Local)] 35 + /// impl<K: FutureKind> MyTrait<K> for MyType<K> { ... } 36 + /// 37 + /// // Add bounds only for specific variants 38 + /// #[kinds(Sendable where T: Send, Local)] 39 + /// impl<K: FutureKind, T: Clone> MyTrait<K> for Container<T> { ... } 40 + /// // Generates: impl<T: Clone + Send> MyTrait<Sendable> for Container<T> 41 + /// // impl<T: Clone> MyTrait<Local> for Container<T> 42 + /// ``` 43 + /// 44 + /// # Example 45 + /// 46 + /// ```rust,ignore 47 + /// use std::marker::PhantomData; 48 + /// use futures_kind::{FutureKind, Sendable, Local, kinds}; 49 + /// 50 + /// trait Counter<K: FutureKind> { 51 + /// fn next(&self) -> K::Future<'_, u32>; 52 + /// } 53 + /// 54 + /// struct Memory<K> { 55 + /// val: u32, 56 + /// _marker: PhantomData<K>, 57 + /// } 58 + /// 59 + /// #[kinds] 60 + /// impl<K: FutureKind> Counter<K> for Memory<K> { 61 + /// fn next(&self) -> K::Future<'_, u32> { 62 + /// let val = self.val; 63 + /// Box::pin(async move { val + 1 }) 64 + /// } 65 + /// } 66 + /// ``` 67 + #[proc_macro_attribute] 68 + pub fn kinds(attr: TokenStream, item: TokenStream) -> TokenStream { 69 + let input = parse_macro_input!(item as ItemImpl); 70 + let kinds = parse_kinds(&attr); 71 + 72 + match generate_impls(&input, &kinds) { 73 + Ok(tokens) => tokens.into(), 74 + Err(err) => err.to_compile_error().into(), 75 + } 76 + } 77 + 78 + #[allow(clippy::expect_used)] // expects are guarded by starts_with checks 79 + fn parse_kinds(attr: &TokenStream) -> Vec<FutureKindVariant> { 80 + let attr_str = attr.to_string(); 81 + let attr_str = attr_str.trim(); 82 + 83 + if attr_str.is_empty() { 84 + // Default: generate both 85 + return vec![ 86 + FutureKindVariant { 87 + kind: FutureKindType::Sendable, 88 + extra_bounds: vec![], 89 + }, 90 + FutureKindVariant { 91 + kind: FutureKindType::Local, 92 + extra_bounds: vec![], 93 + }, 94 + ]; 95 + } 96 + 97 + let mut kinds = Vec::new(); 98 + 99 + // Split by comma, but be careful not to split inside where clauses 100 + // We'll parse by finding variant names and their optional where clauses 101 + let mut remaining = attr_str; 102 + 103 + while !remaining.is_empty() { 104 + remaining = remaining.trim_start_matches(',').trim(); 105 + if remaining.is_empty() { 106 + break; 107 + } 108 + 109 + // Find the variant name 110 + let (kind_type, rest) = if remaining.starts_with("Sendable") { 111 + ( 112 + Some(FutureKindType::Sendable), 113 + remaining 114 + .strip_prefix("Sendable") 115 + .expect("guarded by starts_with"), 116 + ) 117 + } else if remaining.starts_with("Local") { 118 + ( 119 + Some(FutureKindType::Local), 120 + remaining 121 + .strip_prefix("Local") 122 + .expect("guarded by starts_with"), 123 + ) 124 + } else { 125 + // Skip unknown token until comma or end 126 + let end = remaining.find(',').unwrap_or(remaining.len()); 127 + remaining = &remaining[end..]; 128 + continue; 129 + }; 130 + 131 + let rest = rest.trim(); 132 + 133 + // Check for where clause 134 + let (extra_bounds, after_where) = if rest.starts_with("where") { 135 + let where_content = rest 136 + .strip_prefix("where") 137 + .expect("guarded by starts_with") 138 + .trim(); 139 + // Find where this variant's where clause ends (at next Sendable/Local or end) 140 + let end_idx = find_next_variant(where_content); 141 + let bounds_str = where_content[..end_idx].trim().trim_end_matches(','); 142 + 143 + // Parse the bounds 144 + let bounds = parse_where_predicates(bounds_str); 145 + (bounds, &where_content[end_idx..]) 146 + } else { 147 + (vec![], rest) 148 + }; 149 + 150 + if let Some(kind) = kind_type { 151 + kinds.push(FutureKindVariant { kind, extra_bounds }); 152 + } 153 + 154 + remaining = after_where; 155 + } 156 + 157 + if kinds.is_empty() { 158 + // Fallback to both if parsing failed 159 + vec![ 160 + FutureKindVariant { 161 + kind: FutureKindType::Sendable, 162 + extra_bounds: vec![], 163 + }, 164 + FutureKindVariant { 165 + kind: FutureKindType::Local, 166 + extra_bounds: vec![], 167 + }, 168 + ] 169 + } else { 170 + kinds 171 + } 172 + } 173 + 174 + fn find_next_variant(s: &str) -> usize { 175 + // Find the position of the next "Sendable" or "Local" that starts a new variant 176 + // We need to be careful about these words appearing inside bounds 177 + let mut depth: usize = 0; 178 + 179 + for (i, c) in s.chars().enumerate() { 180 + match c { 181 + '<' => depth += 1, 182 + '>' => depth = depth.saturating_sub(1), 183 + ',' if depth == 0 => { 184 + // Check if after this comma we have a variant name 185 + if let Some(after_comma) = s.get(i + 1..) { 186 + let after_comma = after_comma.trim_start(); 187 + if after_comma.starts_with("Sendable") || after_comma.starts_with("Local") { 188 + return i; 189 + } 190 + } 191 + } 192 + _ => {} 193 + } 194 + } 195 + 196 + s.len() 197 + } 198 + 199 + fn parse_where_predicates(s: &str) -> Vec<WherePredicate> { 200 + if s.is_empty() { 201 + return vec![]; 202 + } 203 + 204 + // Split by '+' for multiple bounds on same type, or parse as single predicate 205 + // Actually, where predicates are separated by commas, and each can have multiple bounds with + 206 + // For simplicity, we'll try to parse the whole thing as comma-separated predicates 207 + let mut predicates = vec![]; 208 + 209 + // Split carefully by comma, respecting angle brackets 210 + let mut depth: usize = 0; 211 + let mut start = 0; 212 + let chars: Vec<char> = s.chars().collect(); 213 + 214 + for (i, &c) in chars.iter().enumerate() { 215 + match c { 216 + '<' => depth += 1, 217 + '>' => depth = depth.saturating_sub(1), 218 + ',' if depth == 0 => { 219 + let pred_str = s[start..i].trim(); 220 + if let Ok(pred) = syn::parse_str::<WherePredicate>(pred_str) { 221 + predicates.push(pred); 222 + } 223 + start = i + 1; 224 + } 225 + _ => {} 226 + } 227 + } 228 + 229 + // Don't forget the last predicate 230 + let pred_str = s[start..].trim(); 231 + if !pred_str.is_empty() 232 + && let Ok(pred) = syn::parse_str::<WherePredicate>(pred_str) 233 + { 234 + predicates.push(pred); 235 + } 236 + 237 + predicates 238 + } 239 + 240 + fn generate_impls(input: &ItemImpl, kinds: &[FutureKindVariant]) -> syn::Result<TokenStream2> { 241 + // Find the K type parameter 242 + let k_param = find_k_param(input)?; 243 + 244 + // Generate impl for each requested kind 245 + let impls: Vec<TokenStream2> = kinds 246 + .iter() 247 + .map(|kind| generate_impl_for_kind(input, &k_param, kind)) 248 + .collect(); 249 + 250 + Ok(quote! { 251 + #(#impls)* 252 + }) 253 + } 254 + 255 + #[derive(Clone, Copy, PartialEq, Eq)] 256 + enum FutureKindType { 257 + Sendable, 258 + Local, 259 + } 260 + 261 + impl FutureKindType { 262 + fn kind_type(self) -> TokenStream2 { 263 + match self { 264 + Self::Sendable => quote!(::futures_kind::Sendable), 265 + Self::Local => quote!(::futures_kind::Local), 266 + } 267 + } 268 + 269 + fn future_type(self) -> TokenStream2 { 270 + match self { 271 + Self::Sendable => quote!(::futures::future::BoxFuture), 272 + Self::Local => quote!(::futures::future::LocalBoxFuture), 273 + } 274 + } 275 + } 276 + 277 + #[derive(Clone)] 278 + struct FutureKindVariant { 279 + kind: FutureKindType, 280 + extra_bounds: Vec<WherePredicate>, 281 + } 282 + 283 + fn find_k_param(input: &ItemImpl) -> syn::Result<Ident> { 284 + // Look for a type parameter that has FutureKind bound 285 + for param in &input.generics.params { 286 + if let GenericParam::Type(type_param) = param { 287 + for bound in &type_param.bounds { 288 + if let syn::TypeParamBound::Trait(trait_bound) = bound { 289 + let path = &trait_bound.path; 290 + if path 291 + .segments 292 + .last() 293 + .is_some_and(|s| s.ident == "FutureKind") 294 + { 295 + return Ok(type_param.ident.clone()); 296 + } 297 + } 298 + } 299 + } 300 + } 301 + 302 + Err(syn::Error::new_spanned( 303 + &input.generics, 304 + "Expected a type parameter with FutureKind bound (e.g., `K: FutureKind`)", 305 + )) 306 + } 307 + 308 + fn generate_impl_for_kind( 309 + input: &ItemImpl, 310 + k_param: &Ident, 311 + variant: &FutureKindVariant, 312 + ) -> TokenStream2 { 313 + let kind_type = variant.kind.kind_type(); 314 + let future_type = variant.kind.future_type(); 315 + 316 + // Clone and modify generics - remove the K parameter 317 + let mut new_generics = input.generics.clone(); 318 + new_generics.params = new_generics 319 + .params 320 + .into_iter() 321 + .filter(|p| { 322 + if let GenericParam::Type(tp) = p { 323 + tp.ident != *k_param 324 + } else { 325 + true 326 + } 327 + }) 328 + .collect(); 329 + 330 + // Remove where clause predicates that reference K, and add extra bounds 331 + if let Some(ref mut where_clause) = new_generics.where_clause { 332 + where_clause.predicates = where_clause 333 + .predicates 334 + .clone() 335 + .into_iter() 336 + .filter(|pred| !predicate_references_ident(pred, k_param)) 337 + .collect(); 338 + // Add variant-specific extra bounds 339 + for bound in &variant.extra_bounds { 340 + where_clause.predicates.push(bound.clone()); 341 + } 342 + } else if !variant.extra_bounds.is_empty() { 343 + // Create a where clause if we have extra bounds but none existed 344 + let mut predicates = syn::punctuated::Punctuated::new(); 345 + for bound in &variant.extra_bounds { 346 + predicates.push(bound.clone()); 347 + } 348 + new_generics.where_clause = Some(syn::WhereClause { 349 + where_token: syn::token::Where::default(), 350 + predicates, 351 + }); 352 + } 353 + 354 + // Replace K in self_ty 355 + let new_self_ty = replace_ident_in_type(&input.self_ty, k_param, &kind_type); 356 + 357 + // Replace K in trait path if present 358 + let new_trait = input.trait_.as_ref().map(|(bang, path, for_token)| { 359 + let new_path = replace_ident_in_path(path, k_param, &kind_type); 360 + (*bang, new_path, *for_token) 361 + }); 362 + 363 + // Transform methods 364 + let new_items: Vec<ImplItem> = input 365 + .items 366 + .iter() 367 + .map(|item| transform_impl_item(item, k_param, &kind_type, &future_type)) 368 + .collect(); 369 + 370 + let (impl_generics, _, where_clause) = new_generics.split_for_impl(); 371 + 372 + let trait_tokens = new_trait.map(|(bang, path, for_token)| { 373 + quote! { #bang #path #for_token } 374 + }); 375 + 376 + quote! { 377 + impl #impl_generics #trait_tokens #new_self_ty #where_clause { 378 + #(#new_items)* 379 + } 380 + } 381 + } 382 + 383 + fn predicate_references_ident(pred: &syn::WherePredicate, ident: &Ident) -> bool { 384 + let tokens = pred.to_token_stream().to_string(); 385 + tokens.contains(&ident.to_string()) 386 + } 387 + 388 + fn replace_ident_in_type(ty: &Type, from: &Ident, to: &TokenStream2) -> TokenStream2 { 389 + let ty_str = ty.to_token_stream().to_string(); 390 + let from_str = from.to_string(); 391 + 392 + // Simple token replacement 393 + let replaced = ty_str.replace(&from_str, &to.to_string()); 394 + replaced.parse().unwrap_or_else(|_| ty.to_token_stream()) 395 + } 396 + 397 + fn replace_ident_in_path(path: &syn::Path, from: &Ident, to: &TokenStream2) -> syn::Path { 398 + let path_str = path.to_token_stream().to_string(); 399 + let from_str = from.to_string(); 400 + 401 + let replaced = path_str.replace(&from_str, &to.to_string()); 402 + syn::parse_str(&replaced).unwrap_or_else(|_| path.clone()) 403 + } 404 + 405 + #[allow(clippy::wildcard_enum_match_arm)] // We want to pass through unknown variants unchanged 406 + fn transform_impl_item( 407 + item: &ImplItem, 408 + k_param: &Ident, 409 + kind_type: &TokenStream2, 410 + future_type: &TokenStream2, 411 + ) -> ImplItem { 412 + match item { 413 + ImplItem::Fn(method) => { 414 + ImplItem::Fn(transform_method(method, k_param, kind_type, future_type)) 415 + } 416 + other => other.clone(), 417 + } 418 + } 419 + 420 + fn transform_method( 421 + method: &ImplItemFn, 422 + k_param: &Ident, 423 + kind_type: &TokenStream2, 424 + future_type: &TokenStream2, 425 + ) -> ImplItemFn { 426 + let mut new_method = method.clone(); 427 + 428 + // Transform return type if it's K::Future<...> 429 + if let ReturnType::Type(arrow, ty) = &method.sig.output { 430 + let new_ty = transform_return_type(ty, k_param, future_type); 431 + new_method.sig.output = ReturnType::Type(*arrow, Box::new(new_ty)); 432 + } 433 + 434 + // Transform method body: replace K with concrete kind type 435 + // This handles K::into_kind, K::Future, etc. 436 + let body_str = new_method.block.to_token_stream().to_string(); 437 + let k_str = k_param.to_string(); 438 + let kind_str = kind_type.to_string(); 439 + 440 + // Replace K followed by :: (handles K::into_kind, K::Future, etc.) 441 + // syn may tokenize as "K ::" or "K::" depending on context 442 + let replaced = body_str 443 + .replace(&format!("{k_str} ::"), &format!("{kind_str} ::")) 444 + .replace(&format!("{k_str}::"), &format!("{kind_str}::")); 445 + 446 + if let Ok(new_block) = syn::parse_str(&replaced) { 447 + new_method.block = new_block; 448 + } 449 + 450 + new_method 451 + } 452 + 453 + fn transform_return_type(ty: &Type, k_param: &Ident, future_type: &TokenStream2) -> Type { 454 + // Check if it's K::Future<...> 455 + if let Type::Path(TypePath { qself: None, path }) = ty 456 + && let (Some(first), Some(second)) = (path.segments.get(0), path.segments.get(1)) 457 + && path.segments.len() == 2 458 + && first.ident == *k_param 459 + && second.ident == "Future" 460 + && let syn::PathArguments::AngleBracketed(args) = &second.arguments 461 + { 462 + // Extract the generic arguments from Future<'a, T> 463 + let args_tokens = &args.args; 464 + return parse_quote!(#future_type<#args_tokens>); 465 + } 466 + 467 + // Fallback: do string replacement 468 + let ty_str = ty.to_token_stream().to_string(); 469 + let k_str = k_param.to_string(); 470 + let pattern = format!("{k_str} :: Future"); 471 + 472 + if ty_str.contains(&pattern) { 473 + let replaced = ty_str.replace(&pattern, &future_type.to_string()); 474 + syn::parse_str(&replaced).unwrap_or_else(|_| ty.clone()) 475 + } else { 476 + ty.clone() 477 + } 478 + }
+93 -3
src/lib.rs futures_kind/src/lib.rs
··· 3 3 use core::future::Future; 4 4 use futures::future::{BoxFuture, LocalBoxFuture}; 5 5 6 + pub use futures::future::FutureExt; 7 + pub use futures_kind_macros::kinds; 8 + 6 9 /// An abstraction over [`Future`]s. 7 10 /// 8 11 /// This trait allows you to write async code that works with both `Send` and `!Send` futures, ··· 39 42 /// } 40 43 /// } 41 44 /// 42 - /// // Implement for !Send futures (e.g., WASM or single-threaded runtime) 45 + /// // Implement for !Send futures (e.g., Wasm or single-threaded runtime) 43 46 /// impl Database<Local> for InMemoryDb { 44 47 /// fn get<'a>(&'a self, key: &'a str) -> LocalBoxFuture<'a, Option<String>> { 45 48 /// async move { self.data.get(key).cloned() }.boxed_local() ··· 103 106 /// 104 107 /// This is especially useful for abstracting over `Send` and `!Send` futures. 105 108 type Future<'a, T: 'a>: Future<Output = T> + 'a; 109 + 110 + /// Lift a future into this kind's future type. 111 + /// 112 + /// # Example 113 + /// 114 + /// ```rust 115 + /// use futures_kind::{FutureKind, Sendable, Local}; 116 + /// use futures::future::{BoxFuture, LocalBoxFuture}; 117 + /// 118 + /// fn sendable_example() -> BoxFuture<'static, u32> { 119 + /// Sendable::into_kind(async { 42 }) 120 + /// } 121 + /// 122 + /// fn local_example() -> LocalBoxFuture<'static, u32> { 123 + /// Local::into_kind(async { 42 }) 124 + /// } 125 + /// ``` 126 + fn into_kind<'a, T, F>(f: F) -> Self::Future<'a, T> 127 + where 128 + T: 'a, 129 + F: Future<Output = T> + 'a, 130 + Self::Future<'a, T>: IntoFutureKind<'a, T, F>, 131 + { 132 + IntoFutureKind::into_kind(f) 133 + } 106 134 } 107 135 108 136 /// Abstraction over [`Send`] futures. ··· 141 169 /// Abstraction over [`!Send`][Send] futures. 142 170 /// 143 171 /// Use this when your async code runs in single-threaded environments like 144 - /// WASM, browser contexts, or single-threaded executors that don't require 172 + /// Wasm, browser contexts, or single-threaded executors that don't require 145 173 /// futures to be `Send`. 146 174 /// 147 175 /// # Example ··· 156 184 /// 157 185 /// struct BrowserCache; 158 186 /// 159 - /// // Implement for !Send futures - can be used in WASM 187 + /// // Implement for !Send futures - can be used in Wasm 160 188 /// impl Cache<Local> for BrowserCache { 161 189 /// fn fetch<'a>(&'a self, key: &'a str) -> LocalBoxFuture<'a, Option<String>> { 162 190 /// async move { ··· 171 199 impl FutureKind for Local { 172 200 type Future<'a, T: 'a> = LocalBoxFuture<'a, T>; 173 201 } 202 + 203 + /// A trait for lifting futures into their [`FutureKind`]-specific type. 204 + /// 205 + /// This abstracts over the wrapping operation, allowing generic code to construct 206 + /// the appropriate future type without knowing the concrete kind. 207 + /// 208 + /// Typically you'll use [`FutureKind::into_kind`] instead of calling this directly. 209 + /// 210 + /// # Example 211 + /// 212 + /// ```rust 213 + /// use futures_kind::{FutureKind, Sendable, Local}; 214 + /// use futures::future::{BoxFuture, LocalBoxFuture}; 215 + /// 216 + /// trait Counter<K: FutureKind> { 217 + /// fn next(&self) -> K::Future<'_, u32>; 218 + /// } 219 + /// 220 + /// struct Memory { val: u32 } 221 + /// 222 + /// impl Counter<Sendable> for Memory { 223 + /// fn next(&self) -> BoxFuture<'_, u32> { 224 + /// let val = self.val; 225 + /// Sendable::into_kind(async move { val + 1 }) 226 + /// } 227 + /// } 228 + /// 229 + /// impl Counter<Local> for Memory { 230 + /// fn next(&self) -> LocalBoxFuture<'_, u32> { 231 + /// let val = self.val; 232 + /// Local::into_kind(async move { val + 1 }) 233 + /// } 234 + /// } 235 + /// ``` 236 + pub trait IntoFutureKind<'a, T, F> 237 + where 238 + T: 'a, 239 + F: Future<Output = T> + 'a, 240 + { 241 + /// Lift a future into this type. 242 + fn into_kind(f: F) -> Self; 243 + } 244 + 245 + impl<'a, T, F> IntoFutureKind<'a, T, F> for BoxFuture<'a, T> 246 + where 247 + T: 'a, 248 + F: Future<Output = T> + Send + 'a, 249 + { 250 + fn into_kind(f: F) -> Self { 251 + Box::pin(f) 252 + } 253 + } 254 + 255 + impl<'a, T, F> IntoFutureKind<'a, T, F> for LocalBoxFuture<'a, T> 256 + where 257 + T: 'a, 258 + F: Future<Output = T> + 'a, 259 + { 260 + fn into_kind(f: F) -> Self { 261 + Box::pin(f) 262 + } 263 + }