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