← All docs

Fibers

Cooperative coroutines (PHP 8.1+ Fiber): create, start, suspend, resume, with FiberError.

A Fiber is a cooperative coroutine: a callable that owns its own call stack and can suspend execution at arbitrary depth. Control alternates explicitly between the caller and the fiber — there is no preemption.

elephc supports Fibers on the supported native target matrix: macOS ARM64, Linux ARM64, and Linux x86_64.

Class summary

MethodSignatureBehavior
__construct__construct(callable $callback)Allocate a fiber and capture the callable. The callable runs the first time start() is called.
startstart(mixed $arg0 = null, ..., mixed $arg6 = null): mixedSwitch into the fiber and run it until it suspends or returns. Up to seven arguments are forwarded to the callback. Returns the value yielded via Fiber::suspend(), or null if the fiber terminates before suspending.
resumeresume(mixed $value = null): mixedDeliver a value to the fiber’s pending Fiber::suspend() call and continue execution. Returns the next yielded value, or null if the fiber terminates.
throwthrow(Throwable $exception): mixedRe-raise an exception inside the fiber at its pending suspend point. The exception unwinds the fiber’s local try/catch chain.
getReturngetReturn(): mixedRead the value the fiber returned after termination. Raises FiberError if called before the fiber terminates.
isStartedisStarted(): boolTrue once start() has been called.
isSuspendedisSuspended(): boolTrue while the fiber is paused at a Fiber::suspend() call.
isRunningisRunning(): boolTrue while the fiber is currently executing.
isTerminatedisTerminated(): boolTrue after the fiber’s callable has returned.
Fiber::suspendstatic suspend(mixed $value = null): mixed(Called from inside a fiber.) Yield the value to the resumer and pause; resumes with the value the next resume() delivers.
Fiber::getCurrentstatic getCurrent(): ?FiberThe currently executing fiber, or null when called from the main thread. Internally this is represented as a boxed mixed value.

FiberError is modeled as an Error subclass, matching PHP. catch (FiberError $e), catch (Error $e), and catch (Throwable $e) all apply; catch (Exception $e) does not.

Lifecycle states

A fiber moves through four states in order, never going backwards:

StateWhen
NotStartedJust constructed; start() has not been called yet.
RunningCurrently executing. Fiber::getCurrent() returns this fiber.
SuspendedPaused inside Fiber::suspend(), waiting for a resume() from the caller.
TerminatedThe callable has returned. start()/resume() are no longer valid on it.

Example

<?php
$counter = new Fiber(function(): void {
    $a = Fiber::suspend("one");
    echo "resumed with " . $a;
    $b = Fiber::suspend("two");
    echo "resumed with " . $b;
    Fiber::suspend("three");
});

echo $counter->start();         // one
echo "|";
echo $counter->resume("alpha"); // resumed with alpha two
echo "|";
echo $counter->resume("beta");  // resumed with beta three

Output:

one|resumed with alpha two|resumed with beta three

Argument and capture transport

Fiber calls cross a stack boundary. Before switching stacks, elephc copies the values needed by the callback into fixed fields on the Fiber object:

  • start_args[0..6] stores up to seven boxed mixed values passed to $fiber->start(...)
  • float_args[0..6] stores raw float captures
  • user_arg_max records how many leading start_args slots may be overwritten by start()

This copy is sometimes called “spilling”: the caller’s argument registers or stack-passed overflow arguments are saved into stable Fiber-owned storage before __rt_fiber_switch adopts the Fiber’s separate stack.

Closures with use (...) captures are evaluated when the Fiber is constructed, not when it starts. Captured int-like values, objects, arrays, callables, and mixed values use one integer slot; captured strings use two integer slots (ptr + len); captured floats use one float slot. Heap-backed captures are retained when the Fiber is constructed and released when the Fiber object is freed.

Runtime model

Each Fiber owns a native stack allocated with mmap. The bottom 16 KiB is protected with mprotect(PROT_NONE) as a guard page, and the usable stack defaults to 256 KiB. When the Fiber object is freed, the mapped stack is returned to the OS with munmap.

The context switch is cooperative:

  • AArch64 saves x19-x28, x29-x30, and d8-d15
  • x86_64 SysV saves rbx, rbp, and r12-r15; the saved return address resumes through the normal ret path
  • _fiber_current, the exception-handler chain, and the cleanup call-frame chain are swapped with the target context

The first switch into a Fiber returns into __rt_fiber_entry, a trampoline on the fresh stack. The trampoline installs a sentinel exception handler, calls a generated ABI wrapper for the callback, stores the terminal return value, marks the Fiber Terminated, and switches back to the caller.

Limitations

These are current implementation limits, not PHP design rules:

LimitationNotes
start() is fixed-arityFiber::start() has seven optional mixed parameters. Calls with more than seven values are rejected, and a callback with more than seven visible start parameters is rejected. This is not true PHP variadic forwarding.
Variadic callback parameters are not supportedFiber callbacks such as function (...$args): void {} are rejected because the runtime currently forwards fixed start_args slots instead of building a PHP variadic array.
Capture storage is fixed-sizeCaptures share a fixed slot budget with the callback ABI: seven integer slots and seven float slots. Strings consume two integer slots. Capture overflow is a compile-time error.
Callback arguments cannot be by-referenceFiber callbacks such as function (&$value): void {} are rejected because start arguments are boxed and stored before the stack switch.
Callback targets must be statically knownnew Fiber(...) accepts closures, variables holding known closures/callables, and known first-class callables. Arbitrary runtime callable values, such as unknown strings or dynamically computed callbacks, are rejected.
Mixed arithmetic still needs explicit castsValues transferred by start(), resume(), Fiber::suspend(), and getReturn() are boxed mixed cells. Echo, comparison, gettype(), instanceof, and typed callback parameters handle them, but arithmetic on an untyped value received from Fiber::suspend() may not auto-unbox. Cast explicitly before computing, for example (int)$value + 10.
Fiber::getCurrent() has imprecise internal typingPHP exposes ?Fiber; elephc currently represents the result as boxed mixed internally. Runtime checks such as instanceof Fiber work, but type inference is less precise than PHP’s signature.
Stack size is fixedEach Fiber gets a 256 KiB usable stack plus a 16 KiB guard page. There is no user-facing stack-size configuration. Stack overflow faults through the guard page rather than raising a catchable PHP exception.
Cooperative onlyFibers do not provide parallel execution, preemption, timers, or an event loop. Scheduling is entirely explicit through start(), resume(), suspend(), and throw().