Closure
Rust closures can be passed to PHP through a wrapper class PhpClosure. The
Rust closure must be static (i.e. can only reference things with a 'static
lifetime, so not self in methods), and can take up to 8 parameters, all of
which must implement FromZval. The return type must implement IntoZval.
Passing closures from Rust to PHP is feature-gated behind the closure feature.
Enable it in your Cargo.toml:
ext-php-rs = { version = "...", features = ["closure"] }
PHP callables (which includes closures) can be passed to Rust through the
Callable type. When calling a callable, you must provide it with a Vec of
arguments, all of which must implement IntoZval and Clone.
T parameter | &T parameter | T Return type | &T Return type | PHP representation |
|---|---|---|---|---|
Callable | No | Closure, Callablefor PHP functions | No | Callables are implemented in PHP, closures are represented as an instance of PhpClosure. |
Internally, when you enable the closure feature, a class PhpClosure is
registered alongside your other classes:
<?php
class PhpClosure
{
public function __invoke(..$args): mixed;
}
This class cannot be instantiated from PHP. When the class is invoked, the underlying Rust closure is called. There are three types of closures in Rust:
Fn and FnMut
These closures can be called multiple times. FnMut differs from Fn in the
fact that it can modify variables in its scope.
Example
#![cfg_attr(windows, feature(abi_vectorcall))] extern crate ext_php_rs; use ext_php_rs::prelude::*; #[php_function] pub fn closure_get_string() -> Closure { // Return a closure which takes two integers and returns a string Closure::wrap(Box::new(|a, b| { format!("A: {} B: {}", a, b) }) as Box<dyn Fn(i32, i32) -> String>) } #[php_function] pub fn closure_count() -> Closure { let mut count = 0i32; // Return a closure which takes an integer, adds it to a persistent integer, // and returns the updated value. Closure::wrap(Box::new(move |a: i32| { count += a; count }) as Box<dyn FnMut(i32) -> i32>) } fn main() {}
FnOnce
Closures that implement FnOnce can only be called once. They consume some sort
of value. Calling these closures more than once will cause them to throw an
exception. They must be wrapped using the wrap_once function instead of
wrap.
Internally, the FnOnce closure is wrapped again by an FnMut closure, which
owns the FnOnce closure until it is called. If the FnMut closure is called
again, the FnOnce closure would have already been consumed, and an exception
will be thrown.
Example
#![cfg_attr(windows, feature(abi_vectorcall))] extern crate ext_php_rs; use ext_php_rs::prelude::*; #[php_function] pub fn closure_return_string() -> Closure { let example: String = "Hello, world!".into(); // This closure consumes `example` and therefore cannot be called more than once. Closure::wrap_once(Box::new(move || { example }) as Box<dyn FnOnce() -> String>) } fn main() {}
Closures must be boxed as PHP classes cannot support generics, therefore trait objects must be used. These must be boxed to have a compile time size.
Callable
Callables are simply represented as zvals. You can attempt to get a callable
function by its name, or as a parameter. They can be called through the
try_call method implemented on Callable, which returns a zval in a result.
Callable parameter
#![cfg_attr(windows, feature(abi_vectorcall))] extern crate ext_php_rs; use ext_php_rs::prelude::*; #[php_function] pub fn callable_parameter(call: ZendCallable) { let val = call.try_call(vec![&0, &1, &"Hello"]).expect("Failed to call function"); dbg!(val); } fn main() {}