18Asynchronous Programming
Cryo supports async / await as a first-class language feature. An async function is compiled into a state machine: its body is split at every suspension point, the values that must survive a suspension become fields of a generated struct, and calling the function builds that struct instead of running the body. Nothing runs until the resulting future is polled.
The model is stackless. A future is an ordinary struct with no hidden heap allocation, no separate stack, and no runtime machinery of its own - async is a compile-time transformation, and the executor that drives futures is an ordinary library (std::future), not part of the language.
This section describes the surface. For how the lowering and the runtime are built - and why futures are never pinned - see docs/async-internals.md.
18.1 Async Functions
async before function makes the function asynchronous. The declared return type is the value the function eventually produces, not the future:
import std::future;
async function add_later(a: i64, b: i64) -> i64 {
const base: i64 = await ready_value(a);
return base + b;
}
add_later(1, 2) returns a future whose output is i64. The body has not begun.
An async function may return void, be generic, and be declared in any order relative to its callers:
async function log_it(msg: Str) -> void { ... }
async function first<T>(a: T, b: T) -> T { ... }
18.2 The await Operator
await suspends the enclosing async body until its operand - which must implement Future - completes, and evaluates to that future's output.
const n: i64 = await add_later(1, 2);
await is an ordinary prefix operator and may appear anywhere an expression may appear: in a condition, a loop body, an operand of a larger expression, a match subject, a match arm body, and a match arm guard.
async function drain(mut q: Queue) -> i64 {
mut total: i64 = 0;
while (await q.has_more()) {
total = total + (await q.pop()) * 2;
}
match (await q.status()) {
Status::Clean => { return total; }
Status::Dirty(n) if await q.can_retry() => { return total - n; }
_ => { return 0; }
}
}
await is only legal inside an async function or method. Using it elsewhere - an ordinary function or method, a global's initializer, or a lambda's body, which is a function of its own even when the lambda is written in an async body - is an error at the await (E0600).
18.3 The Future Trait
await is defined in terms of one trait, from std::future:
type trait Future {
type Output;
poll(mut &this, cx: Context*) -> Poll<Output>;
}
| Type | Meaning |
|---|---|
Poll<T> | Poll::Ready(T) or Poll::Pending. Helpers: is_pending(), is_ready(), into_ready(). |
Context | The poll context. cx.waker() yields the Waker to register for a wake-up. |
Waker | A handle that reschedules the task. Waker::noop() is the no-op waker used by simple drivers. |
poll returns Poll::Pending only after arranging for cx.waker() to be invoked when progress becomes possible; a future that returns Pending without registering a waker will never be polled again by a real executor.
The bound is written Future<T>, which binds the associated Output positionally (see section 10.5):
function drive<F, R>(fut: F) -> R where F: Future<R> { ... }
Hand-written futures are ordinary types implementing the trait, and are indistinguishable to await from compiler-generated ones:
implement trait Future for struct Countdown {
type Output = i64;
poll(mut &this, cx: Context*) -> Poll<i64> {
if (this.n == 0) { return Poll::Ready(this.total); }
this.n = this.n - 1;
cx.waker().wake();
return Poll::Pending;
}
}
18.4 How an Async Function Is Compiled
Each async function generates one struct - the state machine - plus a Future implementation for it. The original function becomes a constructor returning that struct.
- A
statefield records which suspension point to resume at. Each poll dispatches on it and re-enters there. - Every parameter, and every local that is live across a suspension, becomes a field. A local used only between two suspensions stays an ordinary stack local.
- Every awaited sub-future is stored in a field and polled from the resuming state.
Poll::Pendingfrom a sub-future records the state and returnsPending; the next poll resumes at that state.
Two consequences are worth stating because they are guarantees, not implementation details:
- No hidden allocation. The state machine is a value type. Its size is known at compile time, and a future is only heap-allocated if you put it somewhere that allocates (spawning it as a task, for example).
- Futures are freely movable. Cryo futures are never self-referential, so no pinning discipline exists and none is needed. This is guaranteed by the restrictions in section 18.7, which reject the constructs that would create a self-reference. Moving a future between polls, including polling it by hand, is well defined.
18.5 Async Methods
async applies to methods in type struct and implement blocks, with any receiver form:
type struct Counter {
base: i64;
async bump(&this, n: i64) -> i64 {
const step: i64 = await tick();
return this.base + n + step;
}
async consume(this) -> i64 { ... }
static async make(n: i64) -> Counter { ... }
}
A &this / mut &this receiver stays borrowed for the whole operation, including across suspensions: the enclosing frame re-establishes the receiver's address before every poll, so a method may read and write through this after an await exactly as it does before one. The receiver must be a place the caller can name again - see section 18.7.
18.6 Async Trait Methods
A trait method may be async, with or without a default body:
type trait AsyncRead {
async read(mut &this, buf: Array<u8>) -> AsyncIo;
/// A default body, written once over `read`.
async read_exact(mut &this, buf: Array<u8>) -> AsyncIo {
mut io: AsyncIo = await this.read(buf);
while (io.is_short()) {
io = await this.read(io.take_buf());
}
return io;
}
}
Each implementation's async method lowers to its own state machine, so two implementors return two different concrete future types while the trait declares one signature. This is expressed with an implicit associated type: async read(...) -> AsyncIo declares an associated future type and returns it, and each implement trait block binds that associated type to its own generated future. Default bodies are instantiated per implementation and may be overridden.
A generic consumer needs only the trait bound. The future's own bound is implied by the trait and is never respelled at the use site:
async function slurp<S>(mut s: S, buf: Array<u8>) -> AsyncIo
where S: AsyncRead {
return await s.read(buf);
}
18.7 Restrictions
The rules below exist to keep futures free of self-references, which is what makes them movable and removes the need for a pinning discipline.
| Rule | Diagnostic |
|---|---|
A reference may not be held live across an await. Owned values are unrestricted. | E0455 |
| The address of a local or parameter of the current frame may not be handed to a future that outlives the current step. | E0455 |
An async method must be awaited on a receiver that names storage - a local, parameter, field, or dereference. A temporary or call result must be bound to a local first. | E0455 |
An async method's future must be awaited at the call, not stored and awaited later. | E0455 |
An async function may not await itself, directly or transitively - the state machine would contain itself and have no finite size. Rewrite the recursion as a loop. | E0600 |
There are no async fields. | E0364 |
An await, or a break/continue leaving a loop that awaits, may not stand inside an unsafe block or a static match arm (see below). | E0600 |
The last rule has a different reason: a suspend and an edge out of an awaiting loop each become a transition between states, and neither form can be split into states. An unsafe block's scope would end at the first state boundary, and a static match keeps its arm only after the body has been compiled into states. Everything else is allowed inside them, return included. The rewrite keeps the decision inside and the transition outside: bind the awaited value to a const ahead of the block, or set a flag inside it and break after it.
Each suspension runs on a fresh frame, which is why frame addresses do not survive one:
async function bad(n: i64) -> i64 {
const p: i64* = &n; // address into this frame
const step: i64 = await tick();
return *p + step; // E0455: `p` is dangling here
}
async function good(n: i64) -> i64 {
const step: i64 = await tick();
return n + step; // the owned value is carried for you
}
The owned-value rewrite is always available, and it is the idiom the standard library's async I/O follows: an operation that needs a buffer owns the buffer for the duration and hands it back on completion, rather than borrowing one from the caller's frame.
18.8 async function main
main may be declared async:
import std::future;
async function main() -> i32 {
const body: Str = await fetch("example.com");
return body.length() as i32;
}
The compiler renames the asynchronous body out of the entry-point slot and synthesises a synchronous main in its place that creates an Executor scoped to the entry point, drives the body to completion on it, and tears the runtime down at exit. Parameters are forwarded, so argument handling is identical to a synchronous main.
-> i32 and -> void are both accepted. A generic main, or any other return type, is an error (E0365).
18.9 Driving Futures
A future does nothing until something polls it. std::future provides two drivers.
block_on runs a single future to completion on the calling thread, polling in a loop with a no-op waker. It has no reactor, so it suits compute-only futures that make progress on every poll:
import std::future;
const n: i64 = future::block_on(add_later(1, 2));
Executor is the real runtime: a worker pool with a ready queue, a reactor for I/O and timer readiness, a blocking pool for work no reactor can drive, and catch_unwind isolation at the poll boundary so one panicking task cannot take down a worker.
The blocking pool exists because readiness is not universal. getaddrinfo has no non-blocking form on either platform, and a regular file is always "ready" as far as epoll is concerned, so neither can be parked on the reactor. Calling one directly inside a poll stalls a worker for its whole duration — on a single-threaded executor, the whole runtime. std::future::blocking runs such calls on threads nobody polls on and fires a Waker when they finish; dns::Resolve is the one that ships using it, so an async name lookup costs a worker nothing while it waits.
The pool creates threads only on demand and only when none is idle, so its ceiling costs nothing until the work arrives; jobs beyond it queue. Because one pool serves every offloading caller in an executor, that ceiling has to cover their combined in-flight count — a caller that holds one job while waiting on a second deadlocks against a ceiling too low to run both. The default is deliberately generous; Executor::with_threads_and_blocking overrides it.
mut ex: Executor = Executor::new();
mut h1: JoinHandle<i64> = ex.spawn(work(1));
mut h2: JoinHandle<i64> = ex.spawn(work(2));
const a: i64 = h1.join();
const b: i64 = h2.join();
| Operation | Meaning |
|---|---|
Executor::new() | Create a runtime with a default worker pool and its reactor. |
Executor::with_threads(n) | Same, with exactly n worker threads (clamped to >= 1). |
Executor::with_threads_and_blocking(n, m) | Same, and cap the blocking pool at m threads instead of the default. |
ex.spawn(fut) | Schedule fut as an independent task; returns a JoinHandle<T>. |
ex.block_on(fut) | Drive one future to completion on this executor and return its output. |
h.join() | Wait for the task and take its output. |
h.abort() | Cancel the task. |
h.detach() | Let the task run without holding the handle. Dropping a handle detaches. |
Executor implements Drop, so an executor held in a local tears its runtime down at the end of that scope; join the work you care about before then. Timer and I/O futures require an executor's reactor, so they cannot be driven by the plain block_on.
18.10 Timers and Combinators
Sleep completes after a delay, using the reactor's deadline chain rather than blocking a thread:
import std::time;
await Sleep::new(Duration::from_millis(50));
await Sleep::until(deadline);
The Futures namespace composes futures. Each takes two futures and nests for higher arities:
| Combinator | Behaviour |
|---|---|
Futures::join(a, b) | Completes when both complete; yields both outputs. |
Futures::try_join(a, b) | Completes on the first error, abandoning the sibling; otherwise both outputs. |
Futures::select(a, b) | Completes with whichever finishes first; the loser is dropped. |
Futures::timeout(dur, fut) | fut's output, or Elapsed if the deadline passes first. |
Futures::timeout_at(instant, fut) | The same against an absolute deadline. |
The duration comes first, so the deadline reads before the work it bounds:
match (await Futures::timeout(Duration::from_secs(5), fetch(url))) {
Result::Ok(body) => { ... }
Result::Err(_) => { ... } // timed out
}
18.11 Cancellation
Cancellation is a plain drop. There is no cancellation token and no separate cancel operation: dropping a future before it completes cancels the operation it represents, and the future's Drop releases whatever it holds - deregistering a waker from the reactor, disarming a timer, closing a socket it owns. This is why select cancels its loser and timeout cancels its victim simply by releasing them, and why Executor::drop cancels tasks still parked.
Because a future owns the resources of an in-flight operation, cancellation is complete by construction: there is no window in which a cancelled read is still writing into a buffer someone else now owns.
18.12 Async I/O
std::net is asynchronous. The reactor is epoll on Linux and IOCP with \Device\Afd on Windows, presented through one interface.
Async I/O futures own the handle and the buffer for the duration of the operation and hand both back on completion. That is what makes them sound rather than merely careful: a future may move between polls, so a buffer identified by a pointer into the caller's frame would be written through a stale address.
mut io: TcpIo = await TcpRead::start(stream, buf);
mut stream: TcpStream = io.take_stream();
mut buf: Array<u8> = io.take_buf();
match (io.outcome()) {
Result::Ok(n) => { ... } // n == 0 means the peer closed
Result::Err(e) => { ... }
}
| Future | Completes with |
|---|---|
TcpConnect | A connected TcpStream. |
TcpAccept | TcpAccepted - the listener back, plus the accepted connection. |
TcpRead / TcpWrite | TcpIo - the socket and buffer back, plus the byte count or error. |
TlsHandshake | A negotiated TlsIo. |
TlsRead / TlsWrite | TlsIo, with the same hand-back contract. |
The AsyncRead and AsyncWrite traits (section 18.6) abstract over transports, so protocol code is written once and runs over plain TCP or TLS:
async function greet<S>(mut conn: S) -> Result<(), IoError>
where S: AsyncWrite {
await conn.write_all(Str::new("HELLO\r\n").as_bytes().to_array());
return Result::Ok(());
}
TlsConnector::connect and TlsAcceptor::accept are async and drive a TLS handshake without blocking: each WANT_READ / WANT_WRITE becomes a reactor registration rather than a spin, and the direction to wait on is taken from the TLS error code. start_connect / start_accept stop one step short and return the TlsHandshake future itself, for a caller that needs to select over it or give it a deadline.
import a::b;
import a::b::{ X, Y };
import a::b as c;
import a::b::*;Brings a module, or selected items from it, into scope. One path per declaration: use the brace form for several items, as to alias, and * for everything public (prefer the brace form; wildcards invite name collisions).
namespace std::future;std::future: stackless async foundations — Future, Poll, Context, Waker, and the drivers that poll them.
async function name(param: Type) -> T { ... }Compiles the function into a stackless state machine. Calling it builds a future whose output is the declared return type; nothing runs until that future is polled. Allowed on free functions, methods, and trait methods.
language referencefunction name<T>(param: Type) -> Ret { ... }Declares a function. Parameters are written name: Type and are never inferred; the return type follows -> and defaults to void. Declaration order does not matter: every signature is collected before any body is checked, so functions may call each other freely and recurse.
async function add_later(a: i64, b: i64) -> i64(parameter) a: i64(parameter) b: i64const name: Type = value;An immutable binding: it cannot be reassigned after initialisation. The annotation may be omitted when an initialiser is present - the binding takes the initialiser's concrete type, inferred locally. Mutability is opt-in through mut.
const base: i64await futureSuspends the enclosing async body until the operand - which must implement Future - completes, and evaluates to its output. A prefix operator allowed anywhere an expression is (conditions, arms, guards), but only inside async code.
(parameter) a: i64return value;Leaves the current function with the given value. A function whose return type is not void must return one; a bare return; is for void. In an async function it completes the future with the value.
const base: i64(parameter) b: i64async function log_it(msg: Str) -> void(parameter) msg: Strtype struct Str {
bytes: u8*;
length: u64;
}Str: a borrowed UTF-8 string slice.
std::collections::strfunction name(count: i32, args...)A variadic parameter: the trailing bucket receives any number of further arguments with C's calling convention, so the function stays ABI-compatible with the printf family. Wrap the bucket in VaArgs to read typed values from it.
async function first<T>(a: T, b: T) -> Ttype parameter T(parameter) a: Ttype parameter T(parameter) b: Tconst NAME: Type = value;At module level, a true compile-time constant, conventionally SCREAMING_SNAKE_CASE. (mut at module level declares mutable global state instead.)
const n: i64async function add_later(a: i64, b: i64) -> i64async function drain(mut q: Queue) -> i64mut name: Type = value;A mutable binding, reassignable after declaration - mutability is opt-in, const is the default. At module level it declares mutable global state. Before & it marks an exclusive reference or receiver instead.
(parameter) mut q: Queuemut total: i64while (condition) { ... }Repeats its body while the condition holds. The condition is checked before every iteration, so the body may run zero times.
language reference(parameter) mut q: Queuemut total: i64match (subject) {
Pattern(x) => { ... }
_ => { ... }
}The primary discriminator: branches on enum variants, integers, ranges, and destructuring patterns, and the compiler rejects a match that misses a case. There is no fallthrough. As an expression it evaluates to the chosen arm's value, so every arm must produce the same type.
pattern => { body }Separates a match arm's pattern from its body. Arms do not fall through.
nif (condition) { ... } else if (condition) { ... } else { ... }Branches on a parenthesised boolean condition. Bodies are always braced; there is no single-statement form. Used as an expression, both arms are required and must produce the same type: const sign = if (n < 0) { -1 } else { 1 };.
_type struct Name { ... }
type enum Name { ... }
type trait Name { ... }
type union Name { ... }Begins a type declaration; the keyword that follows says which kind. On its own, type Name = Existing; declares an alias.
type trait Name : Base {
method(&this, other: &This) -> Ret;
}Names a set of methods a type may implement. Inside the body This is the implementing type; methods may carry default bodies, and a trait may require a base trait (type trait Ord : Eq). Generic code asks for a trait with where T: Name.
type trait Futuretype Item;
type Output = i64;An associated type on a trait: named in the trait body, filled in by each implementation, and referred to as This::Item or I::Item.
type OutputFuture::poll(mut &this, cx: Context*) -> Poll<Output>mut &this
mut &TypeAn exclusive, mutating borrow. As a receiver, mut &this lets the method modify the fields - and callers can see that from the signature alone. As a parameter type, mut &T lets the callee mutate through the reference while the caller keeps ownership.
&this
mut &this
thisThe receiver of a method. Its first parameter declares how the method takes it: &this borrows read-only, mut &this borrows exclusively, and this / mut this move the value in and consume it. In the body, this.field reaches the members.
(parameter) cx: Context*type struct Context {
waker: Waker;
}The context threaded through a poll call. Holds the Waker a future registers when it returns Pending. Passed by pointer down the poll tree, matching the stdlib convention of moving aggregates by pointer.
type enum Poll<T> {
Ready(T);
Pending;
}The result of polling a Future: either the final value, or a signal that the value is not ready yet and the task should be re-polled after its Waker fires.
type Outputfunction drive<F, R>(fut: F) -> R
where F: Future<R>type parameter Fwhere F: Future
type parameter R(parameter) fut: Ftype parameter Rwhere T: Ord + Clone, U: HashConstrains generic parameters to types that implement the listed traits, which is what makes those traits' methods callable on T. Bounds on one parameter join with +; parameters separate with ,. A where on a single method narrows the type's parameter for that method only.
type trait Futureimplement struct Name { ... }
implement trait Trait for Type { ... }
implement u64 { ... }Adds methods to a type outside its declaration. implement struct T (or enum, or a bare primitive) opens an inherent block; implement trait Tr for T supplies a trait's methods for one type. Resolution is at compile time - no dynamic dispatch.
implement trait Trait for Type { ... }Introduces a trait implementation: the block supplies, for one concrete type, the method bodies the trait requires.
language referenceimplement trait Trait for Type { ... }Names the type a trait implementation is for. The block that follows supplies the bodies the trait requires.
language referencetype struct Name {
field: Type;
method(&this) -> Ret { ... }
}A value type with named fields and optional methods. Structs live on the stack and are passed by value. Fields are public by default and may carry = default values; methods take their receiver as &this, mut &this, or this.
type struct Countdowntype Output = i64Countdown::poll(mut &this, cx: Context*) -> Poll<i64>Poll::Ready(T)(parameter) cx: Context*Context::waker(&this) -> WakerAn owned clone of the current task's waker, ready to store past this poll. Runs the waker's clone_fn (a refcount bump for an owning waker), so the caller owns the returned waker and must drop it; a plain copy of the field would alias one clone_fn under two drop_fns.
Poll::Pendingtype struct CounterCounter.base: i64Counter::bump(&this, n: i64) -> i64(parameter) n: i64const step: i64Counter.base: i64(parameter) n: i64const step: i64Counter::consume(this) -> i64static name(param: Type) -> Ret { ... }A method that belongs to the type rather than an instance, called as Type::name(..). For structs, static new(..) returning a struct literal is the idiomatic constructor.
Counter::make(n: i64) -> Countertype struct Countertype trait AsyncReadAsyncRead::read(mut &this, buf: Array<u8>) -> AsyncIo(parameter) buf: Array<u8>type struct Array<T, A = GlobalAlloc> where A: Allocator {
ptr: T*;
length: i64;
capacity: i64;
alloc: A;
}Array<T, A>: growable, heap-backed, contiguous sequence.
std::collections::arrayAsyncRead::read_exact(mut &this, buf: Array<u8>) -> AsyncIoA default body, written once over read.
mut io: AsyncIoAsyncRead::read(mut &this, buf: Array<u8>) -> AsyncIo(parameter) buf: Array<u8>mut io: AsyncIoasync function slurp<S>(mut s: S, buf: Array<u8>) -> AsyncIo
where S: AsyncReadtype parameter Swhere S: AsyncRead
(parameter) mut s: Stype trait AsyncRead(parameter) mut s: S(parameter) buf: Array<u8>async function bad(n: i64) -> i64const p: i64*(parameter) n: i64const p: i64*const step: i64async function good(n: i64) -> i64(parameter) n: i64const step: i64async function main() -> i32const body: Strconst body: StrStr::length(&this) -> u64Byte length. For a UTF-8 string, length() >= char_count.
value as TypeExplicit conversion: between numeric types, or reinterpreting one pointer type as another. Cryo never converts implicitly, and as inserts no range checks - a narrowing cast is the programmer's responsibility.
function block_on<F, R>(fut: F) -> R
where F: Future<R>Drive a single future to completion on the calling thread and return its value. The simplest possible executor: poll in a loop with a no-op waker, re-polling immediately on Pending. It has no reactor, so a future that stays Pending forever spins forever — real parking arrives with the Phase-3 executor.
mut ex: Executortype struct Executor {
inner: ExecInner*;
worker_tids: [u64];
}A multi-threaded, poll-driven task executor. Construct one, spawn futures onto it (keeping the JoinHandle to await a result, or dropping it to fire-and-forget), and block_on to drive a root future to its value. A pool of worker threads does all polling; the calling thread only ever blocks on a completion condvar.
static Executor::new() -> ExecutorCreate an executor whose pool is sized to the hardware parallelism.
std::future::executormut h1: JoinHandle<i64>type struct JoinHandle<O> {
shared: TaskShared<O>*;
exec: ExecInner*;
}A handle to a spawned task, yielding its Output via join(). Dropping a handle without join/detach detaches (the task runs on, its result discarded). abort() requests cancellation.
mut ex: ExecutorExecutor::spawn<F, O>(mut &this, fut: F) -> JoinHandle<O>
where F: Future<O>Spawn fut and return a handle to its Output. Workers begin driving it immediately.
mut h2: JoinHandle<i64>const a: i64mut h1: JoinHandle<i64>JoinHandle::join(mut this) -> Result<O, JoinError>Block on the completion condvar until a worker finishes this task, then return its Output (or Err if it was aborted / could not complete / panicked). Consumes the handle and frees the block.
const b: i64mut h2: JoinHandle<i64>type struct Sleep {
deadline: i64;
id: u64;
rt: Reactor*;
}A future that completes once the monotonic clock reaches its deadline.
std::future::timerstatic Sleep::new(dur: Duration) -> SleepComplete once dur has elapsed from now. A zero (or already-elapsed) duration completes on the first poll without touching the reactor.
type struct Duration {
secs: u64;
nanos: u32;
}A length of time: whole secs plus a sub-second nanos remainder (0 <= nanos < 1_000_000_000). POD; copy-by-value, no drop.
static Duration::from_millis(millis: u64) -> Durationstatic Sleep::until(deadline: Instant) -> SleepComplete at deadline on the monotonic clock. A deadline already in the past completes on the first poll.
static Futures::timeout<F, O>(dur: Duration, fut: F) -> Timeout<F, O>
where F: Future<O>Run fut with a deadline dur from now.
static Duration::from_secs(secs: u64) -> DurationResult::Ok(T)body: FResult::Err(E)mut io: TcpIotype struct TcpIo {
stream: TcpStream;
buf: Array<u8>;
result: Result<u64, IoError>;
}The outcome of an async read/write: the socket handed back, plus how many bytes moved (or why none did).
type struct TcpRead {
stream: TcpStream;
buf: Array<u8>;
rt: Reactor*;
}Read as soon as the socket has anything to give. Completes with the byte count — 0 means the peer closed its end — or the error that ended it.
std::net::socket::tcpstatic TcpRead::start(stream: TcpStream, buf: Array<u8>) -> TcpReadBegin a read on stream, filling buf — both owned by this future until it completes.
mut stream: TcpStreamtype struct TcpStream {
fd: i32;
}mut io: TcpIoTcpIo::take_stream(mut &this) -> TcpStreamTake the socket back. Calling it twice yields a closed stream the second time — the outcome holds the socket exactly once.
std::net::socket::tcpmut buf: Array<u8>TcpIo::take_buf(mut &this) -> Array<u8>Take the buffer back. After a read it holds exactly the bytes that arrived; after a write it is untouched, so a caller with a short write still has the bytes it has not sent yet. Calling it twice yields an empty buffer the second time.
std::net::socket::tcpTcpIo::outcome(&this) -> Result<u64, IoError>How many bytes moved, or why none did.
std::net::socket::tcpn: u64e: IoErrorasync function greet<S>(mut conn: S) -> Result<(), IoError>
where S: AsyncWritetype parameter Swhere S: AsyncWrite
(parameter) mut conn: Sprimitive ()The unit type: a value that carries no information, and the empty tuple. Distinct from void, which is the absence of a value - Result<(), E> succeeds with nothing to return. 0 bytes.
type struct IoError {
kind: IoErrorKind;
os_code: i32;
}type trait AsyncWrite {
pending(mut &this) -> Array<u8>*;
async flush(mut &this) -> Result<(), IoError>;
queue<T>(mut &this, data: T) -> Result<(), IoError>;
async send<T>(mut &this, data: T) -> Result<(), IoError>;
}A byte sink that buffers what it is given until told to send it.
std::io::traits(parameter) mut conn: Sstatic Str::new(text: string) -> StrConstruct from a NUL-terminated C-style string literal. The caller certifies that text is valid UTF-8 and outlives this view. The NUL terminator is not included in the view.
Str::as_bytes(&this) -> Slice<u8>Underlying byte view. Use this for byte-granularity algorithms (hashing, equality, substring search).
std::collections::strtype struct Waker {
data: u8*;
}A handle a stalled future stores so it can be re-scheduled: wake_fn(data) tells the executor "poll my task again". This is the manual-vtable shape used throughout the runtime — non-capturing function pointers plus an opaque data pointer identifying the task.
static Waker::noop() -> WakerA waker that does nothing when woken. Used by drivers (like block_on) that re-poll unconditionally and never park a task.
type struct Output {
status: ExitStatus;
stdout: Array<u8>;
stderr: Array<u8>;
}function read(fd: i32, buffer: u8*, count: u64) -> i64POSIX-shaped read over CRT _read. The count is clamped to the CRT's INT_MAX request ceiling - a short read is legal under the POSIX contract, so callers loop as they already must.
break;Exits the innermost enclosing loop immediately - for, while, loop, or do. Inside for (x in ..) it leaves the loop the compiler synthesised around the iterator.
continue;Skips the rest of the current iteration: the innermost loop's condition (or the iterator's next()) runs again.
unsafe { ... }A documentation marker for code that does raw pointer arithmetic or crosses into extern calls. It lowers to a plain block: no check is relaxed inside it and none is imposed outside it - and 1.0 commits to keeping it that way.
static match (T) { ... }Opens a compile-time match on a type parameter: the one arm whose type matches is kept during monomorphisation and the rest are discarded.
static match (T) {
u8 | u16 => { ... }
_ => { ... }
}Selects a body by inspecting a type parameter at compile time. The subject is a type and each arm is a type (| groups several; _ takes the rest). During monomorphisation the one matching arm is kept and the others discarded, so there is no runtime branch.
async function main() -> i32namespace std::future::blocking;std::future::blocking: an off-executor home for work that cannot be polled.
module pagestatic Executor::with_threads_and_blocking(n: u32, blocking: u32) -> ExecutorCreate an executor with n worker threads and a blocking pool that will grow to at most blocking threads.
static Executor::with_threads(n: u32) -> ExecutorCreate an executor with exactly n worker threads (clamped to >= 1 so the pool always makes progress) and the default blocking-pool ceiling.
const n: i64type struct JoinHandle<O> {
shared: TaskShared<O>*;
exec: ExecInner*;
}JoinHandle is also declared in std::thread
A handle to a spawned task, yielding its Output via join(). Dropping a handle without join/detach detaches (the task runs on, its result discarded). abort() requests cancellation.
static Futures::join<A, B, OA, OB>(a: A, b: B) -> Join<A, B, OA, OB>
where A: Future<OA>, B: Future<OB>Run a and b concurrently; complete with both values.
const a: i64const b: i64static Futures::try_join<A, B, T1, T2, E>(a: A, b: B) -> TryJoin<A, B, T1, T2, E>
where A: Future<Result<T1, E>>, B: Future<Result<T2, E>>Run a and b concurrently; complete with both values, or with the first error either reports, dropping the sibling.
static Futures::select<A, B, OA, OB>(a: A, b: B) -> Select<A, B, OA, OB>
where A: Future<OA>, B: Future<OB>Run a and b concurrently; complete as soon as either does, dropping the other.
type struct Elapsed {
deadline: i64;
}A Timeout expired before the operation it was timing finished.
static Futures::timeout_at<F, O>(deadline: Instant, fut: F) -> Timeout<F, O>
where F: Future<O>Run fut with an absolute deadline.
type struct TcpConnect {
fd: i32;
addr: [u8;
28];
addrlen: u32;
err: IoError;
rt: Reactor*;
}Connect to a remote address without blocking. The socket is opened and the handshake started when the future is created; each poll asks the OS whether it finished, parking on writability in between. The socket is closed if the future is dropped before it completes.
std::net::socket::tcptype struct TcpAccept {
listener: TcpListener;
rt: Reactor*;
}Accept the next incoming connection. An accepted socket does not inherit the listener's non-blocking mode, so it is set here — it is about to be used with these same futures. Owns the listener for the duration and hands it back in the TcpAccepted, so a server loop starts the next accept from that.
type struct TcpAccepted {
listener: TcpListener;
result: Result<TcpStream, IoError>;
}The outcome of an async accept: the listener handed back, plus the connection it produced. Both come out through their take_* methods, for the same reason as TcpIo::take_stream.
type struct TcpWrite {
stream: TcpStream;
buf: Array<u8>;
rt: Reactor*;
}Write as soon as the socket has room. Completes with the number of bytes accepted, which may be fewer than asked for — a caller with more to send starts another write for the remainder, which is why the buffer comes back untouched. Owns the socket and the buffer exactly as TcpRead does, and for the same reason.
type struct TlsHandshake {
tls: TlsStream;
is_client: boolean;
verify: boolean;
rt: Reactor*;
armed: u32;
}Drive a TLS handshake to completion without blocking. SSL_connect (client) or SSL_accept (server) is re-issued on every poll — which is exactly what OpenSSL wants after a WANT_* — until it reports success or a real failure.
type struct TlsIo {
tls: TlsStream;
buf: Array<u8>;
result: Result<u64, IoError>;
}The outcome of an async TLS read or write: the stream and the buffer handed back, plus how many bytes moved (or why none did).
std::net::tls::futuretype struct TlsRead {
tls: TlsStream;
buf: Array<u8>;
rt: Reactor*;
armed: u32;
}Read decrypted bytes as soon as the connection has some. Completes with the byte count — 0 means the peer sent close_notify, the TLS equivalent of EOF — or the error that ended it.
type struct TlsWrite {
tls: TlsStream;
buf: Array<u8>;
rt: Reactor*;
armed: u32;
}Write plaintext as soon as the connection has room for the resulting record. Completes with the number of bytes accepted, which may be fewer than asked for — a caller with more to send starts another write for the remainder, which is why the buffer comes back untouched.
std::net::tls::futuretype struct TlsConnector {
ctx: void*;
verify: boolean;
}async TlsConnector::connect(&this, stream: TcpStream, hostname: Str) -> Result<TlsStream, IoError>Wrap an established TcpStream in TLS, performing the client handshake with SNI set to hostname (also the name verified against the peer certificate when verification is on). Consumes the TcpStream; on failure it is closed before returning.
type struct TlsAcceptor {
ctx: void*;
}async TlsAcceptor::accept(&this, stream: TcpStream) -> Result<TlsStream, IoError>Complete the server handshake over an accepted TcpStream. Consumes it; closes it on failure.