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-03-25 22:45:08 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. Revolt v1.0.8 with StreamSelectDriver (default, no ext-ev/ext-uv). ext-eventloop using kqueue driver.
| Benchmark | Revolt | ext-eventloop | Speedup |
|---|---|---|---|
defer() dispatch |
715,053 ops/sec | 3,712,263 ops/sec | 5.2x |
delay(0) dispatch |
234,229 ops/sec | 2,832,500 ops/sec | 12.1x |
repeat() dispatch |
679,452 ops/sec | 17,794,516 ops/sec | 26.2x |
| I/O register + cancel | 2,109,267 ops/sec | 7,635,677 ops/sec | 3.6x |
| Fiber suspend/resume | 221,776 ops/sec | 248,738 ops/sec | 1.1x |
Note: Revolt was tested with its default StreamSelectDriver. With ext-ev or ext-uv backends, Revolt's I/O performance would be higher, though callback dispatch overhead remains in PHP userland. Fiber performance is nearly identical because
suspend()/resume()is handled by the Zend Engine in both cases.
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 26 .phpt tests covering defer, delay, repeat, I/O watchers, signals, suspensions, error handling, and edge cases.
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.