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

extphprs / ext-php-rs / 25379163335

05 May 2026 01:25PM UTC coverage: 72.479% (+6.2%) from 66.241%
25379163335

Pull #734

github

web-flow
Merge 889e3cde4 into 0912e7c24
Pull Request #734: feat!: PHP 8 union, intersection, DNF, and class-union type hints

2871 of 3074 new or added lines in 21 files covered. (93.4%)

3 existing lines in 2 files now uncovered.

11514 of 15886 relevant lines covered (72.48%)

33.34 hits per line

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

86.32
/src/builders/function.rs
1
use crate::{
2
    args::{Arg, ArgInfo},
3
    describe::DocComments,
4
    error::{Error, Result},
5
    flags::{DataType, MethodFlags},
6
    types::{PhpType, Zval},
7
    zend::{ExecuteData, FunctionEntry, ZendType},
8
};
9
use std::{ffi::CString, mem, ptr};
10

11
/// Function representation in Rust.
12
#[cfg(not(windows))]
13
pub type FunctionHandler = extern "C" fn(execute_data: &mut ExecuteData, retval: &mut Zval);
14
#[cfg(windows)]
15
pub type FunctionHandler =
16
    extern "vectorcall" fn(execute_data: &mut ExecuteData, retval: &mut Zval);
17

18
/// Function representation in Rust using pointers.
19
#[cfg(not(windows))]
20
type FunctionPointerHandler = extern "C" fn(execute_data: *mut ExecuteData, retval: *mut Zval);
21
#[cfg(windows)]
22
type FunctionPointerHandler =
23
    extern "vectorcall" fn(execute_data: *mut ExecuteData, retval: *mut Zval);
24

25
/// Builder for registering a function in PHP.
26
#[must_use]
27
#[derive(Debug)]
28
pub struct FunctionBuilder<'a> {
29
    pub(crate) name: String,
30
    function: FunctionEntry,
31
    pub(crate) args: Vec<Arg<'a>>,
32
    n_req: Option<usize>,
33
    pub(crate) retval: Option<PhpType>,
34
    ret_as_ref: bool,
35
    pub(crate) ret_as_null: bool,
36
    pub(crate) docs: DocComments,
37
}
38

