diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md index 2ca6c5f77..4108d1bda 100644 --- a/guide/src/SUMMARY.md +++ b/guide/src/SUMMARY.md @@ -24,6 +24,7 @@ - [Class Object](./types/class_object.md) - [Closure](./types/closure.md) - [Functions & methods](./types/functions.md) + - [Async futures](./macros/impl.md#async) - [Macros](./macros/index.md) - [Module](./macros/module.md) - [Module Startup Function](./macros/module_startup.md) diff --git a/guide/src/macros/impl.md b/guide/src/macros/impl.md index 7214ab939..1b872f194 100644 --- a/guide/src/macros/impl.md +++ b/guide/src/macros/impl.md @@ -8,6 +8,8 @@ implementations cannot be exported to PHP. If you do not want a function exported to PHP, you should place it in a separate `impl` block. +If you want to use async Rust, use `#[php_async_impl]`, instead: see [here »](#async) for more info. + ## Methods 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. Constructors cannot use the visibility or rename attributes listed above. +### Async + +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. + +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). + +Traits annotated with `#[php_impl]` can freely expose any async function, using `await` and any async Rust library. + +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). + +Also, make sure to invoke `EventLoop::shutdown` in the request shutdown handler to clean up the tokio event loop before finishing the request. + +See [here »](#async-example) for the full example. + ## Constants Constants are defined as regular Rust `impl` constants. Any type that implements @@ -162,4 +178,121 @@ var_dump(Human::get_max_age()); // int(100) var_dump(Human::MAX_AGE); // int(100) ``` +### Async example + +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. + +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). + +Make sure to require [php-tokio](https://github.com/danog/php-tokio) as a dependency before proceeding. + +```rust,ignore +use ext_php_rs::prelude::*; +use php_tokio::{php_async_impl, EventLoop}; + +#[php_class] +struct Client {} + +#[php_async_impl] +impl Client { + pub fn init() -> PhpResult { + EventLoop::init() + } + pub fn wakeup() -> PhpResult<()> { + EventLoop::wakeup() + } + pub async fn get(url: &str) -> anyhow::Result { + Ok(reqwest::get(url).await?.text().await?) + } +} + +pub extern "C" fn request_shutdown(_type: i32, _module_number: i32) -> i32 { + EventLoop::shutdown(); + 0 +} + +#[php_module] +pub fn get_module(module: ModuleBuilder) -> ModuleBuilder { + module.request_shutdown_function(request_shutdown) +} +``` + +Here's the async PHP code we use to interact with the Rust class we just exposed. + +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. + +See [here »](https://amphp.org) for more info on async PHP using [amphp](https://amphp.org) + [revolt](https://revolt.run). + +```php + \Client::wakeup()); + } + + public static function reference(): void + { + EventLoop::reference(self::$id); + } + public static function unreference(): void + { + EventLoop::unreference(self::$id); + } + + public static function __callStatic(string $name, array $args): mixed + { + return \Client::$name(...$args); + } +} + + +Client::init(); + +function test(int $delay): void +{ + $url = "https://httpbin.org/delay/$delay"; + $t = time(); + echo "Making async reqwest to $url that will return after $delay seconds...".PHP_EOL; + Client::get($url); + $t = time() - $t; + echo "Got response from $url after ~".$t." seconds!".PHP_EOL; +}; + +$futures = []; +$futures []= async(test(...), 5); +$futures []= async(test(...), 5); +$futures []= async(test(...), 5); + +await($futures); +``` + +Result: + +``` +Making async reqwest to https://httpbin.org/delay/5 that will return after 5 seconds... +Making async reqwest to https://httpbin.org/delay/5 that will return after 5 seconds... +Making async reqwest to https://httpbin.org/delay/5 that will return after 5 seconds... +Got response from https://httpbin.org/delay/5 after ~5 seconds! +Got response from https://httpbin.org/delay/5 after ~5 seconds! +Got response from https://httpbin.org/delay/5 after ~5 seconds! +``` + [`php_function`]: ./function.md