Skip to content

Commit aa88f5a

Browse files
authored
Add async support using php-tokio
Add async support using php-tokio
2 parents 1b55652 + 8462005 commit aa88f5a

File tree

2 files changed

+134
-0
lines changed

2 files changed

+134
-0
lines changed

guide/src/SUMMARY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
- [Class Object](./types/class_object.md)
2525
- [Closure](./types/closure.md)
2626
- [Functions & methods](./types/functions.md)
27+
- [Async futures](./macros/impl.md#async)
2728
- [Macros](./macros/index.md)
2829
- [Module](./macros/module.md)
2930
- [Module Startup Function](./macros/module_startup.md)

guide/src/macros/impl.md

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ implementations cannot be exported to PHP.
88
If you do not want a function exported to PHP, you should place it in a separate
99
`impl` block.
1010

11+
If you want to use async Rust, use `#[php_async_impl]`, instead: see [here »](#async) for more info.
12+
1113
## Methods
1214

1315
Methods basically follow the same rules as functions, so read about the
@@ -63,6 +65,20 @@ the attribute, the function is not exported to PHP like a regular method.
6365

6466
Constructors cannot use the visibility or rename attributes listed above.
6567

68+
### Async
69+
70+
Using `#[php_async_impl]` instead of `#[php_impl]` allows us to expose any async Rust library to PHP, using [PHP fibers](https://www.php.net/manual/en/language.fibers.php), [php-tokio](https://github.com/danog/php-tokio) and the [PHP Revolt event loop](https://revolt.run) under the hood to handle async interoperability.
71+
72+
This allows full compatibility with [amphp](https://amphp.org), [PSL](https://github.com/azjezz/psl), [reactphp](https://reactphp.org) and any other async PHP library based on [Revolt](https://revolt.run).
73+
74+
Traits annotated with `#[php_impl]` can freely expose any async function, using `await` and any async Rust library.
75+
76+
Make sure to also expose the `php_tokio::EventLoop::init` and `php_tokio::EventLoop::wakeup` functions to PHP in order to initialize the event loop, as specified in the full example [here »](#async-example).
77+
78+
Also, make sure to invoke `EventLoop::shutdown` in the request shutdown handler to clean up the tokio event loop before finishing the request.
79+
80+
See [here »](#async-example) for the full example.
81+
6682
## Constants
6783

6884
Constants are defined as regular Rust `impl` constants. Any type that implements
@@ -162,4 +178,121 @@ var_dump(Human::get_max_age()); // int(100)
162178
var_dump(Human::MAX_AGE); // int(100)
163179
```
164180

181+
### Async example
182+
183+
In this example, we're exposing an async Rust HTTP client library called [reqwest](https://docs.rs/reqwest/latest/reqwest/) to PHP, using [PHP fibers](https://www.php.net/manual/en/language.fibers.php), [php-tokio](https://github.com/danog/php-tokio) and the [PHP Revolt event loop](https://revolt.run) under the hood to handle async interoperability.
184+
185+
This allows full compatibility with [amphp](https://amphp.org), [PSL](https://github.com/azjezz/psl), [reactphp](https://reactphp.org) and any other async PHP library based on [Revolt](https://revolt.run).
186+
187+
Make sure to require [php-tokio](https://github.com/danog/php-tokio) as a dependency before proceeding.
188+
189+
```rust,ignore
190+
use ext_php_rs::prelude::*;
191+
use php_tokio::{php_async_impl, EventLoop};
192+
193+
#[php_class]
194+
struct Client {}
195+
196+
#[php_async_impl]
197+
impl Client {
198+
pub fn init() -> PhpResult<u64> {
199+
EventLoop::init()
200+
}
201+
pub fn wakeup() -> PhpResult<()> {
202+
EventLoop::wakeup()
203+
}
204+
pub async fn get(url: &str) -> anyhow::Result<String> {
205+
Ok(reqwest::get(url).await?.text().await?)
206+
}
207+
}
208+
209+
pub extern "C" fn request_shutdown(_type: i32, _module_number: i32) -> i32 {
210+
EventLoop::shutdown();
211+
0
212+
}
213+
214+
#[php_module]
215+
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
216+
module.request_shutdown_function(request_shutdown)
217+
}
218+
```
219+
220+
Here's the async PHP code we use to interact with the Rust class we just exposed.
221+
222+
The `Client::init` method needs to be called only once in order to initialize the Revolt event loop and link it to the Tokio event loop, as shown by the following code.
223+
224+
See [here &raquo;](https://amphp.org) for more info on async PHP using [amphp](https://amphp.org) + [revolt](https://revolt.run).
225+
226+
```php
227+
<?php declare(strict_types=1);
228+
229+
namespace Reqwest;
230+
231+
use Revolt\EventLoop;
232+
233+
use function Amp\async;
234+
use function Amp\Future\await;
235+
236+
final class Client
237+
{
238+
private static ?string $id = null;
239+
240+
public static function init(): void
241+
{
242+
if (self::$id !== null) {
243+
return;
244+
}
245+
246+
$f = \fopen("php://fd/".\Client::init(), 'r+');
247+
\stream_set_blocking($f, false);
248+
self::$id = EventLoop::onReadable($f, fn () => \Client::wakeup());
249+
}
250+
251+
public static function reference(): void
252+
{
253+
EventLoop::reference(self::$id);
254+
}
255+
public static function unreference(): void
256+
{
257+
EventLoop::unreference(self::$id);
258+
}
259+
260+
public static function __callStatic(string $name, array $args): mixed
261+
{
262+
return \Client::$name(...$args);
263+
}
264+
}
265+
266+
267+
Client::init();
268+
269+
function test(int $delay): void
270+
{
271+
$url = "https://httpbin.org/delay/$delay";
272+
$t = time();
273+
echo "Making async reqwest to $url that will return after $delay seconds...".PHP_EOL;
274+
Client::get($url);
275+
$t = time() - $t;
276+
echo "Got response from $url after ~".$t." seconds!".PHP_EOL;
277+
};
278+
279+
$futures = [];
280+
$futures []= async(test(...), 5);
281+
$futures []= async(test(...), 5);
282+
$futures []= async(test(...), 5);
283+
284+
await($futures);
285+
```
286+
287+
Result:
288+
289+
```
290+
Making async reqwest to https://httpbin.org/delay/5 that will return after 5 seconds...
291+
Making async reqwest to https://httpbin.org/delay/5 that will return after 5 seconds...
292+
Making async reqwest to https://httpbin.org/delay/5 that will return after 5 seconds...
293+
Got response from https://httpbin.org/delay/5 after ~5 seconds!
294+
Got response from https://httpbin.org/delay/5 after ~5 seconds!
295+
Got response from https://httpbin.org/delay/5 after ~5 seconds!
296+
```
297+
165298
[`php_function`]: ./function.md

0 commit comments

Comments
 (0)