Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Native effects on worker threads

Worker certification asks every native operation a body can reach for its effect. The compiler classifies each primitive in one exhaustive table (src/effects.rs), so a new primitive cannot be added without stating its effect. Standard modules have no exemption: a std helper is certified by the same walk as user code, and a foreign call is still rejected.

EffectOperationsOn a worker
ComputeText, collections, bytes, hashing, compression, UnicodeAllowed
ClockMonotonic and wall clocks, sleepAllowed
Entropystd.secrets readsAllowed
Process streamStandard input, output and error, print, argsAllowed; no ordering between threads
Pathopen, read_text, metadata, read_dir, std.fs.root, std.fs.symlink, std.fs.canonicalizeAllowed; a relative path resolves against the working directory at the call
HandleMethods of File, ReadDir and std.fs.Root, rooted mutation, std.ready waits and selectors, operations on an open socketAllowed under the loan rules below
Environmentstd.env.get, vars, current_dirAllowed; reads only
Childstd.process spawn, wait, poll and signalAllowed; the runtime’s child table is synchronized
NetworkOpening a std.net socket, name lookupAllowed; the kernel and C library synchronize sockets and the resolver
Process globalstd.env.set_current_dirRejected
Signal dispositionstd.signal.listenRejected

Changing the working directory would change how every other thread resolves a relative path, so a worker body, helper, callback or destructor that reaches set_current_dir is rejected:

error: worker `worker` reaches `std.env.set_current_dir`: changing the working directory affects every thread's relative paths; change it on the main thread before starting workers

Catching termination signals changes how every thread’s process ends, so std.signal.listen is rejected on a worker the same way. A worker may wait on the Signals handle the main thread opened. Nothing in the language writes the environment.

When the rejected function is reached through others, the diagnostic lists them from the worker outward. A rejected type in a called function’s signature or locals names that function and its path the same way. A generic function is named by the location of its definition, and a method as Class.method:

error: worker `work` reaches `std.env.set_current_dir` through `outer` -> `Parser.parse` -> the generic function at std/error.plenty:7:5 -> `wrap`: changing the working directory affects every thread's relative paths; change it on the main thread before starting workers

Thin callable targets

A Callable stores a code address with a concrete signature. The compiler collects every address of that signature in the closed program and certifies all of their bodies, signature types, stored values and destructors. This allows named and capture-free function values, including payload variant constructors, in worker helpers such as result.map_error(Domain.Allocation). Moving a callback through parameters, returned values, classes, lists or channels preserves this check. Recursive signatures and bodies are inspected once per identity.

This is conservative certification by signature. Taking an unsafe address on the main thread can reject a worker’s use of a different function with the same signature, even when that unsafe value never enters the worker. Standard library callbacks share this candidate set: an unsafe user callback with the same signature as a std error mapper can prevent a std worker from certifying. A conditional with one safe and one unsafe candidate is rejected too. An absent address set supplies no evidence and is rejected; a function signature alone is never an effect contract. A concrete captured closure has its own body and capture evidence and can avoid unrelated signatures, while retaining all checks on its actual effects.

Foreign interfaces cannot produce or reinterpret a Callable: their scalar and opaque-pointer ABI is separate from Plenty function values. Erased BoxedCallable environments remain ineligible. The direct worker-entry APIs keep their existing requirements for statically named functions or concrete closures; certification of thin values used inside workers does not add a new spawn or parallel-worker entry form.

Native handles

File, directory streams (ReadDir), entries and metadata cross thread boundaries like any other owned value:

  • An owned handle can move into a worker, a channel or an executor job. The receiving thread closes it, explicitly or when its owner drops.
  • A scoped worker can borrow a handle. Reading, writing, seeking and closing need an exclusive loan, so the parent cannot use a handle a worker holds as &mut File, and two workers cannot hold it mutably at once.
  • A shared loan (&File) permits only operations that do not move the cursor or change the handle: the closed property, readable, writable, rooted openat operations, and std.ready waits. Several workers may wait on one shared handle.
  • A failed thread start drops each moved handle once, which closes it.

Foreign pointers (opaque types) remain ineligible: the compiler cannot know whether a foreign resource tolerates use from another thread.

Blocking and cancellation

Blocking calls stay ordinary function calls; there is no async syntax. A worker that must not block indefinitely waits with a deadline through std.ready, which makes one bounded ppoll per step on the handle and the cancellation token’s wake descriptor, so cancelling the token wakes the wait at once. A deadline that passes leaves the handle and any borrowed buffer intact; the caller decides whether to retry, close or return. A token still does not interrupt an unbounded read, write, recv or child wait.

Resource use

Each worker is one native thread, started explicitly by spawn, an executor or a parallel operation, so a program’s thread count is what it asks for. No facility starts a thread per handle or per connection; a std.ready selector lets one worker wait on many. An idle std.ready waiter does not wake until its handle is ready, its deadline passes or its token is cancelled. std.net.resolve is the exception: each lookup runs on a short-lived helper thread, at most 32 at once. Descriptors are released when their owner closes or drops them, on whichever thread that is.

Supported target: x86_64 Linux GNU. Elsewhere std.ready waits report IoError.Unsupported.