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

davidcole1340 / ext-php-rs / 16401695110

20 Jul 2025 04:00PM UTC coverage: 26.906% (-0.04%) from 26.945%
16401695110

Pull #534

github

web-flow
Merge 33ea1936f into 25f508fc8
Pull Request #534: feat(module): ModuleBuilder::set_name() and ModuleBuilder::set_version()

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

32 existing lines in 1 file now uncovered.

1115 of 4144 relevant lines covered (26.91%)

5.63 hits per line

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

37.93
/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
    zend::{FunctionEntry, ModuleEntry},
13
    PHP_DEBUG, PHP_ZTS,
14
};
15

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

61
impl ModuleBuilder<'_> {
62
    /// Creates a new module builder with a given name and version.
63
    ///
64
    /// # Arguments
65
    ///
66
    /// * `name` - The name of the extension.
67
    /// * `version` - The current version of the extension.
68
    pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
12✔
69
        Self {
70
            name: name.into(),
36✔
71
            version: version.into(),
36✔
72
            functions: vec![],
24✔
73
            constants: vec![],
24✔
74
            classes: vec![],
12✔
75
            ..Default::default()
76
        }
77
    }
78
    /// Overrides module name.
79
    ///
80
    /// # Arguments
81
    ///
82
    /// * `name` - The name of the extension.
NEW
83
    pub fn set_name(mut self, name: impl Into<String>) -> Self {
×
NEW
84
        self.name = name.into();
×
NEW
85
        self
×
86
    }
87

88
    /// Overrides module version.
89
    /// # Arguments
90
    ///
91
    /// * `version` - The current version of the extension.
NEW
92
    pub fn set_version(mut self, version: impl Into<String>) -> Self {
×
NEW
93
        self.version = version.into();
×
NEW
94
        self
×
95
    }
96

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

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

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

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

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

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

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

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

193
    /// Adds a class to the extension.
194
    ///
195
    /// # Panics
196
    ///
197
    /// * Panics if a constant could not be registered.
