• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

davidcole1340 / ext-php-rs / 15920812749

27 Jun 2025 07:32AM UTC coverage: 20.629% (-1.3%) from 21.978%
15920812749

push

github

web-flow
Merge pull request #472 from Qard/enable-zts-on-nix

761 of 3689 relevant lines covered (20.63%)

3.57 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

9.38
/src/closure.rs
1
//! Types and functions used for exporting Rust closures to PHP.
2

3
use std::collections::HashMap;
4

5
use 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.
20
static 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.
50
pub struct Closure(Box<dyn PhpClosure>);
51

52
unsafe impl Send for Closure {}
53
unsafe impl Sync for Closure {}
54

55
impl 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() {
1✔
122
        assert!(!CLOSURE_META.has_ce(), "Closure class already built.");
2✔
123

124
        ClassBuilder::new("RustClosure")
1✔
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))
3✔
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

149
impl 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>> {
1✔
169
        None
1✔
170
    }
171

172
    fn constants() -> &'static [(
×
173
        &'static str,
174
        &'static dyn crate::convert::IntoZvalDyn,
175
        DocComments,
176
    )] {
177
        unimplemented!()
178
    }
179
}
180

181
class_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)]
192
pub 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.
202
pub 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

208
unsafe impl<R> PhpClosure for Box<dyn Fn() -> R>
209
where
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

220
unsafe impl<R> PhpClosure for Box<dyn FnMut() -> R>
221
where
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

232
impl<R> PhpOnceClosure for Box<dyn FnOnce() -> R>
233
where
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

253
macro_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

320
php_closure_impl!(A);
321
php_closure_impl!(A, B);
322
php_closure_impl!(A, B, C);
323
php_closure_impl!(A, B, C, D);
324
php_closure_impl!(A, B, C, D, E);
325
php_closure_impl!(A, B, C, D, E, F);
326
php_closure_impl!(A, B, C, D, E, F, G);
327
php_closure_impl!(A, B, C, D, E, F, G, H);
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc