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

davidcole1340 / ext-php-rs / 18244744117

04 Oct 2025 01:08PM UTC coverage: 30.759% (+3.0%) from 27.728%
18244744117

Pull #533

github

web-flow
Merge 0936605ac into 113ef33a0
Pull Request #533: Feat/interface impl

68 of 152 new or added lines in 9 files covered. (44.74%)

3 existing lines in 2 files now uncovered.

1326 of 4311 relevant lines covered (30.76%)

7.67 hits per line

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

35.97
/src/builders/module.rs
1
use std::{convert::TryFrom, ffi::CString, mem, ptr};
2

3
use super::{ClassBuilder, FunctionBuilder};
4
#[cfg(feature = "enum")]
5
use crate::{builders::enum_builder::EnumBuilder, enum_::RegisteredEnum};
6
use crate::{
7
    class::RegisteredClass,
8
    constant::IntoConst,
9
    describe::DocComments,
10
    error::Result,
11
    ffi::{ext_php_rs_php_build_id, ZEND_MODULE_API_NO},
12
    flags::ClassFlags,
13
    zend::{FunctionEntry, ModuleEntry},
14
    PHP_DEBUG, PHP_ZTS,
15
};
16

17
/// Builds a Zend module extension to be registered with PHP. Must be called
18
/// from within an external function called `get_module`, returning a mutable
19
/// pointer to a `ModuleEntry`.
20
///
21
/// ```rust,no_run
22
/// use ext_php_rs::{
23
///     builders::ModuleBuilder,
24
///     zend::ModuleEntry,
25
///     info_table_start, info_table_end, info_table_row
26
/// };
27
///
28
/// #[no_mangle]
29
/// pub extern "C" fn php_module_info(_module: *mut ModuleEntry) {
30
///     info_table_start!();
31
///     info_table_row!("column 1", "column 2");
32
///     info_table_end!();
33
/// }
34
///
35
/// #[no_mangle]
36
/// pub extern "C" fn get_module() -> *mut ModuleEntry {
37
///     let (entry, _) = ModuleBuilder::new("ext-name", "ext-version")
38
///         .info_function(php_module_info)
39
///         .try_into()
40
///         .unwrap();
41
///     entry.into_raw()
42
/// }
43
/// ```
44
#[must_use]
45
#[derive(Debug, Default)]
46
pub struct ModuleBuilder<'a> {
47
    pub(crate) name: String,
48
    pub(crate) version: String,
49
    pub(crate) functions: Vec<FunctionBuilder<'a>>,
50
    pub(crate) constants: Vec<(String, Box<dyn IntoConst + Send>, DocComments)>,
51
    pub(crate) classes: Vec<fn() -> ClassBuilder>,
52
    pub(crate) interfaces: Vec<fn() -> ClassBuilder>,
53
    #[cfg(feature = "enum")]
54
    pub(crate) enums: Vec<fn() -> EnumBuilder>,
55
    startup_func: Option<StartupShutdownFunc>,
56
    shutdown_func: Option<StartupShutdownFunc>,
57
    request_startup_func: Option<StartupShutdownFunc>,
58
    request_shutdown_func: Option<StartupShutdownFunc>,
59
    post_deactivate_func: Option<unsafe extern "C" fn() -> i32>,
60
    info_func: Option<InfoFunc>,
61
}
62

63
impl ModuleBuilder<'_> {
64
    /// Creates a new module builder with a given name and version.
65
    ///
66
    /// # Arguments
67
    ///
68
    /// * `name` - The name of the extension.
69
    /// * `version` - The current version of the extension.
70
    pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
14✔
71
        Self {
72
            name: name.into(),
42✔
73
            version: version.into(),
42✔
74
            functions: vec![],
28✔
75
            constants: vec![],
28✔
76
            classes: vec![],
14✔
77
            ..Default::default()
78
        }
79
    }
80

81
    /// Overrides module name.
82
    ///
83
    /// # Arguments
84
    ///
85
    /// * `name` - The name of the extension.