UNCOV
198
    pub fn class<T: RegisteredClass>(mut self) -> Self {
×
UNCOV
199
        self.classes.push(|| {
×
200
            let mut builder = ClassBuilder::new(T::CLASS_NAME);
×
201
            for (method, flags) in T::method_builders() {
×
202
                builder = builder.method(method, flags);
×
203
            }
204
            if let Some(parent) = T::EXTENDS {
×
UNCOV
205
                builder = builder.extends(parent);
×
206
            }
207
            for interface in T::IMPLEMENTS {
×
UNCOV
208
                builder = builder.implements(*interface);
×
209
            }
210
            for (name, value, docs) in T::constants() {
×
UNCOV
211
                builder = builder
×
212
                    .dyn_constant(*name, *value, docs)
×
213
                    .expect("Failed to register constant");
×
214
            }
215
            for (name, prop_info) in T::get_properties() {
×
UNCOV
216
                builder = builder.property(name, prop_info.flags, prop_info.docs);
×
217
            }
218
            if let Some(modifier) = T::BUILDER_MODIFIER {
×
UNCOV
219
                builder = modifier(builder);
×
220
            }
221

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

232
    /// Adds an enum to the extension.
233
    #[cfg(feature = "enum")]
UNCOV
234
    pub fn enumeration<T>(mut self) -> Self
×
235
    where
236
        T: RegisteredClass + RegisteredEnum,
237
    {
UNCOV
238
        self.enums.push(|| {
×
UNCOV
239
            let mut builder = EnumBuilder::new(T::CLASS_NAME);
×
240
            for case in T::CASES {
×
241
                builder = builder.case(case);
×
242
            }
243
            for (method, flags) in T::method_builders() {
×
UNCOV
244
                builder = builder.method(method, flags);
×
245
            }
246

UNCOV
247
            builder
×
UNCOV
248
                .registration(|ce| {
×
249
                    T::get_metadata().set_ce(ce);
×
250
                })
251
                .docs(T::DOC_COMMENTS)
×
252
        });
253

UNCOV
254
        self
×
255
    }
256
}
257

258
/// Artifacts from the [`ModuleBuilder`] that should be revisited inside the
259
/// extension startup function.
260
pub struct ModuleStartup {
261
    constants: Vec<(String, Box<dyn IntoConst + Send>)>,
262
    classes: Vec<fn() -> ClassBuilder>,
263
    #[cfg(feature = "enum")]
264
    enums: Vec<fn() -> EnumBuilder>,
265
}
266

267
impl ModuleStartup {
268
    /// Completes startup of the module. Should only be called inside the module
269
    /// startup function.
270
    ///
271
    /// # Errors
272
    ///
273
    /// * Returns an error if a constant could not be registered.
274
    ///
275
    /// # Panics
276
    ///
277
    /// * Panics if a class could not be registered.
278
    pub fn startup(self, _ty: i32, mod_num: i32) -> Result<()> {
1✔
279
        for (name, val) in self.constants {
1✔
UNCOV
280
            val.register_constant(&name, mod_num)?;
×
281
        }
282

283
        self.classes.into_iter().map(|c| c()).for_each(|c| {
1✔
UNCOV
284
            c.register().expect("Failed to build class");
×
285
        });
286

287
        #[cfg(feature = "enum")]
UNCOV
288
        self.enums
×
289
            .into_iter()
290
            .map(|builder| builder())
×
UNCOV
291
            .for_each(|e| {
×
292
                e.register().expect("Failed to build enum");
×
293
            });
294

UNCOV
295
        Ok(())
×
296
    }
297
}
298

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

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

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

310
    fn try_from(builder: ModuleBuilder) -> Result<Self, Self::Error> {
2✔
311
        let mut functions = builder
4✔
312
            .functions
2✔
313
            .into_iter()
314
            .map(FunctionBuilder::build)
2✔
315
            .collect::<Result<Vec<_>>>()?;
UNCOV
316
        functions.push(FunctionEntry::end());
×
UNCOV
317
        let functions = Box::into_raw(functions.into_boxed_slice()) as *const FunctionEntry;
×
318

319
        let name = CString::new(builder.name)?.into_raw();
2✔
320
        let version = CString::new(builder.version)?.into_raw();
2✔
321

322
        let startup = ModuleStartup {
UNCOV
323
            constants: builder
×
324
                .constants
325
                .into_iter()
326
                .map(|(n, v, _)| (n, v))
327
                .collect(),
UNCOV
328
            classes: builder.classes,
×
329
            #[cfg(feature = "enum")]
330
            enums: builder.enums,
×
331
        };
332

333
        Ok((
334
            ModuleEntry {
UNCOV
335
                size: mem::size_of::<ModuleEntry>().try_into()?,
×
336
                zend_api: ZEND_MODULE_API_NO,
337
                zend_debug: u8::from(PHP_DEBUG),
2✔
UNCOV
338
                zts: u8::from(PHP_ZTS),
×
UNCOV
339
                ini_entry: ptr::null(),
×
340
                deps: ptr::null(),
×
341
                name,
×
342
                functions,
×
343
                module_startup_func: builder.startup_func,
×
344
                module_shutdown_func: builder.shutdown_func,
×
345
                request_startup_func: builder.request_startup_func,
×
346
                request_shutdown_func: builder.request_shutdown_func,
×
347
                info_func: builder.info_func,
×
348
                version,
×
349
                globals_size: 0,
350
                #[cfg(not(php_zts))]
UNCOV
351
                globals_ptr: ptr::null_mut(),
×
352
                #[cfg(php_zts)]
353
                globals_id_ptr: ptr::null_mut(),
UNCOV
354
                globals_ctor: None,
×
UNCOV
355
                globals_dtor: None,
×
356
                post_deactivate_func: builder.post_deactivate_func,
×
357
                module_started: 0,
358
                type_: 0,
UNCOV
359
                handle: ptr::null_mut(),
×
360
                module_number: 0,
361
                build_id: unsafe { ext_php_rs_php_build_id() },
×
362
            },
363
            startup,
×
364
        ))
365
    }
366
}
367

368
#[cfg(test)]
369
mod tests {
370
    use crate::test::{
371
        test_deactivate_function, test_function, test_info_function, test_startup_shutdown_function,
372
    };
373

374
    use super::*;
375

376
    #[test]
377
    fn test_new() {
378
        let builder = ModuleBuilder::new("test", "1.0");
379
        assert_eq!(builder.name, "test");
380
        assert_eq!(builder.version, "1.0");
381
        assert!(builder.functions.is_empty());
382
        assert!(builder.constants.is_empty());
383
        assert!(builder.classes.is_empty());
384
        assert!(builder.startup_func.is_none());
385
        assert!(builder.shutdown_func.is_none());
386
        assert!(builder.request_startup_func.is_none());
387
        assert!(builder.request_shutdown_func.is_none());
388
        assert!(builder.post_deactivate_func.is_none());
389
        assert!(builder.info_func.is_none());
390
        #[cfg(feature = "enum")]
391
        assert!(builder.enums.is_empty());
392
    }
393

394
    #[test]
395
    fn test_startup_function() {
396
        let builder =
397
            ModuleBuilder::new("test", "1.0").startup_function(test_startup_shutdown_function);
398
        assert!(builder.startup_func.is_some());
399
    }
400

401
    #[test]
402
    fn test_shutdown_function() {
403
        let builder =
404
            ModuleBuilder::new("test", "1.0").shutdown_function(test_startup_shutdown_function);
405
        assert!(builder.shutdown_func.is_some());
406
    }
407

408
    #[test]
409
    fn test_request_startup_function() {
410
        let builder = ModuleBuilder::new("test", "1.0")
411
            .request_startup_function(test_startup_shutdown_function);
412
        assert!(builder.request_startup_func.is_some());
413
    }
414

415
    #[test]
416
    fn test_request_shutdown_function() {
417
        let builder = ModuleBuilder::new("test", "1.0")
418
            .request_shutdown_function(test_startup_shutdown_function);
419
        assert!(builder.request_shutdown_func.is_some());
420
    }
421

422
    #[test]
423
    fn test_set_post_deactivate_function() {
424
        let builder =
425
            ModuleBuilder::new("test", "1.0").post_deactivate_function(test_deactivate_function);
426
        assert!(builder.post_deactivate_func.is_some());
427
    }
428

429
    #[test]
430
    fn test_set_info_function() {
431
        let builder = ModuleBuilder::new("test", "1.0").info_function(test_info_function);
432
        assert!(builder.info_func.is_some());
433
    }
434

435
    #[test]
436
    fn test_add_function() {
437
        let builder =
438
            ModuleBuilder::new("test", "1.0").function(FunctionBuilder::new("test", test_function));
439
        assert_eq!(builder.functions.len(), 1);
440
    }
441

442
    #[test]
443
    #[cfg(feature = "embed")]
444
    fn test_add_constant() {
445
        let builder =
446
            ModuleBuilder::new("test", "1.0").constant(("TEST_CONST", 42, DocComments::default()));
447
        assert_eq!(builder.constants.len(), 1);
448
        assert_eq!(builder.constants[0].0, "TEST_CONST");
449
        // TODO: Check if the value is 42
450
        assert_eq!(builder.constants[0].2, DocComments::default());
451
    }
452
}
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