Skip to content

Commit 0eb349d

Browse files
committed
Init commit
0 parents  commit 0eb349d

44 files changed

Lines changed: 4632 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Build artifacts
2+
*.o
3+
*.lo
4+
*.la
5+
*.dep
6+
*.loT
7+
.libs/
8+
.deps/
9+
modules/
10+
autom4te.cache/
11+
12+
# phpize / configure generated
13+
acinclude.m4
14+
aclocal.m4
15+
build/
16+
config.guess
17+
config.h
18+
config.h.in
19+
config.h.in~
20+
config.log
21+
config.nice
22+
config.status
23+
config.sub
24+
configure
25+
configure.in
26+
install-sh
27+
libtool
28+
ltmain.sh
29+
Makefile
30+
Makefile.fragments
31+
Makefile.global
32+
Makefile.objects
33+
missing
34+
mkinstalldirs
35+
run-tests.php
36+
37+
# Temporary files
38+
*.swp
39+
*.swo
40+
*~
41+
.idea/
42+
.vscode/
43+
44+
# tmp/ is tracked (benchmarks), but ignore vendor inside it
45+
tmp/vendor/
46+
tmp/composer.lock

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Aleksandr Cherednikov
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
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).

config.m4

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
dnl config.m4 for extension eventloop
2+
3+
PHP_ARG_ENABLE([eventloop],
4+
[whether to enable EventLoop support],
5+
[AS_HELP_STRING([--enable-eventloop],
6+
[Enable EventLoop support])], [no])
7+
8+
if test "$PHP_EVENTLOOP" != "no"; then
9+
AC_DEFINE([HAVE_EVENTLOOP], [1], [Whether EventLoop support is enabled])
10+
11+
AC_CHECK_HEADERS([sys/epoll.h], [
12+
AC_DEFINE([HAVE_EPOLL], [1], [Have epoll support])
13+
])
14+
15+
AC_CHECK_HEADERS([sys/event.h], [
16+
AC_DEFINE([HAVE_KQUEUE], [1], [Have kqueue support])
17+
])
18+
19+
AC_CHECK_HEADERS([poll.h], [
20+
AC_DEFINE([HAVE_POLL], [1], [Have poll support])
21+
])
22+
23+
AC_CHECK_FUNCS([clock_gettime])
24+
25+
dnl Fibers are available since PHP 8.1
26+
AC_DEFINE([HAVE_FIBERS], [1], [Have Fiber support])
27+
28+
EVENTLOOP_SOURCES="eventloop.c eventloop_cb.c eventloop_timer.c eventloop_suspension.c drivers/select.c"
29+
30+
if test "$ac_cv_header_poll_h" = "yes"; then
31+
EVENTLOOP_SOURCES="$EVENTLOOP_SOURCES drivers/poll.c"
32+
fi
33+
34+
if test "$ac_cv_header_sys_epoll_h" = "yes"; then
35+
EVENTLOOP_SOURCES="$EVENTLOOP_SOURCES drivers/epoll.c"
36+
fi
37+
38+
if test "$ac_cv_header_sys_event_h" = "yes"; then
39+
EVENTLOOP_SOURCES="$EVENTLOOP_SOURCES drivers/kqueue.c"
40+
fi
41+
42+
PHP_NEW_EXTENSION([eventloop], [$EVENTLOOP_SOURCES], [$ext_shared])
43+
PHP_ADD_BUILD_DIR([$ext_builddir/drivers])
44+
fi

config.w32

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
ARG_ENABLE("eventloop", "EventLoop support", "no");
2+
3+
if (PHP_EVENTLOOP != "no") {
4+
EXTENSION("eventloop",
5+
"eventloop.c eventloop_cb.c eventloop_timer.c eventloop_suspension.c drivers\\select.c",
6+
PHP_EVENTLOOP_SHARED);
7+
8+
AC_DEFINE("HAVE_EVENTLOOP", 1, "EventLoop support");
9+
}

0 commit comments

Comments
 (0)