39
impl<'a> FunctionBuilder<'a> {
40
    /// Creates a new function builder, used to build functions
41
    /// to be exported to PHP.
42
    ///
43
    /// # Parameters
44
    ///
45
    /// * `name` - The name of the function.
46
    /// * `handler` - The handler to be called when the function is invoked from
47
    ///   PHP.
48
    pub fn new<T: Into<String>>(name: T, handler: FunctionHandler) -> Self {
193✔
49
        Self {
193✔
50
            name: name.into(),
193✔
51
            function: FunctionEntry {
193✔
52
                fname: ptr::null(),
193✔
53
                // SAFETY: `*mut T` and `&mut T` have the same ABI as long as `*mut T` is non-null,
193✔
54
                // aligned and pointing to a `T`. PHP guarantees that these conditions will be met.
193✔
55
                handler: Some(unsafe {
193✔
56
                    mem::transmute::<FunctionHandler, FunctionPointerHandler>(handler)
193✔
57
                }),
193✔
58
                arg_info: ptr::null(),
193✔
59
                num_args: 0,
193✔
60
                flags: 0, // TBD?
193✔
61
                #[cfg(php84)]
193✔
62
                doc_comment: ptr::null(),
193✔
63
                #[cfg(php84)]
193✔
64
                frameless_function_infos: ptr::null(),
193✔
65
            },
193✔
66
            args: vec![],
193✔
67
            n_req: None,
193✔
68
            retval: None,
193✔
69
            ret_as_ref: false,
193✔
70
            ret_as_null: false,
193✔
71
            docs: &[],
193✔
72
        }
193✔
73
    }
193✔
74

75
    /// Create a new function builder for an abstract function that can be used
76
    /// on an abstract class or an interface.
77
    ///
78
    /// # Parameters
79
    ///
80
    /// * `name` - The name of the function.
81
    pub fn new_abstract<T: Into<String>>(name: T) -> Self {
×
82
        Self {
×
83
            name: name.into(),
×
84
            function: FunctionEntry {
×
85
                fname: ptr::null(),
×
86
                handler: None,
×
87
                arg_info: ptr::null(),
×
88
                num_args: 0,
×
89
                flags: MethodFlags::Abstract.bits(),
×
90
                #[cfg(php84)]
×
91
                doc_comment: ptr::null(),
×
92
                #[cfg(php84)]
×
93
                frameless_function_infos: ptr::null(),
×
94
            },
×
95
            args: vec![],
×
96
            n_req: None,
×
97
            retval: None,
×
98
            ret_as_ref: false,
×
99
            ret_as_null: false,
×
100
            docs: &[],
×
101
        }
×
102
    }
×
103

104
    /// Creates a constructor builder, used to build the constructor
105
    /// for classes.
106
    ///
107
    /// # Parameters
108
    ///
109
    /// * `handler` - The handler to be called when the function is invoked from
110
    ///   PHP.
111
    pub fn constructor(handler: FunctionHandler) -> Self {
×
112
        Self::new("__construct", handler)
×
113
    }
×
114

115
    /// Adds an argument to the function.
116
    ///
117
    /// # Parameters
118
    ///
119
    /// * `arg` - The argument to add to the function.
120
    pub fn arg(mut self, arg: Arg<'a>) -> Self {
110✔
121
        self.args.push(arg);
110✔
122
        self
110✔
123
    }
110✔
124

125
    /// Sets the rest of the given arguments as not required.
126
    pub fn not_required(mut self) -> Self {
138✔
127
        self.n_req = Some(self.args.len());
138✔
128
        self
138✔
129
    }
138✔
130

131
    /// Sets the return value of the function.
132
    ///
133
    /// Accepts a [`DataType`] for the simple case (via [`From<DataType> for
134
    /// PhpType`]) or a full [`PhpType`] for compound forms such as
135
    /// [`PhpType::Union`].
136
    ///
137
    /// # Parameters
138
    ///
139
    /// * `ty` - The return type of the function.
140
    /// * `as_ref` - Whether the function returns a reference.
141
    /// * `allow_null` - Whether the function return value is nullable.
142
    pub fn returns<T: Into<PhpType>>(mut self, ty: T, as_ref: bool, allow_null: bool) -> Self {
188✔
143
        let ty = ty.into();
188✔
144
        // PHP rejects `?void` and `?mixed`, so the nullable flag is squashed
145
        // for those single-type returns. Unions never resolve to those types
146
        // syntactically, so the user's `allow_null` is honoured directly.
147
        self.ret_as_null = match &ty {
188✔
148
            PhpType::Simple(dt) => allow_null && *dt != DataType::Void && *dt != DataType::Mixed,
159✔
149
            PhpType::Union(_)
150
            | PhpType::ClassUnion(_)
151
            | PhpType::Intersection(_)
152
            | PhpType::Dnf(_) => allow_null,
29✔
153
        };
154
        self.retval = Some(ty);
188✔
155
        self.ret_as_ref = as_ref;
188✔
156
        self
188✔
157
    }
188✔
158

159
    /// Sets the documentation for the function.
160
    /// This is used to generate the PHP stubs for the function.
161
    ///
162
    /// # Parameters
163
    ///
164
    /// * `docs` - The documentation for the function.
165
    pub fn docs(mut self, docs: DocComments) -> Self {
60✔
166
        self.docs = docs;
60✔
167
        self
60✔
168
    }
60✔
169

170
    /// Builds the function converting it into a Zend function entry.
171
    ///
172
    /// Returns a result containing the function entry if successful.
173
    ///
174
    /// # Errors
175
    ///
176
    /// * `Error::InvalidCString` - If the function name is not a valid C
177
    ///   string.
178
    /// * `Error::IntegerOverflow` - If the number of arguments is too large.
179
    /// * If arg info for an argument could not be created.
180
    /// * If the function name contains NUL bytes.
181
    pub fn build(mut self) -> Result<FunctionEntry> {
162✔
182
        let mut args = Vec::with_capacity(self.args.len() + 1);
162✔
183
        let mut n_req = self.n_req.unwrap_or(self.args.len());
162✔
184
        let variadic = self.args.last().is_some_and(|arg| arg.variadic);
162✔
185

186
        if variadic {
162✔
187
            self.function.flags |= MethodFlags::Variadic.bits();
10✔
188
            n_req = n_req.saturating_sub(1);
10✔
189
        }
152✔
190

191
        // argument header, retval etc
192
        // The first argument is used as `zend_internal_function_info` for the function.
193
        // That struct shares the same memory as `zend_internal_arg_info` which is used
194
        // for the arguments.
195
        args.push(ArgInfo {
162✔
196
            // required_num_args
197
            name: n_req as *const _,
162✔
198
            type_: match &self.retval {
161✔
199
                Some(PhpType::Simple(dt)) => {
142✔
200
                    ZendType::empty_from_type(*dt, self.ret_as_ref, false, self.ret_as_null)
142✔
201
                        .ok_or(Error::InvalidCString)?
142✔
202
                }
203
                Some(PhpType::Union(types)) => ZendType::empty_from_primitive_union(
4✔
204
                    types,
4✔
205
                    self.ret_as_ref,
4✔
206
                    false,
207
                    self.ret_as_null,
4✔
208
                )
209
                .ok_or(Error::InvalidCString)?,
4✔
210
                Some(PhpType::ClassUnion(class_names)) => ZendType::empty_from_class_union(
5✔
211
                    class_names,
5✔
212
                    self.ret_as_ref,
5✔
213
                    false,
214
                    self.ret_as_null,
5✔
215
                )
216
                .ok_or(Error::InvalidCString)?,
5✔
217
                #[cfg(php83)]
218
                Some(PhpType::Intersection(class_names)) => {
4✔
219
                    ZendType::empty_from_class_intersection(
4✔
220
                        class_names,
4✔
221
                        self.ret_as_ref,
4✔
222
                        false,
223
                        self.ret_as_null,
4✔
224
                    )
225
                    .ok_or(Error::InvalidCString)?
4✔
226
                }
227
                #[cfg(not(php83))]
228
                Some(PhpType::Intersection(_)) => return Err(Error::InvalidCString),
229
                #[cfg(php83)]
230
                Some(PhpType::Dnf(terms)) => {
6✔
231
                    ZendType::empty_from_dnf(terms, self.ret_as_ref, false, self.ret_as_null)
6✔
232
                        .ok_or(Error::InvalidCString)?
6✔
233
                }
234
                #[cfg(not(php83))]
235
                Some(PhpType::Dnf(_)) => return Err(Error::InvalidCString),
236
                None => ZendType::empty(false, false),
1✔
237
            },
238
            default_value: ptr::null(),
160✔
239
        });
240

241
        // arguments
242
        args.extend(
160✔
243
            self.args
160✔
244
                .iter()
160✔
245
                .map(Arg::as_arg_info)
160✔
246
                .collect::<Result<Vec<_>>>()?,
160✔
247
        );
248

249
        self.function.fname = CString::new(self.name)?.into_raw();
160✔
250
        self.function.num_args = (args.len() - 1).try_into()?;
160✔
251
        self.function.arg_info = Box::into_raw(args.into_boxed_slice()) as *const ArgInfo;
160✔
252

253
        Ok(self.function)
160✔
254
    }
162✔
255
}
256