86
    pub fn name(mut self, name: impl Into<String>) -> Self {
1✔
87
        self.name = name.into();
3✔
88
        self
1✔
89
    }
90

91
    /// Overrides module version.
92
    ///
93
    /// # Arguments
94
    ///
95
    /// * `version` - The current version of the extension.
96
    pub fn version(mut self, version: impl Into<String>) -> Self {
1✔
97
        self.version = version.into();
3✔
98
        self
1✔
99
    }
100

101
    /// Sets the startup function for the extension.
102
    ///
103
    /// # Arguments
104
    ///
105
    /// * `func` - The function to be called on startup.
106
    pub fn startup_function(mut self, func: StartupShutdownFunc) -> Self {
3✔
107
        self.startup_func = Some(func);
3✔
108
        self
3✔
109
    }
110

111
    /// Sets the shutdown function for the extension.
112
    ///
113
    /// # Arguments
114
    ///
115
    /// * `func` - The function to be called on shutdown.
116
    pub fn shutdown_function(mut self, func: StartupShutdownFunc) -> Self {
1✔
117
        self.shutdown_func = Some(func);
1✔
118
        self
1✔
119
    }
120

121
    /// Sets the request startup function for the extension.
122
    ///
123
    /// # Arguments
124
    ///
125
    /// * `func` - The function to be called when startup is requested.
126
    pub fn request_startup_function(mut self, func: StartupShutdownFunc) -> Self {
1✔
127
        self.request_startup_func = Some(func);
1✔
128
        self
1✔
129
    }
130

131
    /// Sets the request shutdown function for the extension.
132
    ///
133
    /// # Arguments
134
    ///
135
    /// * `func` - The function to be called when shutdown is requested.
136
    pub fn request_shutdown_function(mut self, func: StartupShutdownFunc) -> Self {
1✔
137
        self.request_shutdown_func = Some(func);
1✔
138
        self
1✔
139
    }
140

141
    /// Sets the post request shutdown function for the extension.
142
    ///
143
    /// This function can be useful if you need to do any final cleanup at the
144
    /// very end of a request, after all other resources have been released. For
145
    /// example, if your extension creates any persistent resources that last
146
    /// beyond a single request, you could use this function to clean those up.
147
    /// # Arguments
148
    ///
149
    /// * `func` - The function to be called when shutdown is requested.
150
    pub fn post_deactivate_function(mut self, func: unsafe extern "C" fn() -> i32) -> Self {
1✔
151
        self.post_deactivate_func = Some(func);
1✔
152
        self
1✔
153
    }
154

155
    /// Sets the extension information function for the extension.
156
    ///
157
    /// # Arguments
158
    ///
159
    /// * `func` - The function to be called to retrieve the information about
160
    ///   the extension.
161
    pub fn info_function(mut self, func: InfoFunc) -> Self {
1✔
162
        self.info_func = Some(func);
1✔
163
        self
1✔
164
    }
165

166
    /// Adds a function to the extension.
167
    ///
168
    /// # Arguments
169
    ///
170
    /// * `func` - The function to be added to the extension.
171
    pub fn function(mut self, func: FunctionBuilder<'static>) -> Self {
4✔
172
        self.functions.push(func);
12✔
173
        self
4✔
174
    }
175

176
    /// Adds a constant to the extension.
177
    ///
178
    /// # Arguments
179
    ///
180
    /// * `const` - Tuple containing the name, value and doc comments for the
181
    ///   constant. This is a tuple to support the [`wrap_constant`] macro.
182
    ///
183
    /// [`wrap_constant`]: crate::wrap_constant
184
    pub fn constant(
1✔
185
        mut self,
186
        r#const: (&str, impl IntoConst + Send + 'static, DocComments),
187
    ) -> Self {
188
        let (name, val, docs) = r#const;
4✔
189
        self.constants.push((
3✔
190
            name.into(),
3✔
191
            Box::new(val) as Box<dyn IntoConst + Send>,
2✔
192
            docs,
1✔
193
        ));
194
        self
1✔
195
    }
196

197
    /// Adds a interface to the extension.
198
    ///
199
    /// # Panics
