ScopedJoinSet
A lifetime-aware, scoped concurrency wrapper for tokio::task::JoinSet.
This crate enables spawning non-'static futures that can borrow data from the stack, allowing for safe, efficient, and scoped parallel execution without the need for Arc, Mutex, or cloning.
⚠️ Critical Safety Warning: Cancellation Safety
The scope function provided by this crate is not cancellation safe.
If the future returned by scope(...) is dropped before it completes (for example, if wrapped in a tokio::time::timeout or tokio::select! branch that gets cancelled), the process will abort immediately.
Why?
This strict behavior is required for soundness. Tasks spawned within the scope may borrow local variables from the surrounding stack. If the scope were dropped implicitly without waiting for tasks to finish, those tasks could continue running and access invalid memory (use-after-free). The library enforces an abort to prevent this undefined behavior.
Features
- Spawn non-
'staticfutures: Tasks can borrow local variables directly from the stack. - Functional
scopeAPI: A high-level, easy-to-use closure-based API similar tostd::thread::scopeorcrossbeam::scope, but for async Tokio tasks. - No
Arcrequired: Avoids the overhead and complexity of reference counting for local data. - Type-Safe Results: Tasks return typed results, recovered safely upon completion.
- Zero-Cost Abstractions: Uses strict pinning and pointer casting internally to avoid hash maps or heavy tracking structures.
Installation
Add this to your Cargo.toml:
[]
= "0.8.1"
= { = "1", = ["full"] }
Quick Start
The primary entry point is the scope function. It ensures that all spawned tasks are joined before the scope exits.
use scope;
async
Low-Level API: ScopedJoinSet
If you need more granular control than the closure-based scope function offers, you can use the ScopedJoinSet struct directly.
Note: ScopedJoinSet::new() is unsafe. You must ensure that the ScopedJoinSet is not dropped before all tasks have completed. If it is dropped prematurely, it will trigger the same abort mechanism described above to preserve memory safety.
use ScopedJoinSet;
async
How It Works
Tokio requires all spawned tasks to be 'static. To bypass this limitation safely, ScopedJoinSet employs the following mechanism:
- Heap Allocation: When a task is spawned, a slot for its result is allocated on the heap.
- Pointer Erasure: The user's future is wrapped in a
WriteOutputstruct. This wrapper holds a raw pointer (SendPtr) to the result slot. - Static Promotion: The wrapped future is pinned and unsafely cast to
'static. This is sound because thescopefunction (or correct manual usage ofScopedJoinSet) guarantees the scope—and thus the borrowed data—outlives the task execution. - Completion: Upon task completion, the result is written to the pointer.
ScopedJoinSetretrieves this result viajoin_nextand deallocates the heap slot. - Safety Guard: An
AbortOnDropguard is active during thescopeexecution. If the scope is dropped (cancelled) while tasks are running, the guard triggersstd::process::abort()to prevent invalid memory access.
Comparison with Tokio JoinSet
| Feature | Tokio JoinSet |
scoped-join-set |
|---|---|---|
Spawn non-'static tasks |
❌ No | ✅ Yes |
| Borrow from stack | ❌ No | ✅ Yes |
| Scoped lifetime | ❌ No | ✅ Yes |
| Implicit Drop Behavior | ✅ Safe (Detaches/Cancels) | ❌ Aborts Process |
License
MIT OR Apache-2.0, at your option.