257
#[cfg(test)]
258
mod tests {
259
    #![allow(clippy::unwrap_used)]
260
    use super::*;
261

262
    #[cfg(php83)]
NEW
263
    extern "C" fn noop_handler(_: &mut ExecuteData, _: &mut Zval) {}
×
264

265
    #[test]
266
    #[cfg(php83)]
267
    fn returns_class_union_emits_literal_name_on_retval_arg_info() {
1✔
268
        use crate::ffi::_ZEND_TYPE_LITERAL_NAME_BIT;
269
        use std::ffi::CStr;
270

271
        let entry = FunctionBuilder::new("ret_class_union", noop_handler)
1✔
272
            .returns(
1✔
273
                PhpType::ClassUnion(vec!["Foo".to_owned(), "Bar".to_owned()]),
1✔
274
                false,
275
                false,
276
            )
277
            .build()
1✔
278
            .expect("class union return should build");
1✔
279

280
        // arg_info[0] is the retval slot (zend_internal_function_info).
281
        let retval_info = unsafe { &*entry.arg_info };
1✔
282
        assert_ne!(retval_info.type_.type_mask & _ZEND_TYPE_LITERAL_NAME_BIT, 0,);
1✔
283
        assert!(!retval_info.type_.ptr.is_null());
1✔
284
        let class_str = unsafe { CStr::from_ptr(retval_info.type_.ptr.cast()) };
1✔
285
        assert_eq!(class_str.to_str().unwrap(), "Foo|Bar");
1✔
286
    }
1✔
287

288
    #[test]
289
    #[cfg(php83)]
290
    fn returns_class_union_with_allow_null_propagates_nullable_bit() {
1✔
291
        use crate::ffi::_ZEND_TYPE_NULLABLE_BIT;
292

293
        let entry = FunctionBuilder::new("ret_nullable_class_union", noop_handler)
1✔
294
            .returns(
1✔
295
                PhpType::ClassUnion(vec!["Foo".to_owned(), "Bar".to_owned()]),
1✔
296
                false,
297
                true,
298
            )
299
            .build()
1✔
300
            .expect("nullable class union return should build");
1✔
301

302
        let retval_info = unsafe { &*entry.arg_info };
1✔
303
        assert_ne!(retval_info.type_.type_mask & _ZEND_TYPE_NULLABLE_BIT, 0);
1✔
304
    }
1✔
305

306
    #[test]
307
    #[cfg(php83)]
308
    fn returns_intersection_emits_list_with_intersection_bit_on_retval() {
1✔
309
        use crate::ffi::{_ZEND_TYPE_INTERSECTION_BIT, _ZEND_TYPE_LIST_BIT};
310

311
        let entry = FunctionBuilder::new("ret_intersection", noop_handler)
1✔
312
            .returns(
1✔
313
                PhpType::Intersection(vec!["Countable".to_owned(), "Traversable".to_owned()]),
1✔
314
                false,
315
                false,
316
            )
317
            .build()
1✔
318
            .expect("intersection return should build");
1✔
319

320
        let retval_info = unsafe { &*entry.arg_info };
1✔
321
        assert_ne!(retval_info.type_.type_mask & _ZEND_TYPE_LIST_BIT, 0);
1✔
322
        assert_ne!(retval_info.type_.type_mask & _ZEND_TYPE_INTERSECTION_BIT, 0);
1✔
323
        assert!(!retval_info.type_.ptr.is_null());
1✔
324
    }
1✔
325

326
    #[test]
327
    #[cfg(php83)]
328
    fn returns_intersection_with_allow_null_errors() {
1✔
329
        let result = FunctionBuilder::new("ret_nullable_intersection", noop_handler)
1✔
330
            .returns(
1✔
331
                PhpType::Intersection(vec!["Foo".to_owned(), "Bar".to_owned()]),
1✔
332
                false,
333
                true,
334
            )
335
            .build();
1✔
336

337
        assert!(
1✔
338
            result.is_err(),
1✔
339
            "nullable intersection retval must error: nullable form is the DNF (Foo&Bar)|null"
340
        );
341
    }
1✔
342

343
    #[test]
344
    #[cfg(php83)]
345
    fn returns_dnf_emits_outer_list_with_union_bit_on_retval() {
1✔
346
        use crate::ffi::{_ZEND_TYPE_LIST_BIT, _ZEND_TYPE_UNION_BIT};
347
        use crate::types::DnfTerm;
348

349
        let entry = FunctionBuilder::new("ret_dnf", noop_handler)
1✔
350
            .returns(
1✔
351
                PhpType::Dnf(vec![
1✔
352
                    DnfTerm::Intersection(vec!["A".to_owned(), "B".to_owned()]),
1✔
353
                    DnfTerm::Single("C".to_owned()),
1✔
354
                ]),
1✔
355
                false,
356
                false,
357
            )
358
            .build()
1✔
359
            .expect("DNF return should build");
1✔
360

361
        let retval_info = unsafe { &*entry.arg_info };
1✔
362
        assert_ne!(retval_info.type_.type_mask & _ZEND_TYPE_LIST_BIT, 0);
1✔
363
        assert_ne!(retval_info.type_.type_mask & _ZEND_TYPE_UNION_BIT, 0);
1✔
364
        assert!(!retval_info.type_.ptr.is_null());
1✔
365
    }
1✔
366

367
    #[test]
368
    #[cfg(php83)]
369
    fn returns_dnf_with_allow_null_propagates_nullable_bit() {
1✔
370
        use crate::ffi::_ZEND_TYPE_NULLABLE_BIT;
371
        use crate::types::DnfTerm;
372

373
        let entry = FunctionBuilder::new("ret_nullable_dnf", noop_handler)
1✔
374
            .returns(
1✔
375
                PhpType::Dnf(vec![
1✔
376
                    DnfTerm::Intersection(vec!["A".to_owned(), "B".to_owned()]),
1✔
377
                    DnfTerm::Single("C".to_owned()),
1✔
378
                ]),
1✔
379
                false,
380
                true,
381
            )
382
            .build()
1✔
383
            .expect("nullable DNF return should build");
1✔
384

385
        let retval_info = unsafe { &*entry.arg_info };
1✔
386
        assert_ne!(retval_info.type_.type_mask & _ZEND_TYPE_NULLABLE_BIT, 0);
1✔
387
    }
1✔
388

389
    #[test]
390
    #[cfg(php83)]
391
    fn returns_empty_dnf_errors() {
1✔
392
        let result = FunctionBuilder::new("ret_empty_dnf", noop_handler)
1✔
393
            .returns(PhpType::Dnf(vec![]), false, false)
1✔
394
            .build();
1✔
395
        assert!(result.is_err());
1✔
396
    }
1✔
397
}
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

© 2026 Coveralls, Inc