200
    ///
201
    /// * Panics if a constant could not be registered.
NEW
202
    pub fn interface<T: RegisteredClass>(mut self) -> Self {
×
NEW
203
        self.interfaces.push(|| {
×
NEW
204
            let mut builder = ClassBuilder::new(T::CLASS_NAME);
×
NEW
205
            for (method, flags) in T::method_builders() {
×
NEW
206
                builder = builder.method(method, flags);
×
207
            }
NEW
208
            for interface in T::IMPLEMENTS {
×
NEW
209
                builder = builder.implements(*interface);
×
210
            }
NEW
211
            for (name, value, docs) in T::constants() {
×
NEW
212
                builder = builder
×
NEW
213
                    .dyn_constant(*name, *value, docs)
×
NEW
214
                    .expect("Failed to register constant");
×
215
            }
216

NEW
217
            if let Some(modifier) = T::BUILDER_MODIFIER {
×
NEW
218
                builder = modifier(builder);
×
219
            }
220

NEW
221
            builder = builder.flags(ClassFlags::Interface);
×
NEW
222
            builder
×
NEW
223
                .object_override::<T>()
×
NEW
224
                .registration(|ce| {
×
NEW
225
                    T::get_metadata().set_ce(ce);
×
226
                })
NEW
227
                .docs(T::DOC_COMMENTS)
×
228
        });
NEW
229
        self
×
230
    }
231

232
    /// Adds a class to the extension.
233
    ///
234
    /// # Panics
235
    ///
236
    /// * Panics if a constant could not be registered.
237
    pub fn class<T: RegisteredClass>(mut self) -> Self {
×
238
        self.classes.push(|| {
×
239
            let mut builder = ClassBuilder::new(T::CLASS_NAME);
×
240
            for (method, flags) in T::method_builders() {
×
241
                builder = builder.method(method, flags);
×
242
            }
243
            if let Some(parent) = T::EXTENDS {
×
244
                builder = builder.extends(parent);
×
245
            }
246
            for interface in T::IMPLEMENTS {
×
247
                builder = builder.implements(*interface);
×
248
            }
249
            for (name, value, docs) in T::constants() {
×
250
                builder = builder
×
251
                    .dyn_constant(*name, *value, docs)
×
252
                    .expect("Failed to register constant");
×
253
            }
254
            for (name, prop_info) in T::get_properties() {
×
255
                builder = builder.property(name, prop_info.flags, prop_info.docs);
×
256
            }
257
            if let Some(modifier) = T::BUILDER_MODIFIER {
×
258
                builder = modifier(builder);
×
259
            }
260

261
            builder
×
262
                .object_override::<T>()
×
263
                .registration(|ce| {
×
264
                    T::get_metadata().set_ce(ce);
×
265
                })
266
                .docs(T::DOC_COMMENTS)
×
267
        });
268
        self
×
269
    }
270

271
    /// Adds an enum to the extension.
272
    #[cfg(feature = "enum")]
273
    pub fn enumeration<T>(mut self) -> Self
×
274
    where
275
        T: RegisteredClass + RegisteredEnum,
276
    {
277
        self.enums.push(|| {
×
278
            let mut builder = EnumBuilder::new(T::CLASS_NAME);
×
279
            for case in T::CASES {
×
280
                builder = builder.case(case);
×
281
            }
282
            for (method, flags) in T::method_builders() {
×
283
                builder = builder.method(method, flags);
×
284
            }
285

286
            builder
×
287
                .registration(|ce| {
×
288
                    T::get_metadata().set_ce(ce);
×
289
                })
290
                .docs(T::DOC_COMMENTS)
×
291
        });
292

293
        self
×
294
    }
295
}
296

297
/// Artifacts from the [`ModuleBuilder`] that should be revisited inside the
298
/// extension startup function.
299
pub struct ModuleStartup {
300
    constants: Vec<(String, Box<dyn IntoConst + Send>)>,
301
    classes: Vec<fn() -> ClassBuilder>,
302
    interfaces: Vec<fn() -> ClassBuilder>,
303
    #[cfg(feature = "enum")]
304
    enums: Vec<fn() -> EnumBuilder>,
305
}
306

