Skip to content

Commit 55724c9

Browse files
committed
Add async support using php-tokio
1 parent 1b55652 commit 55724c9

File tree

3 files changed

+176
-0
lines changed

3 files changed

+176
-0
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ cfg-if = "1.0"
1818
once_cell = "1.17"
1919
anyhow = { version = "1", optional = true }
2020
ext-php-rs-derive = { version = "=0.10.1", path = "./crates/macros" }
21+
php-tokio = "0.1.4"
2122

2223
[dev-dependencies]
2324
skeptic = "0.13"

guide/src/macros/impl.md

Lines changed: 131 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 &rauquo;](#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 &rauquo;](#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,119 @@ 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+
```rust,no_run
188+
use ext_php_rs::prelude::*;
189+
use php_tokio::EventLoop;
190+
191+
#[php_class]
192+
struct Client {}
193+
194+
#[php_async_impl]
195+
impl Client {
196+
pub fn init() -> PhpResult<u64> {
197+
EventLoop::init()
198+
}
199+
pub fn wakeup() -> PhpResult<()> {
200+
EventLoop::wakeup()
201+
}
202+
pub async fn get(url: &str) -> anyhow::Result<String> {
203+
Ok(reqwest::get(url).await?.text().await?)
204+
}
205+
}
206+
207+
pub extern "C" fn request_shutdown(_type: i32, _module_number: i32) -> i32 {
208+
EventLoop::shutdown();
209+
0
210+
}
211+
212+
#[php_module]
213+
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
214+
module.request_shutdown_function(request_shutdown)
215+
}
216+
```
217+
218+
Here's the async PHP code we use to interact with the Rust class we just exposed.
219+
220+
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 shwon by the following code.
221+
222+
See [here &raquo;](https://amphp.org) for more info on async PHP using [amphp](https://amphp.org) + [revolt](https://revolt.run).
223+
224+
```php
225+
<?php declare(strict_types=1);
226+
227+
namespace Reqwest;
228+
229+
use Revolt\EventLoop;
230+
231+
use function Amp\async;
232+
use function Amp\Future\await;
233+
234+
final class Client
235+
{
236+
private static ?string $id = null;
237+
238+
public static function init(): void
239+
{
240+
if (self::$id !== null) {
241+
return;
242+
}
243+
244+
$f = \fopen("php://fd/".\Client::init(), 'r+');
245+
\stream_set_blocking($f, false);
246+
self::$id = EventLoop::onReadable($f, fn () => \Client::wakeup());
247+
}
248+
249+
public static function reference(): void
250+
{
251+
EventLoop::reference(self::$id);
252+
}
253+
public static function unreference(): void
254+
{
255+
EventLoop::unreference(self::$id);
256+
}
257+
258+
public static function __callStatic(string $name, array $args): mixed
259+
{
260+
return \Client::$name(...$args);
261+
}
262+
}
263+
264+
265+
Client::init();
266+
267+
function test(int $delay): void
268+
{
269+
$url = "https://httpbin.org/delay/$delay";
270+
$t = time();
271+
echo "Making async reqwest to $url that will return after $delay seconds...".PHP_EOL;
272+
Client::get($url);
273+
$t = time() - $t;
274+
echo "Got response from $url after ~".$t." seconds!".PHP_EOL;
275+
};
276+
277+
$futures = [];
278+
$futures []= async(test(...), 5);
279+
$futures []= async(test(...), 5);
280+
$futures []= async(test(...), 5);
281+
282+
await($futures);
283+
```
284+
285+
Result:
286+
287+
```
288+
Making async reqwest to https://httpbin.org/delay/5 that will return after 5 seconds...
289+
Making async reqwest to https://httpbin.org/delay/5 that will return after 5 seconds...
290+
Making async reqwest to https://httpbin.org/delay/5 that will return after 5 seconds...
291+
Got response from https://httpbin.org/delay/5 after ~5 seconds!
292+
Got response from https://httpbin.org/delay/5 after ~5 seconds!
293+
Got response from https://httpbin.org/delay/5 after ~5 seconds!
294+
```
295+
165296
[`php_function`]: ./function.md

src/lib.rs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ pub mod prelude {
4848
pub use crate::php_extern;
4949
pub use crate::php_function;
5050
pub use crate::php_impl;
51+
pub use crate::php_async_impl;
5152
pub use crate::php_module;
5253
pub use crate::php_print;
5354
pub use crate::php_println;
@@ -390,6 +391,49 @@ pub use ext_php_rs_derive::php_function;
390391
/// ```
391392
pub use ext_php_rs_derive::php_impl;
392393

394+
/// Just like php_impl, annotates a structs `impl` block, declaring that
395+
/// all methods and constants declared inside the `impl` block will be declared
396+
/// as PHP methods and constants.
397+
///
398+
/// This variant of php_impl supports async Rust methods, using [php-tokio](https://github.com/danog/php-tokio)
399+
/// to integrate [tokio](https://tokio.rs) with PHP fibers and the [Revolt](https://revolt.run) event loop,
400+
/// compatible with [Amphp](https://amphp.org), [PSL](https://github.com/azjezz/psl) and any other async PHP library based on Revolt.
401+
///
402+
/// # Example
403+
///
404+
/// ```no_run
405+
/// # #![cfg_attr(windows, feature(abi_vectorcall))]
406+
/// # use ext_php_rs::prelude::*;
407+
/// use php_tokio::EventLoop;
408+
///
409+
/// #[php_class]
410+
/// struct Client {}
411+
///
412+
/// #[php_async_impl]
413+
/// impl Client {
414+
/// pub fn init() -> PhpResult<u64> {
415+
/// EventLoop::init()
416+
/// }
417+
/// pub fn wakeup() -> PhpResult<()> {
418+
/// EventLoop::wakeup()
419+
/// }
420+
/// pub async fn get(url: &str) -> anyhow::Result<String> {
421+
/// Ok(reqwest::get(url).await?.text().await?)
422+
/// }
423+
/// }
424+
///
425+
/// pub extern "C" fn request_shutdown(_type: i32, _module_number: i32) -> i32 {
426+
/// EventLoop::shutdown();
427+
/// 0
428+
/// }
429+
///
430+
/// #[php_module]
431+
/// pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
432+
/// module.request_shutdown_function(request_shutdown)
433+
/// }
434+
/// ```
435+
pub use php_tokio::php_async_impl;
436+
393437
/// Annotates a function that will be used by PHP to retrieve information about
394438
/// the module.
395439
///

0 commit comments

Comments
 (0)