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

extphprs / ext-php-rs / 26832618561

02 Jun 2026 04:11PM UTC coverage: 72.397% (-0.08%) from 72.479%
26832618561

push

github

ptondereau
fix(tests): wrap PHP function handlers in zend_fastcall! for windows

FunctionHandler resolves to extern "C" on unix and to
extern "vectorcall" on windows. The new compound-type integration
handlers (class union, intersection, DNF, primitive union) and the
src/builders/function.rs noop_handler test helper were declared
as plain extern "C", which only matches the unix alias and so
failed to type-check on windows with E0308.

Wrap each handler in zend_fastcall! { ... }, the same macro
closure.rs and builders/class.rs already use. The macro rewrites
the ABI to vectorcall on windows and stays C on unix.

Also gates the noop_handler helper behind cfg(php83) since the
tests that consume it are all PHP 8.3+ (class union, intersection,
DNF return types).

Drops a redundant `#![cfg_attr(windows, feature(abi_vectorcall))]`
attribute inside src/describe/mod.rs's tests module: inner
`feature` attributes only take effect at the crate root, so it was
a no-op that produced "the `#![feature]` attribute can only be
used at the crate root" on every windows compile. The crate root
already enables the feature in src/lib.rs.

0 of 1 new or added line in 1 file covered. (0.0%)

257 existing lines in 10 files now uncovered.

11564 of 15973 relevant lines covered (72.4%)

33.46 hits per line

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

49.38
/src/types/string/mod.rs
1
//! Represents a string in the PHP world. Similar to a C string, but is
2
//! reference counted and contains the length of the string.
3

4
#[cfg(feature = "smartstring")]
5
mod smartstring_impl;
6

7
use std::{
8
    borrow::Cow,
9
    cmp::Ordering,
10
    convert::TryFrom,
11
    ffi::{CStr, CString},
12
    fmt::Debug,
13
    hash::{Hash, Hasher},
14
    ptr, slice,
15
};
16

17
use parking_lot::{Mutex, const_mutex};
18

19
use crate::{
20
    boxed::{ZBox, ZBoxable},
21
    convert::{FromZval, IntoZval},
22
    error::{Error, Result},
23
    ffi::{
24
        ext_php_rs_is_known_valid_utf8, ext_php_rs_set_known_valid_utf8,
25
        ext_php_rs_zend_string_init, ext_php_rs_zend_string_release, zend_string,
26
        zend_string_init_interned,
27
    },
28
    flags::DataType,
29
    types::Zval,
30
};
31

32
/// A borrowed Zend string.
33
///
34
/// Although this object does implement [`Sized`], it is in fact not sized. As C
35
/// cannot represent unsized types, an array of size 1 is used at the end of the
36
/// type to represent the contents of the string, therefore this type is
37
/// actually unsized. All constructors return [`ZBox<ZendStr>`], the owned
38
/// variant.
39
///
40
/// Once the `ptr_metadata` feature lands in stable rust, this type can
41
/// potentially be changed to a DST using slices and metadata. See the tracking issue here: <https://github.com/rust-lang/rust/issues/81513>
42
pub type ZendStr = zend_string;
43

44
// Adding to the Zend interned string hashtable is not atomic and can be
45
// contested when PHP is compiled with ZTS, so an empty mutex is used to ensure
46
// no collisions occur on the Rust side. Not much we can do about collisions
47
// on the PHP side, but some safety is better than none.
48
static INTERNED_LOCK: Mutex<()> = const_mutex(());
49

