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

extphprs / ext-php-rs / 25379640517

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

Pull #734

github

ptondereau
ci(build): skip cargo test on macOS until libphp is available

shivammathur/setup-php's Homebrew formulas (NTS php@x.y and
-debug-zts) do not ship a libphp shared library at
<php-config --prefix>/lib. With macos-latest now running macos-15
(ld-prime + chained fixups by default), the test binary aborts at
image load with "symbol not found in flat namespace" before any
test runs, since undefined PHP runtime data symbols can no longer
be deferred to runtime via -Wl,-undefined,dynamic_lookup.

The previous carve-out only excluded macOS + Rust nightly. Widen
it to all macOS variants. Build coverage on macOS still runs in
the step above. Restore the test step for macOS once a libphp is
wired into the runner (Homebrew --enable-embed build, or a custom
install step).
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