1use 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
19static CLOSURE_META: ClassMetadata<Closure> = ClassMetadata::new();
21
22pub struct Closure(Box<dyn PhpClosure>);
51
52unsafe impl Send for Closure {}
53unsafe impl Sync for Closure {}
54
55impl Closure {
56 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 pub fn wrap_once<T>(func: T) -> Self
108 where
109 T: PhpOnceClosure + 'static,
110 {
111 func.into_closure()
112 }
113
114 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 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#[allow(clippy::missing_safety_doc)]
192pub unsafe trait PhpClosure {
193 fn invoke<'a>(&'a mut self, parser: ArgParser<'a, '_>, ret: &mut Zval);
195}
196
197pub trait PhpOnceClosure {
203 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);