50
// Clippy complains about there being no `is_empty` function when implementing
51
// on the alias `ZendStr` :( <https://github.com/rust-lang/rust-clippy/issues/7702>
52
#[allow(clippy::len_without_is_empty)]
53
impl ZendStr {
54
    /// Creates a new Zend string from a slice of bytes.
55
    ///
56
    /// # Parameters
57
    ///
58
    /// * `str` - String content.
59
    /// * `persistent` - Whether the string should persist through the request
60
    ///   boundary.
61
    ///
62
    /// # Panics
63
    ///
64
    /// Panics if the function was unable to allocate memory for the Zend
65
    /// string.
66
    ///
67
    /// # Safety
68
    ///
69
    /// When passing `persistent` as `false`, the caller must ensure that the
70
    /// object does not attempt to live after the request finishes. When a
71
    /// request starts and finishes in PHP, the Zend heap is deallocated and a
72
    /// new one is created, which would leave a dangling pointer in the
73
    /// [`ZBox`].
74
    ///
75
    /// # Example
76
    ///
77
    /// ```no_run
78
    /// use ext_php_rs::types::ZendStr;
79
    ///
80
    /// let s = ZendStr::new("Hello, world!", false);
81
    /// let php = ZendStr::new([80, 72, 80], false);
82
    /// ```
83
    pub fn new(str: impl AsRef<[u8]>, persistent: bool) -> ZBox<Self> {
458✔
84
        let s = str.as_ref();
458✔
85
        unsafe {
86
            let ptr = ext_php_rs_zend_string_init(s.as_ptr().cast(), s.len(), persistent)
458✔
87
                .as_mut()
458✔
88
                .expect("Failed to allocate memory for new Zend string");
458✔
89
            ZBox::from_raw(ptr)
458✔
90
        }
91
    }
458✔
92

93
    /// Creates a new Zend string from a [`CStr`].
94
    ///
95
    /// # Parameters
96
    ///
97
    /// * `str` - String content.
98
    /// * `persistent` - Whether the string should persist through the request
99
    ///   boundary.
100
    ///
101
    /// # Panics
102
    ///
103
    /// Panics if the function was unable to allocate memory for the Zend
104
    /// string.
105
    ///
106
    /// # Safety
107
    ///
108
    /// When passing `persistent` as `false`, the caller must ensure that the
109
    /// object does not attempt to live after the request finishes. When a
110
    /// request starts and finishes in PHP, the Zend heap is deallocated and a
111
    /// new one is created, which would leave a dangling pointer in the
112
    /// [`ZBox`].
113
    ///
114
    /// # Example
115
    ///
116
    /// ```no_run
117
    /// use ext_php_rs::types::ZendStr;
118
    /// use std::ffi::CString;
119
    ///
120
    /// let c_s = CString::new("Hello world!").unwrap();
121
    /// let s = ZendStr::from_c_str(&c_s, false);
122
    /// ```
123
    #[must_use]
124
    pub fn from_c_str(str: &CStr, persistent: bool) -> ZBox<Self> {
×
125
        unsafe {
UNCOV
126
            let ptr =
×
127
                ext_php_rs_zend_string_init(str.as_ptr(), str.to_bytes().len() as _, persistent);
×
128

129
            ZBox::from_raw(
×
UNCOV
130
                ptr.as_mut()
×
UNCOV
131
                    .expect("Failed to allocate memory for new Zend string"),
×
132
            )
133
        }
UNCOV
134
    }
×
135

136
    /// Creates a new interned Zend string from a slice of bytes.
137
    ///
138
    /// An interned string is only ever stored once and is immutable. PHP stores
139
    /// the string in an internal hashtable which stores the interned
140
    /// strings.
141
    ///
142
    /// As Zend hashtables are not thread-safe, a mutex is used to prevent two
143
    /// interned strings from being created at the same time.
144
    ///
145
    /// Interned strings are not used very often. You should almost always use a
146
    /// regular zend string, except in the case that you know you will use a
147
    /// string that PHP will already have interned, such as "PHP".
148
    ///
149
    /// # Parameters
150
    ///
151
    /// * `str` - String content.
152
    /// * `persistent` - Whether the string should persist through the request
153
    ///   boundary.
154
    ///
155
    /// # Panics
156
    ///
157
    /// Panics under the following circumstances:
158
    ///
159
    /// * The function used to create interned strings has not been set.
160
    /// * The function could not allocate enough memory for the Zend string.
161
    ///
162
    /// # Safety
163
    ///
164
    /// When passing `persistent` as `false`, the caller must ensure that the
165
    /// object does not attempt to live after the request finishes. When a
166
    /// request starts and finishes in PHP, the Zend heap is deallocated and a
167
    /// new one is created, which would leave a dangling pointer in the
168
    /// [`ZBox`].
169
    ///
170
    /// # Example
171
    ///
172
    /// ```no_run
173
    /// use ext_php_rs::types::ZendStr;
174
    ///
175
    /// let s = ZendStr::new_interned("PHP", true);
176
    /// ```
177
    pub fn new_interned(str: impl AsRef<[u8]>, persistent: bool) -> ZBox<Self> {
1✔
178
        let _lock = INTERNED_LOCK.lock();
1✔
179
        let s = str.as_ref();
1✔
180
        unsafe {
181
            let init = zend_string_init_interned.expect("`zend_string_init_interned` not ready");
1✔
182
            let ptr = init(s.as_ptr().cast(), s.len() as _, persistent)
1✔
183
                .as_mut()
1✔
184
                .expect("Failed to allocate memory for new Zend string");
1✔
185
            ZBox::from_raw(ptr)
1✔
186
        }
187
    }
1✔
188

189
    /// Creates a new interned Zend string from a [`CStr`].
190
    ///
191
    /// An interned string is only ever stored once and is immutable. PHP stores
192
    /// the string in an internal hashtable which stores the interned
193
    /// strings.
194
    ///
195
    /// As Zend hashtables are not thread-safe, a mutex is used to prevent two
196
    /// interned strings from being created at the same time.
197
    ///
198
    /// Interned strings are not used very often. You should almost always use a
199
    /// regular zend string, except in the case that you know you will use a
200
    /// string that PHP will already have interned, such as "PHP".
201
    ///
202
    /// # Parameters
203
    ///
204
    /// * `str` - String content.
205
    /// * `persistent` - Whether the string should persist through the request
206
    ///   boundary.
207
    ///
208
    /// # Panics
209
    ///
210
    /// Panics under the following circumstances:
211
    ///
212
    /// * The function used to create interned strings has not been set.
213
    /// * The function could not allocate enough memory for the Zend string.
214
    ///
215
    /// # Safety
216
    ///
217
    /// When passing `persistent` as `false`, the caller must ensure that the
218
    /// object does not attempt to live after the request finishes. When a
219
    /// request starts and finishes in PHP, the Zend heap is deallocated and a
220
    /// new one is created, which would leave a dangling pointer in the
221
    /// [`ZBox`].
222
    ///
223
    /// # Example
224
    ///
225
    /// ```no_run
226
    /// use ext_php_rs::types::ZendStr;
227
    /// use std::ffi::CString;
228
    ///
229
    /// let c_s = CString::new("PHP").unwrap();
230
    /// let s = ZendStr::interned_from_c_str(&c_s, true);
231
    /// ```
UNCOV
232
    pub fn interned_from_c_str(str: &CStr, persistent: bool) -> ZBox<Self> {
×
UNCOV
233
        let _lock = INTERNED_LOCK.lock();
×
234

235
        unsafe {
UNCOV
236
            let init = zend_string_init_interned.expect("`zend_string_init_interned` not ready");
×
237
            let ptr = init(str.as_ptr(), str.to_bytes().len() as _, persistent);
×
238

239
            ZBox::from_raw(
×
UNCOV
240
                ptr.as_mut()
×
UNCOV
241
                    .expect("Failed to allocate memory for new Zend string"),
×
242
            )
243
        }
UNCOV
244
    }
×
245

246
    /// Returns the length of the string.
247
    ///
248
    /// # Example
249
    ///
250
    /// ```no_run
251
    /// use ext_php_rs::types::ZendStr;
252
    ///
253
    /// let s = ZendStr::new("hello, world!", false);
254
    /// assert_eq!(s.len(), 13);
255
    /// ```
256
    #[must_use]
257
    pub fn len(&self) -> usize {
328✔
258
        self.len
328✔
259
    }
328✔
260

261
    /// Returns true if the string is empty, false otherwise.
262
    ///
263
    /// # Example
264
    ///
265
    /// ```no_run
266
    /// use ext_php_rs::types::ZendStr;
267
    ///
268
    /// let s = ZendStr::new("hello, world!", false);
269
    /// assert_eq!(s.is_empty(), false);
270
    /// ```
271
    #[must_use]
272
    pub fn is_empty(&self) -> bool {
×
UNCOV
273
        self.len() == 0
×
UNCOV
274
    }
×
275

276
    /// Attempts to return a reference to the underlying bytes inside the Zend
277
    /// string as a [`CStr`].
278
    ///
279
    /// # Errors
280
    ///
281
    /// Returns an [`Error::InvalidCString`] variant if the string contains null
282
    /// bytes.
283
    pub fn as_c_str(&self) -> Result<&CStr> {
×
284
        let bytes_with_null =
×
285
            unsafe { slice::from_raw_parts(self.val.as_ptr().cast(), self.len() + 1) };
×
UNCOV
286
        CStr::from_bytes_with_nul(bytes_with_null).map_err(|_| Error::InvalidCString)
×
UNCOV
287
    }
×
288

289
    /// Attempts to return a reference to the underlying bytes inside the Zend
290
    /// string.
291
    ///
292
    /// # Errors
293
    ///
294
    /// Returns an [`Error::InvalidUtf8`] variant if the [`str`] contains
295
    /// non-UTF-8 characters.
296
    ///
297
    /// # Example
298
    ///
299
    /// ```no_run
300
    /// use ext_php_rs::types::ZendStr;
301
    ///
302
    /// let s = ZendStr::new("hello, world!", false);
303
    /// assert!(s.as_str().is_ok());
304
    /// ```
305
    #[inline]
306
    pub fn as_str(&self) -> Result<&str> {
314✔
307
        if unsafe { ext_php_rs_is_known_valid_utf8(self.as_ptr()) } {
314✔
308
            let str = unsafe { std::str::from_utf8_unchecked(self.as_bytes()) };
41✔
309
            return Ok(str);
41✔
310
        }
273✔
311
        let str = std::str::from_utf8(self.as_bytes()).map_err(|_| Error::InvalidUtf8)?;
273✔
312
        unsafe { ext_php_rs_set_known_valid_utf8(self.as_ptr().cast_mut()) };
273✔
313
        Ok(str)
273✔
314
    }
314✔
315

316
    /// Returns a reference to the underlying bytes inside the Zend string.
317
    #[must_use]
318
    pub fn as_bytes(&self) -> &[u8] {
321✔
319
        unsafe { slice::from_raw_parts(self.val.as_ptr().cast(), self.len()) }
321✔
320
    }
321✔
321

322
    /// Returns a raw pointer to this object
323
    #[must_use]
324
    pub fn as_ptr(&self) -> *const ZendStr {
802✔
325
        ptr::from_ref(self)
802✔
326
    }
802✔
327

328
    /// Returns a mutable pointer to this object
329
    pub fn as_mut_ptr(&mut self) -> *mut ZendStr {
×
UNCOV
330
        ptr::from_mut(self)
×
UNCOV
331
    }
×
332
}
333

