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 /// If the class has already been built, this function returns early without
119 /// doing anything. This allows for safe repeated calls in test
120 /// environments.
121 ///
122 /// # Panics
123 ///
124 /// Panics if the `RustClosure` PHP class cannot be registered.
125 pub fn build() {
126 if CLOSURE_META.has_ce() {
127 return;
128 }
129
130 ClassBuilder::new("RustClosure")
131 .method(
132 FunctionBuilder::new("__invoke", Self::invoke)
133 .not_required()
134 .arg(Arg::new("args", DataType::Mixed).is_variadic())
135 .returns(DataType::Mixed, false, true),
136 MethodFlags::Public,
137 )
138 .object_override::<Self>()
139 .registration(|ce| CLOSURE_META.set_ce(ce))
140 .register()
141 .expect("Failed to build `RustClosure` PHP class.");
142 }
143
144 zend_fastcall! {
145 /// External function used by the Zend interpreter to call the closure.
146 extern "C" fn invoke(ex: &mut ExecuteData, ret: &mut Zval) {
147 let (parser, this) = ex.parser_method::<Self>();
148 let this = this.expect("Internal closure function called on non-closure class");
149
150 this.0.invoke(parser, ret);
151 }
152 }
153}
154
155impl RegisteredClass for Closure {
156 const CLASS_NAME: &'static str = "RustClosure";
157
158 const BUILDER_MODIFIER: Option<fn(ClassBuilder) -> ClassBuilder> = None;
159 const EXTENDS: Option<ClassEntryInfo> = None;
160 const IMPLEMENTS: &'static [ClassEntryInfo] = &[];
161
162 fn get_metadata() -> &'static ClassMetadata<Self> {
163 &CLOSURE_META
164 }
165
166 fn get_properties<'a>() -> HashMap<&'static str, PropertyInfo<'a, Self>> {
167 HashMap::new()
168 }
169
170 fn method_builders() -> Vec<(FunctionBuilder<'static>, MethodFlags)> {
171 unimplemented!()
172 }
173
174 fn constructor() -> Option<crate::class::ConstructorMeta<Self>> {
175 None
176 }
177
178 fn constants() -> &'static [(
179 &'static str,
180 &'static dyn crate::convert::IntoZvalDyn,
181 DocComments,
182 )] {
183 unimplemented!()
184 }
185}
186
187class_derives!(Closure);
188
189/// Implemented on types which can be used as PHP closures.
190///
191/// Types must implement the `invoke` function which will be called when the
192/// closure is called from PHP. Arguments must be parsed from the
193/// [`ExecuteData`] and the return value is returned through the [`Zval`].
194///
195/// This trait is automatically implemented on functions with up to 8
196/// parameters.
197#[allow(clippy::missing_safety_doc)]
198pub unsafe trait PhpClosure {
199 /// Invokes the closure.
200 fn invoke<'a>(&'a mut self, parser: ArgParser<'a, '_>, ret: &mut Zval);
201}
202
203/// Implemented on [`FnOnce`] types which can be used as PHP closures. See
204/// [`Closure`].
205///
206/// Internally, this trait should wrap the [`FnOnce`] closure inside a [`FnMut`]
207/// closure, and prevent the user from calling the closure more than once.
208pub trait PhpOnceClosure {
209 /// Converts the Rust [`FnOnce`] closure into a [`FnMut`] closure, and then
210 /// into a PHP closure.
211 fn into_closure(self) -> Closure;
212}
213
214unsafe impl<R> PhpClosure for Box<dyn Fn() -> R>
215where
216 R: IntoZval,
217{
218 fn invoke(&mut self, _: ArgParser, ret: &mut Zval) {
219 if let Err(e) = self().set_zval(ret, false) {
220 let _ = PhpException::default(format!("Failed to return closure result to PHP: {e}"))
221 .throw();
222 }
223 }
224}
225
226unsafe impl<R> PhpClosure for Box<dyn FnMut() -> R>
227where
228 R: IntoZval,
229{
230 fn invoke(&mut self, _: ArgParser, ret: &mut Zval) {
231 if let Err(e) = self().set_zval(ret, false) {
232 let _ = PhpException::default(format!("Failed to return closure result to PHP: {e}"))
233 .throw();
234 }
235 }
236}
237
238impl<R> PhpOnceClosure for Box<dyn FnOnce() -> R>
239where
240 R: IntoZval + 'static,
241{
242 fn into_closure(self) -> Closure {
243 let mut this = Some(self);
244
245 Closure::wrap(Box::new(move || {
246 let Some(this) = this.take() else {
247 let _ = PhpException::default(
248 "Attempted to call `FnOnce` closure more than once.".into(),
249 )
250 .throw();
251 return Option::<R>::None;
252 };
253
254 Some(this())
255 }) as Box<dyn FnMut() -> Option<R>>)
256 }
257}
258
259macro_rules! php_closure_impl {
260 ($($gen: ident),*) => {
261 php_closure_impl!(Fn; $($gen),*);
262 php_closure_impl!(FnMut; $($gen),*);
263
264 impl<$($gen),*, Ret> PhpOnceClosure for Box<dyn FnOnce($($gen),*) -> Ret>
265 where
266 $(for<'a> $gen: FromZval<'a> + 'static,)*
267 Ret: IntoZval + 'static,
268 {
269 fn into_closure(self) -> Closure {
270 let mut this = Some(self);
271
272 Closure::wrap(Box::new(move |$($gen),*| {
273 let Some(this) = this.take() else {
274 let _ = PhpException::default(
275 "Attempted to call `FnOnce` closure more than once.".into(),
276 )
277 .throw();
278 return Option::<Ret>::None;
279 };
280
281 Some(this($($gen),*))
282 }) as Box<dyn FnMut($($gen),*) -> Option<Ret>>)
283 }
284 }
285 };
286
287 ($fnty: ident; $($gen: ident),*) => {
288 unsafe impl<$($gen),*, Ret> PhpClosure for Box<dyn $fnty($($gen),*) -> Ret>
289 where
290 $(for<'a> $gen: FromZval<'a>,)*
291 Ret: IntoZval
292 {
293 fn invoke(&mut self, parser: ArgParser, ret: &mut Zval) {
294 $(
295 let mut $gen = Arg::new(stringify!($gen), $gen::TYPE);
296 )*
297
298 let parser = parser
299 $(.arg(&mut $gen))*
300 .parse();
301
302 if parser.is_err() {
303 return;
304 }
305
306 let result = self(
307 $(
308 match $gen.consume() {
309 Ok(val) => val,
310 _ => {
311 let _ = PhpException::default(concat!("Invalid parameter type for `", stringify!($gen), "`.").into()).throw();
312 return;
313 }
314 }
315 ),*
316 );
317
318 if let Err(e) = result.set_zval(ret, false) {
319 let _ = PhpException::default(format!("Failed to return closure result to PHP: {}", e)).throw();
320 }
321 }
322 }
323 };
324}
325
326php_closure_impl!(A);
327php_closure_impl!(A, B);
328php_closure_impl!(A, B, C);
329php_closure_impl!(A, B, C, D);
330php_closure_impl!(A, B, C, D, E);
331php_closure_impl!(A, B, C, D, E, F);
332php_closure_impl!(A, B, C, D, E, F, G);
333php_closure_impl!(A, B, C, D, E, F, G, H);