axcherednikov / eventloop
Native PHP extension providing a high-performance event loop with epoll/kqueue/poll support. API-compatible with Revolt.
Package info
github.com/axcherednikov/php-eventloop
Language:C
Type:php-ext
Ext name:ext-eventloop
pkg:composer/axcherednikov/eventloop
Requires
- php: >=8.1
This package is auto-updated.
Last update: 2026-05-23 16:13:59 UTC
README
A native PHP extension that brings a high-performance event loop directly into the engine. Inspired by and API-compatible with Revolt -- not a replacement, but a native alternative written in C for zero-overhead async I/O.
Why? Revolt is an excellent userland library. This extension takes the same proven API design and moves it into a PHP extension, eliminating userland dispatch overhead and leveraging OS-level I/O primitives (epoll, kqueue, poll) directly from C.
Key Differences from Revolt
| Revolt | ext-eventloop | |
|---|---|---|
| Implementation | PHP userland | C extension |
| Installation | composer require revolt/event-loop |
phpize && make install |
| I/O backend | Configurable (ev, event, uv) | Auto-detected (epoll / kqueue / poll / select) |
| Fiber suspension | Yes | Yes |
| API contract | Revolt\EventLoop::* |
EventLoop\EventLoop::* |
The API surface mirrors Revolt's, so migrating between the two is straightforward -- adjust the namespace and you're done.
Requirements
- PHP >= 8.1 (Fiber support required)
- A POSIX-compatible OS (Linux, macOS, FreeBSD, etc.)
Installation
Via PIE (recommended)
pie install axcherednikov/eventloop
From source
git clone https://github.com/axcherednikov/php-eventloop.git cd php-eventloop phpize ./configure --enable-eventloop make make test sudo make install
Then enable the extension:
; php.ini or conf.d/eventloop.ini extension=eventloop
Verify:
php -m | grep eventloop
Quick Start
<?php use EventLoop\EventLoop; // Defer a callback to the next loop tick EventLoop::defer(function (string $callbackId) { echo "Deferred callback executed\n"; }); // Delay execution by 1.5 seconds EventLoop::delay(1.5, function (string $callbackId) { echo "This runs after 1.5 seconds\n"; }); // Repeat every 500ms $id = EventLoop::repeat(0.5, function (string $callbackId) { echo "Tick\n"; }); // Cancel the repeater after 3 seconds EventLoop::delay(3, function () use ($id) { EventLoop::cancel($id); }); EventLoop::run();
API Reference
All methods are static on EventLoop\EventLoop.
Scheduling
| Method | Description |
|---|---|
queue(Closure $closure, mixed ...$args): void |
Queue a microtask for immediate execution |
defer(Closure $closure): string |
Defer to the next event loop iteration |
delay(float $delay, Closure $closure): string |
Execute after $delay seconds |
repeat(float $interval, Closure $closure): string |
Execute every $interval seconds |
I/O Watchers
| Method | Description |
|---|---|
onReadable(resource $stream, Closure $closure): string |
Execute when a stream becomes readable |
onWritable(resource $stream, Closure $closure): string |
Execute when a stream becomes writable |
Signal Handling
| Method | Description |
|---|---|
onSignal(int $signal, Closure $closure): string |
Execute when a signal is received |
Callback Management
| Method | Description |
|---|---|
enable(string $id): string |
Enable a disabled callback |
disable(string $id): string |
Disable a callback (can be re-enabled) |
cancel(string $id): void |
Permanently cancel a callback |
reference(string $id): string |
Reference a callback (keeps the loop alive) |
unreference(string $id): string |
Unreference a callback |
isEnabled(string $id): bool |
Check if a callback is enabled |
isReferenced(string $id): bool |
Check if a callback is referenced |
getType(string $id): CallbackType |
Get the callback type |
getIdentifiers(): array |
Get all registered callback IDs |
Loop Control
| Method | Description |
|---|---|
run(): void |
Run the event loop |
stop(): void |
Stop the event loop |
isRunning(): bool |
Check if the loop is running |
getDriver(): string |
Get the active I/O driver name |
Error Handling
| Method | Description |
|---|---|
setErrorHandler(?Closure $handler): void |
Set the error handler for exceptions in callbacks |
getErrorHandler(): ?Closure |
Get the current error handler |
Fiber Suspension
$fiber = new Fiber(function () { $suspension = EventLoop::getSuspension(); EventLoop::defer(function () use ($suspension) { $suspension->resume('hello'); }); $value = $suspension->suspend(); // "hello" echo $value; // "hello" }); $fiber->start(); EventLoop::run();
| Method | Description |
|---|---|
Suspension::suspend(): mixed |
Suspend the current fiber |
Suspension::resume(mixed $value = null): void |
Resume with a value |
Suspension::throw(Throwable $e): void |
Resume by throwing an exception |
I/O Drivers
The extension automatically selects the best I/O driver available on your system at compile time. There is no manual configuration needed -- you always get optimal performance for your platform.
| Driver | Platforms | Scalability | Notes |
|---|---|---|---|
| epoll | Linux 2.6+ | O(1) | Kernel tracks descriptors; returns only ready ones |
| kqueue | macOS, FreeBSD, OpenBSD | O(1) | Same principle as epoll, native to BSD systems |
| poll | Any POSIX | O(n) | No descriptor limit, but scans all on every call |
| select | Universal (fallback) | O(n) | Oldest API, limited to ~1024 descriptors |
Selection priority: epoll > kqueue > poll > select. The first one that compiles and initializes successfully wins.
In practice this means:
- Linux servers (the most common deployment) get epoll -- handles thousands of connections with near-zero overhead
- macOS (local development) gets kqueue -- equally efficient
- Older or exotic systems gracefully fall back to poll or select
Check which driver is active:
echo EventLoop::getDriver(); // "epoll" on Linux, "kqueue" on macOS
Benchmarks
Environment: PHP 8.5.4, Apple M1 Max, macOS, 100,000 iterations (average of 3 runs). Revolt v1.0.8 tested with all four available drivers (StreamSelect, Ev, Event, UV). ext-eventloop using kqueue driver.
| Benchmark | Revolt StreamSelect | Revolt Ev | Revolt Event | Revolt UV | ext-eventloop |
|---|---|---|---|---|---|
defer() |
765,598 ops/sec | 743,057 ops/sec | 733,417 ops/sec | 742,926 ops/sec | 3,658,412 ops/sec |
delay(0) |
242,383 ops/sec | 486,501 ops/sec | 464,196 ops/sec | 183,585 ops/sec | 3,047,625 ops/sec |
repeat(0) |
687,196 ops/sec | 707,010 ops/sec | 74,708 ops/sec | 73,491 ops/sec | 17,371,529 ops/sec |
| I/O register + cancel | 2,114,361 ops/sec | 2,003,474 ops/sec | 1,980,326 ops/sec | 1,953,935 ops/sec | 7,193,404 ops/sec |
| Fiber suspend/resume | 221,383 ops/sec | 220,659 ops/sec | 218,318 ops/sec | 220,244 ops/sec | 243,242 ops/sec |
These benchmarks measure callback dispatch and scheduling throughput. The I/O backend (StreamSelect, Ev, Event, UV) primarily affects polling efficiency at high concurrency, not dispatch speed — that's why all four Revolt drivers show similar numbers here. ext-eventloop moves the entire dispatch path into C, which is where the difference comes from. Fiber performance is nearly identical because
suspend()/resume()is handled by the Zend Engine directly.
Migrating from Revolt
The API contract is intentionally compatible. In most cases, a namespace swap is all you need:
- use Revolt\EventLoop; + use EventLoop\EventLoop;
If you use Revolt\EventLoop\Suspension:
- use Revolt\EventLoop\Suspension; + use EventLoop\Suspension;
Testing
make test
The extension ships with 37 .phpt tests covering defer, delay, repeat, I/O watchers, signals, suspensions, error handling, and edge cases.
Changelog
Release notes are available in CHANGELOG.md.
Acknowledgements
This project is built on the ideas and API design of Revolt by Aaron Piotrowski, Niklas Keller, and contributors. Revolt's clean, well-thought-out API made it the natural foundation for a native implementation. Full credit to the Revolt team for defining the contract that this extension follows.
License
Licensed under the MIT License.