334
unsafe impl ZBoxable for ZendStr {
335
    fn free(&mut self) {
215✔
336
        unsafe { ext_php_rs_zend_string_release(self) };
215✔
337
    }
215✔
338
}
339

340
impl Debug for ZendStr {
341
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
×
UNCOV
342
        self.as_str().fmt(f)
×
UNCOV
343
    }
×
344
}
345

346
impl AsRef<[u8]> for ZendStr {
347
    fn as_ref(&self) -> &[u8] {
×
UNCOV
348
        self.as_bytes()
×
UNCOV
349
    }
×
350
}
351

352
impl<T> PartialEq<T> for ZendStr
353
where
354
    T: AsRef<[u8]>,
355
{
356
    fn eq(&self, other: &T) -> bool {
×
UNCOV
357
        self.as_ref() == other.as_ref()
×
UNCOV
358
    }
×
359
}
360

361
impl Eq for ZendStr {}
362

363
impl PartialOrd for ZendStr {
364
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
×
UNCOV
365
        Some(self.cmp(other))
×
UNCOV
366
    }
×
367
}
368

369
impl Ord for ZendStr {
370
    fn cmp(&self, other: &Self) -> Ordering {
×
371
        self.as_ref().cmp(other.as_ref())
×
372
    }
×
373
}
374