307
impl ModuleStartup {
308
    /// Completes startup of the module. Should only be called inside the module
309
    /// startup function.
310
    ///
311
    /// # Errors
312
    ///
313
    /// * Returns an error if a constant could not be registered.
314
    ///
315
    /// # Panics
316
    ///
317
    /// * Panics if a class could not be registered.
318
    pub fn startup(self, _ty: i32, mod_num: i32) -> Result<()> {
1✔
319
        for (name, val) in self.constants {
1✔
320
            val.register_constant(&name, mod_num)?;
×
321
        }
322

323
        self.classes.into_iter().map(|c| c()).for_each(|c| {
1✔
324
            c.register().expect("Failed to build class");
×
325
        });
326

NEW
327
        self.interfaces.into_iter().map(|c| c()).for_each(|c| {
×
NEW
328
            c.register().expect("Failed to build interface");
×
329
        });
330

331
        #[cfg(feature = "enum")]
332
        self.enums
×
333
            .into_iter()
334
            .map(|builder| builder())
×
335
            .for_each(|e| {
×
336
                e.register().expect("Failed to build enum");
×
337
            });
338

339
        Ok(())
×
340
    }
341
}
342

343
/// A function to be called when the extension is starting up or shutting down.
344
pub type StartupShutdownFunc = unsafe extern "C" fn(_type: i32, _module_number: i32) -> i32;
345

346
/// A function to be called when `phpinfo();` is called.
347
pub type InfoFunc = unsafe extern "C" fn(zend_module: *mut ModuleEntry);
348

349
/// Builds a [`ModuleEntry`] and [`ModuleStartup`] from a [`ModuleBuilder`].
350
/// This is the entry point for the module to be registered with PHP.
351
impl TryFrom<ModuleBuilder<'_>> for (ModuleEntry, ModuleStartup) {
352
    type Error = crate::error::Error;
353

354
    fn try_from(builder: ModuleBuilder) -> Result<Self, Self::Error> {
2✔
355
        let mut functions = builder
4✔
356
            .functions
2✔
357
            .into_iter()
358
            .map(FunctionBuilder::build)
2✔
359
            .collect::<Result<Vec<_>>>()?;
360
        functions.push(FunctionEntry::end());
×
361
        let functions = Box::into_raw(functions.into_boxed_slice()) as *const FunctionEntry;
×
362

363
        let name = CString::new(builder.name)?.into_raw();
2✔
364
        let version = CString::new(builder.version)?.into_raw();
2✔
365

366
        let startup = ModuleStartup {
367
            constants: builder
×
368
                .constants
369
                .into_iter()
370
                .map(|(n, v, _)| (n, v))
371
                .collect(),
372
            classes: builder.classes,
×
NEW
373
            interfaces: builder.interfaces,
×
374
            #[cfg(feature = "enum")]
375
            enums: builder.enums,
×
376
        };
377

378
        Ok((
379
            ModuleEntry {
380
                size: mem::size_of::<ModuleEntry>().try_into()?,
×
381
                zend_api: ZEND_MODULE_API_NO,
382
                zend_debug: u8::from(PHP_DEBUG),
2✔
383
                zts: u8::from(PHP_ZTS),
×
384
                ini_entry: ptr::null(),
×
385
                deps: ptr::null(),
×
386
                name,
×
387
                functions,
×
388
                module_startup_func: builder.startup_func,
×
389
                module_shutdown_func: builder.shutdown_func,
×
390
                request_startup_func: builder.request_startup_func,
×
391
                request_shutdown_func: builder.request_shutdown_func,
×
392
                info_func: builder.info_func,
×
393
                version,
×
394
                globals_size: 0,
395
                #[cfg(not(php_zts))]
396
                globals_ptr: ptr::null_mut(),
×
397
                #[cfg(php_zts)]
398
                globals_id_ptr: ptr::null_mut(),
399
                globals_ctor: None,
×
400
                globals_dtor: None,
×
401
                post_deactivate_func: builder.post_deactivate_func,
×
402
                module_started: 0,
403
                type_: 0,
404
                handle: ptr::null_mut(),
×
405
                module_number: 0,
406
                build_id: unsafe { ext_php_rs_php_build_id() },
×
407
            },
408
            startup,
×
409
        ))
410
    }
