std.channel
std.channel provides async multi-producer, multi-consumer channels. Senders and receivers are owned handles; cloning a handle retains the underlying channel, and dropping or closing handles updates channel liveness.
How It Works
bounded<T>(capacity) returns a (Sender<T>, Receiver<T>) pair. Capacity 0 creates a rendezvous channel. Positive capacity allows sends to complete before a receiver is waiting.
Channel<void> is the ordinary zero-payload event channel. send(void()) queues or rendezvous-transfers exactly one logical event, and receive produces Recv.Value(void()). Capacity counts events rather than payload bytes, close remains the separate Recv.Closed case, and cancellation does not manufacture or consume an event. The implementation may omit payload storage, but there is no special API, queue kind, readiness path, or event coalescing.
Channel payloads cross runtime worker queues, so T must be thread-send-safe. Scalars, send-safe enums, and plain owned structs whose fields are send-safe are accepted. Checked references, raw pointers, str, slices, fixed arrays, unqualified Futures, scoped Join handles, unqualified erased callables, Unique<T>, and user-defined drop types are rejected in the current language. Direct closure expressions may be sent only when their explicit capture environment is send-safe. An erased callable payload is accepted only when its element type retains that proof with where Send; named functions and noncapturing closures can prove Send + Sync.
A cold Future is a payload only as ^Future<T> where Send. This qualification proves the entire frame portable and does not require its eventual T to be Send: the receiver owns and may execute the frame locally. A region-bound qualified Future is still rejected because channel storage can outlive its borrow. An unqualified Future cannot be strengthened at send.
Payload ownership moves into the channel during send. A received Recv.Value(value) is owned by the receiver and drops on the receiver side like any other local value. Buffered payloads that are never received are destroyed by channel cleanup when the channel becomes unreachable. Since user-defined drop types are not valid channel payloads in the current language, channel cleanup cannot run a user destructor on a different worker thread; runtime-owned channel handle cleanup is thread-safe by contract.
type Job = ^(^self) -> i32 where Send;
let (tx, rx) = channel.bounded<Job>(1);
let base = 40;
let job: Job = [base] () -> i32 { return base + 2; };
await tx.send(<-job); // qualified erased callable is a valid payload
_ = rx;If the same closure is first erased to unqualified ^(^self) -> i32, the send is rejected. Qualification is part of the static channel element type; the runtime stores no capability witness and cannot recover one from the erased environment.
fn work(value: i32) -> ^Future<i32> where Send {
return value + 1;
}
let (future_tx, future_rx) = channel.bounded<^Future<i32> where Send>(1);
let future = work(41);
await future_tx.send(<-future); // accepted: exact qualified identity
_ = future_rx;The existing unqualified ^Future<i32> case remains a compile error. There is no hidden frame inspection, runtime portability flag, or cast that repairs it.
import std.channel;
fn main() {
let (tx, rx) = channel.bounded<i32>(1);
await tx.send(42);
let msg = await rx.recv();
match msg {
.Value(v) => {
assert v == 42;
},
.Closed => {
assert false;
}
}
}An event channel uses the same surface:
let (tx, rx) = channel.bounded<void>(1);
await tx.send(void());
match await rx.recv() {
.Value(done) => { assert done == void(); },
.Closed => { assert false; },
}Closing
Closing a sender prevents future sends through that handle. Buffered values can still be received before Recv.Closed.
import std.channel;
fn value_or_closed(msg: channel.Recv<i32>) -> i32 {
match msg {
.Value(v) => { return v; },
.Closed => { return -1; }
}
}
fn main() {
let (tx, rx) = channel.bounded<i32>(1);
await tx.send(5);
tx.close();
assert value_or_closed(await rx.recv()) == 5;
assert value_or_closed(await rx.recv()) == -1;
}Ownership, Allocation, Errors, And Unspecified Behavior
Sender<T> and Receiver<T> are owned handles to shared channel state. Cloning a handle retains that state; dropping a handle releases that endpoint. Buffered values are owned by the channel until received or cleaned up.
bounded(capacity) allocates runtime channel state. send may suspend when the buffer is full or capacity is zero. Sending through a closed endpoint throws ChannelError.Closed; receiving from a closed and drained channel returns Recv.Closed.
Scheduling fairness between waiting senders and receivers, wake ordering, and cross-worker batching are unspecified. Use Capsule<T> for isolated owned payloads that need to cross worker threads, and use std.async for scheduler and cancellation behavior.
API Reference
Errors And Enums
| Name | Cases | Description |
|---|---|---|
ChannelError | Closed | Sending failed because the channel is closed. |
Recv<T> | Value(T), Closed | Result of receiving from a channel. |
Types
| Type | Kind | Public fields | Methods | Description |
|---|---|---|---|---|
Sender<T> | struct | None | clone, close, send | Sending handle. |
Receiver<T> | struct | None | clone, close, recv | Receiving handle. |
Functions And Methods
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
bounded | bounded<T>(capacity: usize) -> (Sender<T>, Receiver<T>) | capacity: logical message slots | (Sender<T>, Receiver<T>) | Creates a bounded channel; Channel<void> counts zero-byte events as ordinary slots. |
Sender.clone | clone() -> Sender<T> | None | Sender<T> | Retains another sending handle. |
Sender.close | close() | None | void | Closes this sending handle. |
Sender.send | send(value: T) -> ^async.Future<void throws(ChannelError)> where Send | value: value to send | ^async.Future<void throws(ChannelError)> where Send | Sends a value asynchronously; the channel requires T: Send. |
Receiver.clone | clone() -> Receiver<T> | None | Receiver<T> | Retains another receiving handle. |
Receiver.close | close() | None | void | Closes this receiving handle. |
Receiver.recv | recv() -> ^async.Future<Recv<T>> where Send | None | ^async.Future<Recv<T>> where Send | Receives a value or closed notification asynchronously; the channel requires T: Send. |
Status
std.channel is the current public async channel API. For the full exported surface, see Standard Library Inventory.