ext_php_rs/
closure.rs

1//! Types and functions used for exporting Rust closures to PHP.
2
3use std::collections::HashMap;
4
5use crate::{
6    args::{Arg, ArgParser},
7    builders::{ClassBuilder, FunctionBuilder},
8    class::{ClassMetadata, RegisteredClass},
9    convert::{FromZval, IntoZval},
10    exception::PhpException,
11    flags::{DataType, MethodFlags},
12    props::Property,
13    types::Zval,
14    zend::ExecuteData,
15    zend_fastcall,
16};
17
18/// Class entry and handlers for Rust closures.
19static CLOSURE_META: ClassMetadata<Closure> = ClassMetadata::new();
20
21/// Wrapper around a Rust closure, which can be exported to PHP.
22///
23/// Closures can have up to 8 parameters, all must implement [`FromZval`], and
24/// can return anything that implements [`IntoZval`]. Closures must have a
25/// static lifetime, and therefore cannot modify any `self` references.
26///
27/// Internally, closures are implemented as a PHP class. A class `RustClosure`
28/// is registered with an `__invoke` method:
29///
30/// ```php
31/// <?php
32///
33/// class RustClosure {
34///     public function __invoke(...$args): mixed {
35///         // ...    
36///     }
37/// }
38/// ```
39///
40/// The Rust closure is then double boxed, firstly as a `Box<dyn Fn(...) ->
41/// ...>` (depending on the signature of the closure) and then finally boxed as
42/// a `Box<dyn PhpClosure>`. This is a workaround, as `PhpClosure` is not
43/// generically implementable on types that implement `Fn(T, ...) -> Ret`. Make
44/// a suggestion issue if you have a better idea of implementing this!.
45///
46/// When the `__invoke` method is called from PHP, the `invoke` method is called
47/// on the `dyn PhpClosure`\ trait object, and from there everything is
48/// basically the same as a regular PHP function.
49pub struct Closure(Box<dyn PhpClosure>);
50
51unsafe impl Send for Closure {}
52unsafe impl Sync for Closure {}
53
54impl Closure {
55    /// Wraps a [`Fn`] or [`FnMut`] Rust closure into a type which can be
56    /// returned to PHP.
57    ///
58    /// The closure can accept up to 8 arguments which implement [`IntoZval`],
59    /// and can return any type which implements [`FromZval`]. The closure
60    /// must have a static lifetime, so cannot reference `self`.
61    ///
62    /// # Parameters
63    ///
64    /// * `func` - The closure to wrap. Should be boxed in the form `Box<dyn
65    ///   Fn[Mut](...) -> ...>`.
66    ///
67    /// # Example
68    ///
69    /// ```rust,no_run
70    /// use ext_php_rs::closure::Closure;
71    ///
72    /// let closure = Closure::wrap(Box::new(|name| {
73    ///     format!("Hello {}", name)
74    /// }) as Box<dyn Fn(String) -> String>);
75    /// ```
76    pub fn wrap<T>(func: T) -> Self
77    where
78        T: PhpClosure + 'static,
79    {
80        Self(Box::new(func) as Box<dyn PhpClosure>)
81    }
82
83    /// Wraps a [`FnOnce`] Rust closure into a type which can be returned to
84    /// PHP. If the closure is called more than once from PHP, an exception
85    /// is thrown.
86    ///
87    /// The closure can accept up to 8 arguments which implement [`IntoZval`],
88    /// and can return any type which implements [`FromZval`]. The closure
89    /// must have a static lifetime, so cannot reference `self`.
90    ///
91    /// # Parameters
92    ///
93    /// * `func` - The closure to wrap. Should be boxed in the form `Box<dyn
94    ///   FnOnce(...) -> ...>`.
95    ///
96    /// # Example
97    ///
98    /// ```rust,no_run
99    /// use ext_php_rs::closure::Closure;
100    ///
101    /// let name: String = "Hello world".into();
102    /// let closure = Closure::wrap_once(Box::new(|| {
103    ///     name
104    /// }) as Box<dyn FnOnce() -> String>);
105    /// ```
106    pub fn wrap_once<T>(func: T) -> Self
107    where
108        T: PhpOnceClosure + 'static,
109    {
110        func.into_closure()
111    }
112
113    /// Builds the class entry for [`Closure`], registering it with PHP. This
114    /// function should only be called once inside your module startup
115    /// function.
116    ///
117    /// # Panics
118    ///
119    /// Panics if the function is called more than once.
120    pub fn build() {
121        if CLOSURE_META.has_ce() {
122            panic!("Closure has already been built.");
123        }
124
125        let ce = ClassBuilder::new("RustClosure")
126            .method(
127                FunctionBuilder::new("__invoke", Self::invoke)
128                    .not_required()
129                    .arg(Arg::new("args", DataType::Mixed).is_variadic())
130                    .returns(DataType::Mixed, false, true)
131                    .build()
132                    .expect("Failed to build `RustClosure` PHP class."),
133                MethodFlags::Public,
134            )
135            .object_override::<Self>()
136            .build()
137            .expect("Failed to build `RustClosure` PHP class.");
138        CLOSURE_META.set_ce(ce);
139    }
140
141    zend_fastcall! {
142        /// External function used by the Zend interpreter to call the closure.
143        extern "C" fn invoke(ex: &mut ExecuteData, ret: &mut Zval) {
144            let (parser, this) = ex.parser_method::<Self>();
145            let this = this.expect("Internal closure function called on non-closure class");
146
147            this.0.invoke(parser, ret)
148        }
149    }
150}
151
152impl RegisteredClass for Closure {
153    const CLASS_NAME: &'static str = "RustClosure";
154
155    fn get_metadata() -> &'static ClassMetadata<Self> {
156        &CLOSURE_META
157    }
158
159    fn get_properties<'a>() -> HashMap<&'static str, Property<'a, Self>> {
160        HashMap::new()
161    }
162}
163
164class_derives!(Closure);
165
166/// Implemented on types which can be used as PHP closures.
167///
168/// Types must implement the `invoke` function which will be called when the
169/// closure is called from PHP. Arguments must be parsed from the
170/// [`ExecuteData`] and the return value is returned through the [`Zval`].
171///
172/// This trait is automatically implemented on functions with up to 8
173/// parameters.
174#[allow(clippy::missing_safety_doc)]
175pub unsafe trait PhpClosure {
176    /// Invokes the closure.
177    fn invoke<'a>(&'a mut self, parser: ArgParser<'a, '_>, ret: &mut Zval);
178}
179
180/// Implemented on [`FnOnce`] types which can be used as PHP closures. See
181/// [`Closure`].
182///
183/// Internally, this trait should wrap the [`FnOnce`] closure inside a [`FnMut`]
184/// closure, and prevent the user from calling the closure more than once.
185pub trait PhpOnceClosure {
186    /// Converts the Rust [`FnOnce`] closure into a [`FnMut`] closure, and then
187    /// into a PHP closure.
188    fn into_closure(self) -> Closure;
189}
190
191unsafe impl<R> PhpClosure for Box<dyn Fn() -> R>
192where
193    R: IntoZval,
194{
195    fn invoke(&mut self, _: ArgParser, ret: &mut Zval) {
196        if let Err(e) = self().set_zval(ret, false) {
197            let _ = PhpException::default(format!("Failed to return closure result to PHP: {}", e))
198                .throw();
199        }
200    }
201}
202
203unsafe impl<R> PhpClosure for Box<dyn FnMut() -> R>
204where
205    R: IntoZval,
206{
207    fn invoke(&mut self, _: ArgParser, ret: &mut Zval) {
208        if let Err(e) = self().set_zval(ret, false) {
209            let _ = PhpException::default(format!("Failed to return closure result to PHP: {}", e))
210                .throw();
211        }
212    }
213}
214
215impl<R> PhpOnceClosure for Box<dyn FnOnce() -> R>
216where
217    R: IntoZval + 'static,
218{
219    fn into_closure(self) -> Closure {
220        let mut this = Some(self);
221
222        Closure::wrap(Box::new(move || {
223            let this = match this.take() {
224                Some(this) => this,
225                None => {
226                    let _ = PhpException::default(
227                        "Attempted to call `FnOnce` closure more than once.".into(),
228                    )
229                    .throw();
230                    return Option::<R>::None;
231                }
232            };
233
234            Some(this())
235        }) as Box<dyn FnMut() -> Option<R>>)
236    }
237}
238
239macro_rules! php_closure_impl {
240    ($($gen: ident),*) => {
241        php_closure_impl!(Fn; $($gen),*);
242        php_closure_impl!(FnMut; $($gen),*);
243
244        impl<$($gen),*, Ret> PhpOnceClosure for Box<dyn FnOnce($($gen),*) -> Ret>
245        where
246            $(for<'a> $gen: FromZval<'a> + 'static,)*
247            Ret: IntoZval + 'static,
248        {
249            fn into_closure(self) -> Closure {
250                let mut this = Some(self);
251
252                Closure::wrap(Box::new(move |$($gen),*| {
253                    let this = match this.take() {
254                        Some(this) => this,
255                        None => {
256                            let _ = PhpException::default(
257                                "Attempted to call `FnOnce` closure more than once.".into(),
258                            )
259                            .throw();
260                            return Option::<Ret>::None;
261                        }
262                    };
263
264                    Some(this($($gen),*))
265                }) as Box<dyn FnMut($($gen),*) -> Option<Ret>>)
266            }
267        }
268    };
269
270    ($fnty: ident; $($gen: ident),*) => {
271        unsafe impl<$($gen),*, Ret> PhpClosure for Box<dyn $fnty($($gen),*) -> Ret>
272        where
273            $(for<'a> $gen: FromZval<'a>,)*
274            Ret: IntoZval
275        {
276            fn invoke(&mut self, parser: ArgParser, ret: &mut Zval) {
277                $(
278                    let mut $gen = Arg::new(stringify!($gen), $gen::TYPE);
279                )*
280
281                let parser = parser
282                    $(.arg(&mut $gen))*
283                    .parse();
284
285                if parser.is_err() {
286                    return;
287                }
288
289                let result = self(
290                    $(
291                        match $gen.consume() {
292                            Ok(val) => val,
293                            _ => {
294                                let _ = PhpException::default(concat!("Invalid parameter type for `", stringify!($gen), "`.").into()).throw();
295                                return;
296                            }
297                        }
298                    ),*
299                );
300
301                if let Err(e) = result.set_zval(ret, false) {
302                    let _ = PhpException::default(format!("Failed to return closure result to PHP: {}", e)).throw();
303                }
304            }
305        }
306    };
307}
308
309php_closure_impl!(A);
310php_closure_impl!(A, B);
311php_closure_impl!(A, B, C);
312php_closure_impl!(A, B, C, D);
313php_closure_impl!(A, B, C, D, E);
314php_closure_impl!(A, B, C, D, E, F);
315php_closure_impl!(A, B, C, D, E, F, G);
316php_closure_impl!(A, B, C, D, E, F, G, H);