Skip to content

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.

Zynx
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.

Zynx
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.

Zynx
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:

Zynx
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.

Zynx
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

NameCasesDescription
ChannelErrorClosedSending failed because the channel is closed.
Recv<T>Value(T), ClosedResult of receiving from a channel.

Types

TypeKindPublic fieldsMethodsDescription
Sender<T>structNoneclone, close, sendSending handle.
Receiver<T>structNoneclone, close, recvReceiving handle.

Functions And Methods

MethodSignatureParametersReturnsDescription
boundedbounded<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.cloneclone() -> Sender<T>NoneSender<T>Retains another sending handle.
Sender.closeclose()NonevoidCloses this sending handle.
Sender.sendsend(value: T) -> ^async.Future<void throws(ChannelError)> where Sendvalue: value to send^async.Future<void throws(ChannelError)> where SendSends a value asynchronously; the channel requires T: Send.
Receiver.cloneclone() -> Receiver<T>NoneReceiver<T>Retains another receiving handle.
Receiver.closeclose()NonevoidCloses this receiving handle.
Receiver.recvrecv() -> ^async.Future<Recv<T>> where SendNone^async.Future<Recv<T>> where SendReceives 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.

Released under the MIT License.