|
| 1 | +# ext-eventloop |
| 2 | + |
| 3 | +A native PHP extension that brings a high-performance event loop directly into the engine. Inspired by and API-compatible with [Revolt](https://github.com/revoltphp/event-loop) -- not a replacement, but a **native alternative** written in C for zero-overhead async I/O. |
| 4 | + |
| 5 | +> **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. |
| 6 | +
|
| 7 | +## Key Differences from Revolt |
| 8 | + |
| 9 | +| | Revolt | ext-eventloop | |
| 10 | +|---|---|---| |
| 11 | +| Implementation | PHP userland | C extension | |
| 12 | +| Installation | `composer require revolt/event-loop` | `phpize && make install` | |
| 13 | +| I/O backend | Configurable (ev, event, uv) | Auto-detected (epoll / kqueue / poll / select) | |
| 14 | +| Fiber suspension | Yes | Yes | |
| 15 | +| API contract | `Revolt\EventLoop::*` | `EventLoop\EventLoop::*` | |
| 16 | + |
| 17 | +The API surface mirrors Revolt's, so migrating between the two is straightforward -- adjust the namespace and you're done. |
| 18 | + |
| 19 | +## Requirements |
| 20 | + |
| 21 | +- PHP >= 8.1 (Fiber support required) |
| 22 | +- A POSIX-compatible OS (Linux, macOS, FreeBSD, etc.) |
| 23 | + |
| 24 | +## Installation |
| 25 | + |
| 26 | +```bash |
| 27 | +git clone https://github.com/axcherednikov/php-eventloop.git |
| 28 | +cd php-eventloop |
| 29 | + |
| 30 | +phpize |
| 31 | +./configure --enable-eventloop |
| 32 | +make |
| 33 | +make test |
| 34 | +sudo make install |
| 35 | +``` |
| 36 | + |
| 37 | +Then enable the extension: |
| 38 | + |
| 39 | +```ini |
| 40 | +; php.ini or conf.d/eventloop.ini |
| 41 | +extension=eventloop |
| 42 | +``` |
| 43 | + |
| 44 | +Verify: |
| 45 | + |
| 46 | +```bash |
| 47 | +php -m | grep eventloop |
| 48 | +``` |
| 49 | + |
| 50 | +## Quick Start |
| 51 | + |
| 52 | +```php |
| 53 | +<?php |
| 54 | + |
| 55 | +use EventLoop\EventLoop; |
| 56 | + |
| 57 | +// Defer a callback to the next loop tick |
| 58 | +EventLoop::defer(function (string $callbackId) { |
| 59 | + echo "Deferred callback executed\n"; |
| 60 | +}); |
| 61 | + |
| 62 | +// Delay execution by 1.5 seconds |
| 63 | +EventLoop::delay(1.5, function (string $callbackId) { |
| 64 | + echo "This runs after 1.5 seconds\n"; |
| 65 | +}); |
| 66 | + |
| 67 | +// Repeat every 500ms |
| 68 | +$id = EventLoop::repeat(0.5, function (string $callbackId) { |
| 69 | + echo "Tick\n"; |
| 70 | +}); |
| 71 | + |
| 72 | +// Cancel the repeater after 3 seconds |
| 73 | +EventLoop::delay(3, function () use ($id) { |
| 74 | + EventLoop::cancel($id); |
| 75 | +}); |
| 76 | + |
| 77 | +EventLoop::run(); |
| 78 | +``` |
| 79 | + |
| 80 | +## API Reference |
| 81 | + |
| 82 | +All methods are static on `EventLoop\EventLoop`. |
| 83 | + |
| 84 | +### Scheduling |
| 85 | + |
| 86 | +| Method | Description | |
| 87 | +|---|---| |
| 88 | +| `queue(Closure $closure, mixed ...$args): void` | Queue a microtask for immediate execution | |
| 89 | +| `defer(Closure $closure): string` | Defer to the next event loop iteration | |
| 90 | +| `delay(float $delay, Closure $closure): string` | Execute after `$delay` seconds | |
| 91 | +| `repeat(float $interval, Closure $closure): string` | Execute every `$interval` seconds | |
| 92 | + |
| 93 | +### I/O Watchers |
| 94 | + |
| 95 | +| Method | Description | |
| 96 | +|---|---| |
| 97 | +| `onReadable(resource $stream, Closure $closure): string` | Execute when a stream becomes readable | |
| 98 | +| `onWritable(resource $stream, Closure $closure): string` | Execute when a stream becomes writable | |
| 99 | + |
| 100 | +### Signal Handling |
| 101 | + |
| 102 | +| Method | Description | |
| 103 | +|---|---| |
| 104 | +| `onSignal(int $signal, Closure $closure): string` | Execute when a signal is received | |
| 105 | + |
| 106 | +### Callback Management |
| 107 | + |
| 108 | +| Method | Description | |
| 109 | +|---|---| |
| 110 | +| `enable(string $id): string` | Enable a disabled callback | |
| 111 | +| `disable(string $id): string` | Disable a callback (can be re-enabled) | |
| 112 | +| `cancel(string $id): void` | Permanently cancel a callback | |
| 113 | +| `reference(string $id): string` | Reference a callback (keeps the loop alive) | |
| 114 | +| `unreference(string $id): string` | Unreference a callback | |
| 115 | +| `isEnabled(string $id): bool` | Check if a callback is enabled | |
| 116 | +| `isReferenced(string $id): bool` | Check if a callback is referenced | |
| 117 | +| `getType(string $id): CallbackType` | Get the callback type | |
| 118 | +| `getIdentifiers(): array` | Get all registered callback IDs | |
| 119 | + |
| 120 | +### Loop Control |
| 121 | + |
| 122 | +| Method | Description | |
| 123 | +|---|---| |
| 124 | +| `run(): void` | Run the event loop | |
| 125 | +| `stop(): void` | Stop the event loop | |
| 126 | +| `isRunning(): bool` | Check if the loop is running | |
| 127 | +| `getDriver(): string` | Get the active I/O driver name | |
| 128 | + |
| 129 | +### Error Handling |
| 130 | + |
| 131 | +| Method | Description | |
| 132 | +|---|---| |
| 133 | +| `setErrorHandler(?Closure $handler): void` | Set the error handler for exceptions in callbacks | |
| 134 | +| `getErrorHandler(): ?Closure` | Get the current error handler | |
| 135 | + |
| 136 | +### Fiber Suspension |
| 137 | + |
| 138 | +```php |
| 139 | +$fiber = new Fiber(function () { |
| 140 | + $suspension = EventLoop::getSuspension(); |
| 141 | + |
| 142 | + EventLoop::defer(function () use ($suspension) { |
| 143 | + $suspension->resume('hello'); |
| 144 | + }); |
| 145 | + |
| 146 | + $value = $suspension->suspend(); // "hello" |
| 147 | + echo $value; // "hello" |
| 148 | +}); |
| 149 | + |
| 150 | +$fiber->start(); |
| 151 | +EventLoop::run(); |
| 152 | +``` |
| 153 | + |
| 154 | +| Method | Description | |
| 155 | +|---|---| |
| 156 | +| `Suspension::suspend(): mixed` | Suspend the current fiber | |
| 157 | +| `Suspension::resume(mixed $value = null): void` | Resume with a value | |
| 158 | +| `Suspension::throw(Throwable $e): void` | Resume by throwing an exception | |
| 159 | + |
| 160 | +## I/O Drivers |
| 161 | + |
| 162 | +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. |
| 163 | + |
| 164 | +| Driver | Platforms | Scalability | Notes | |
| 165 | +|---|---|---|---| |
| 166 | +| **epoll** | Linux 2.6+ | O(1) | Kernel tracks descriptors; returns only ready ones | |
| 167 | +| **kqueue** | macOS, FreeBSD, OpenBSD | O(1) | Same principle as epoll, native to BSD systems | |
| 168 | +| **poll** | Any POSIX | O(n) | No descriptor limit, but scans all on every call | |
| 169 | +| **select** | Universal (fallback) | O(n) | Oldest API, limited to ~1024 descriptors | |
| 170 | + |
| 171 | +**Selection priority:** epoll > kqueue > poll > select. The first one that compiles and initializes successfully wins. |
| 172 | + |
| 173 | +In practice this means: |
| 174 | +- **Linux servers** (the most common deployment) get **epoll** -- handles thousands of connections with near-zero overhead |
| 175 | +- **macOS** (local development) gets **kqueue** -- equally efficient |
| 176 | +- Older or exotic systems gracefully fall back to **poll** or **select** |
| 177 | + |
| 178 | +Check which driver is active: |
| 179 | + |
| 180 | +```php |
| 181 | +echo EventLoop::getDriver(); // "epoll" on Linux, "kqueue" on macOS |
| 182 | +``` |
| 183 | + |
| 184 | +## Benchmarks |
| 185 | + |
| 186 | +**Environment:** PHP 8.5.4, Apple M1 Max, macOS, 100,000 iterations. |
| 187 | +Revolt v1.0.8 with StreamSelectDriver (default, no ext-ev/ext-uv). ext-eventloop using kqueue driver. |
| 188 | + |
| 189 | +| Benchmark | Revolt | ext-eventloop | Speedup | |
| 190 | +|---|--:|--:|--:| |
| 191 | +| `defer()` dispatch | 715,053 ops/sec | 3,712,263 ops/sec | **5.2x** | |
| 192 | +| `delay(0)` dispatch | 234,229 ops/sec | 2,832,500 ops/sec | **12.1x** | |
| 193 | +| `repeat()` dispatch | 679,452 ops/sec | 17,794,516 ops/sec | **26.2x** | |
| 194 | +| I/O register + cancel | 2,109,267 ops/sec | 7,635,677 ops/sec | **3.6x** | |
| 195 | +| Fiber suspend/resume | 221,776 ops/sec | 248,738 ops/sec | **1.1x** | |
| 196 | + |
| 197 | +> **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. |
| 198 | +
|
| 199 | +## Migrating from Revolt |
| 200 | + |
| 201 | +The API contract is intentionally compatible. In most cases, a namespace swap is all you need: |
| 202 | + |
| 203 | +```diff |
| 204 | +- use Revolt\EventLoop; |
| 205 | ++ use EventLoop\EventLoop; |
| 206 | +``` |
| 207 | + |
| 208 | +If you use `Revolt\EventLoop\Suspension`: |
| 209 | + |
| 210 | +```diff |
| 211 | +- use Revolt\EventLoop\Suspension; |
| 212 | ++ use EventLoop\Suspension; |
| 213 | +``` |
| 214 | + |
| 215 | +## Testing |
| 216 | + |
| 217 | +```bash |
| 218 | +make test |
| 219 | +``` |
| 220 | + |
| 221 | +The extension ships with 26 `.phpt` tests covering defer, delay, repeat, I/O watchers, signals, suspensions, error handling, and edge cases. |
| 222 | + |
| 223 | +## Acknowledgements |
| 224 | + |
| 225 | +This project is built on the ideas and API design of [Revolt](https://github.com/revoltphp/event-loop) 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. |
| 226 | + |
| 227 | +## License |
| 228 | + |
| 229 | +Licensed under the [MIT License](LICENSE). |
0 commit comments