411
}
412

413
#[cfg(test)]
414
mod tests {
415
    use crate::test::{
416
        test_deactivate_function, test_function, test_info_function, test_startup_shutdown_function,
417
    };
418

419
    use super::*;
420

421
    #[test]
422
    fn test_new() {
423
        let builder = ModuleBuilder::new("test", "1.0");
424
        assert_eq!(builder.name, "test");
425
        assert_eq!(builder.version, "1.0");
426
        assert!(builder.functions.is_empty());
427
        assert!(builder.constants.is_empty());
428
        assert!(builder.classes.is_empty());
429
        assert!(builder.interfaces.is_empty());
430
        assert!(builder.startup_func.is_none());
431
        assert!(builder.shutdown_func.is_none());
432
        assert!(builder.request_startup_func.is_none());
433
        assert!(builder.request_shutdown_func.is_none());
434
        assert!(builder.post_deactivate_func.is_none());
435
        assert!(builder.info_func.is_none());
436
        #[cfg(feature = "enum")]
437
        assert!(builder.enums.is_empty());
438
    }
439

440
    #[test]
441
    fn test_name() {
442
        let builder = ModuleBuilder::new("test", "1.0").name("new_test");
443
        assert_eq!(builder.name, "new_test");
444
    }
445

446
    #[test]
447
    fn test_version() {
448
        let builder = ModuleBuilder::new("test", "1.0").version("2.0");
449
        assert_eq!(builder.version, "2.0");
450
    }
451

452
    #[test]
453
    fn test_startup_function() {
454
        let builder =
455
            ModuleBuilder::new("test", "1.0").startup_function(test_startup_shutdown_function);
456
        assert!(builder.startup_func.is_some());
457
    }
458

459
    #[test]
460
    fn test_shutdown_function() {
461
        let builder =
462
            ModuleBuilder::new("test", "1.0").shutdown_function(test_startup_shutdown_function);
463
        assert!(builder.shutdown_func.is_some());
464
    }
465

466
    #[test]
467
    fn test_request_startup_function() {
468
        let builder = ModuleBuilder::new("test", "1.0")
469
            .request_startup_function(test_startup_shutdown_function);
470
        assert!(builder.request_startup_func.is_some());
471
    }
472

473
    #[test]
474
    fn test_request_shutdown_function() {
475
        let builder = ModuleBuilder::new("test", "1.0")
476
            .request_shutdown_function(test_startup_shutdown_function);
477
        assert!(builder.request_shutdown_func.is_some());
478
    }
479

480
    #[test]
481
    fn test_set_post_deactivate_function() {
482
        let builder =
483
            ModuleBuilder::new("test", "1.0").post_deactivate_function(test_deactivate_function);
484
        assert!(builder.post_deactivate_func.is_some());
485
    }
486

487
    #[test]
488
    fn test_set_info_function() {
489
        let builder = ModuleBuilder::new("test", "1.0").info_function(test_info_function);
490
        assert!(builder.info_func.is_some());
491
    }
492

493
    #[test]
494
    fn test_add_function() {
495
        let builder =
496
            ModuleBuilder::new("test", "1.0").function(FunctionBuilder::new("test", test_function));
497
        assert_eq!(builder.functions.len(), 1);
498
    }
499

500
    #[test]
501
    #[cfg(feature = "embed")]
502
    fn test_add_constant() {
503
        let builder =
504
            ModuleBuilder::new("test", "1.0").constant(("TEST_CONST", 42, DocComments::default()));
505
        assert_eq!(builder.constants.len(), 1);
506
        assert_eq!(builder.constants[0].0, "TEST_CONST");
507
        // TODO: Check if the value is 42
508
        assert_eq!(builder.constants[0].2, DocComments::default());
509
    }
510
}
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