375
impl Hash for ZendStr {
UNCOV
376
    fn hash<H: Hasher>(&self, state: &mut H) {
×
UNCOV
377
        self.as_bytes().hash(state);
×
378
    }
×
379
}
380

381
impl ToOwned for ZendStr {
382
    type Owned = ZBox<ZendStr>;
383

UNCOV
384
    fn to_owned(&self) -> Self::Owned {
×
UNCOV
385
        Self::new(self.as_bytes(), false)
×
UNCOV
386
    }
×
387
}
388

389
impl<'a> TryFrom<&'a ZendStr> for &'a CStr {
390
    type Error = Error;
391

392
    fn try_from(value: &'a ZendStr) -> Result<Self> {
×
393
        value.as_c_str()
×
394
    }
×
395
}
396

397
impl<'a> TryFrom<&'a ZendStr> for &'a str {
398
    type Error = Error;
399

400
    fn try_from(value: &'a ZendStr) -> Result<Self> {
×
UNCOV
401
        value.as_str()
×
UNCOV
402
    }
×
403
}
404

405
impl TryFrom<&ZendStr> for String {
406
    type Error = Error;
407

408
    fn try_from(value: &ZendStr) -> Result<Self> {
1✔
409
        value.as_str().map(ToString::to_string)
1✔
410
    }
1✔
411
}
412

