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