413
impl<'a> From<&'a ZendStr> for Cow<'a, ZendStr> {
UNCOV
414
    fn from(value: &'a ZendStr) -> Self {
×
UNCOV
415
        Cow::Borrowed(value)
×
416
    }
×
417
}
418

419
impl From<&CStr> for ZBox<ZendStr> {
UNCOV
420
    fn from(value: &CStr) -> Self {
×
UNCOV
421
        ZendStr::from_c_str(value, false)
×
422
    }
×
423
}
424

425
impl From<CString> for ZBox<ZendStr> {
UNCOV
426
    fn from(value: CString) -> Self {
×
UNCOV
427
        ZendStr::from_c_str(&value, false)
×
428
    }
×
429
}
430

431
impl From<&str> for ZBox<ZendStr> {
UNCOV
432
    fn from(value: &str) -> Self {
×
UNCOV
433
        ZendStr::new(value.as_bytes(), false)
×
UNCOV
434
    }
×
435
}
436

437
impl From<String> for ZBox<ZendStr> {
438
    fn from(value: String) -> Self {
×
439
        ZendStr::new(value.as_str(), false)
×
440
    }
×
441
}
442

443
impl From<ZBox<ZendStr>> for Cow<'_, ZendStr> {
UNCOV
444
    fn from(value: ZBox<ZendStr>) -> Self {
×
UNCOV
445
        Cow::Owned(value)
×
UNCOV
446
    }
×
447
}
448

449
impl From<Cow<'_, ZendStr>> for ZBox<ZendStr> {
UNCOV
450
    fn from(value: Cow<'_, ZendStr>) -> Self {
×
UNCOV
451
        value.into_owned()
×
UNCOV
452
    }
×
453
}
454

455
macro_rules! try_into_zval_str {
456
    ($type: ty) => {
457
        impl TryFrom<$type> for Zval {
458
            type Error = Error;
459

UNCOV
460
            fn try_from(value: $type) -> Result<Self> {
×
UNCOV
461
                let mut zv = Self::new();
×
UNCOV
462
                zv.set_string(&value, false)?;
×
463
                Ok(zv)
×
464
            }
×
465
        }
466

467
        impl IntoZval for $type {
468
            const TYPE: DataType = DataType::String;
469
            const NULLABLE: bool = false;
470

471
            fn set_zval(self, zv: &mut Zval, persistent: bool) -> Result<()> {
202✔
472
                zv.set_string(&self, persistent)
202✔
473
            }
202✔
474
        }
475
    };
476
}
477

478
try_into_zval_str!(String);
479
try_into_zval_str!(&str);
480
try_from_zval!(String, string, String);
481

482
impl<'a> FromZval<'a> for &'a str {
483
    const TYPE: DataType = DataType::String;
484

UNCOV
485
    fn from_zval(zval: &'a Zval) -> Option<Self> {
×
UNCOV
486
        zval.str()
×
UNCOV
487
    }
×
488
}
489

490
#[cfg(test)]
491
#[cfg(feature = "embed")]
492
mod tests {
493
    use crate::embed::Embed;
494

495
    #[test]
496
    fn test_string() {
1✔
497
        Embed::run(|| {
1✔
498
            let result = Embed::eval("'foo';");
1✔
499

500
            assert!(result.is_ok());
1✔
501

502
            let zval = result.as_ref().expect("Unreachable");
1✔
503

504
            assert!(zval.is_string());
1✔
505
            assert_eq!(zval.string(), Some("foo".to_string()));
1✔
506
        });
1✔
507
    }
1✔
508

509
    #[test]
510
    fn test_zend_string_init_fast() {
1✔
511
        Embed::run(|| {
1✔
512
            let cases: &[(&[u8], usize, bool)] = &[
1✔
513
                // (input, expected_len, should_be_interned)
1✔
514
                (b"", 0, true),
1✔
515
                (b"a", 1, true),
1✔
516
                (b"x", 1, true),
1✔
517
                (b"\0", 1, true),
1✔
518
                (b"\xff", 1, true),
1✔
519
                (b"hello", 5, false),
1✔
520
                (b"ab", 2, false),
1✔
521
            ];
1✔
522

523
            for &(input, expected_len, interned) in cases {
7✔
524
                let s = crate::types::ZendStr::new(input, false);
7✔
525
                assert_eq!(s.len(), expected_len, "len mismatch for {input:?}");
7✔
526
                assert_eq!(s.as_bytes(), input, "content mismatch for {input:?}");
7✔
527

528
                if interned {
7✔
529
                    let s2 = crate::types::ZendStr::new(input, false);
5✔
530
                    assert!(
5✔
531
                        std::ptr::eq(s.as_ptr(), s2.as_ptr()),
5✔
532
                        "expected interned pointer for {input:?}"
533
                    );
534
                }
2✔
535
            }
536

537
            // All printable ASCII single-chars return interned pointers
538
            for c in b' '..=b'~' {
95✔
539
                let s1 = crate::types::ZendStr::new(&[c][..], false);
95✔
540
                let s2 = crate::types::ZendStr::new(&[c][..], false);
95✔
541
                assert!(
95✔
542
                    std::ptr::eq(s1.as_ptr(), s2.as_ptr()),
95✔
543
                    "expected interned pointer for single char {c:#x}"
544
                );
545
            }
546
        });
1✔
547
    }
1✔
548
}
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