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

davidcole1340 / ext-php-rs / 16401140698

20 Jul 2025 02:13PM UTC coverage: 26.945% (+1.2%) from 25.766%
16401140698

Pull #489

github

Xenira
refactor(enum): use raii box pattern for enum values

Refs: #178
Pull Request #489: feat(enum): add basic enum support

118 of 288 new or added lines in 7 files covered. (40.97%)

1 existing line in 1 file now uncovered.

1115 of 4138 relevant lines covered (26.95%)

5.64 hits per line

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

40.0
/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

79
    /// Sets the startup function for the extension.
80
    ///
81
    /// # Arguments
82
    ///
83
    /// * `func` - The function to be called on startup.
84
    pub fn startup_function(mut self, func: StartupShutdownFunc) -> Self {
3✔
85
        self.startup_func = Some(func);
3✔
86
        self
3✔
87
    }
88

89
    /// Sets the shutdown function for the extension.
90
    ///
91
    /// # Arguments
92
    ///
93
    /// * `func` - The function to be called on shutdown.
94
    pub fn shutdown_function(mut self, func: StartupShutdownFunc) -> Self {
1✔
95
        self.shutdown_func = Some(func);
1✔
96
        self
1✔
97
    }
98

99
    /// Sets the request startup function for the extension.
100
    ///
101
    /// # Arguments
102
    ///
103
    /// * `func` - The function to be called when startup is requested.
104
    pub fn request_startup_function(mut self, func: StartupShutdownFunc) -> Self {
1✔
105
        self.request_startup_func = Some(func);
1✔
106
        self
1✔
107
    }
108

109
    /// Sets the request shutdown function for the extension.
110
    ///
111
    /// # Arguments
112
    ///
113
    /// * `func` - The function to be called when shutdown is requested.
114
    pub fn request_shutdown_function(mut self, func: StartupShutdownFunc) -> Self {
1✔
115
        self.request_shutdown_func = Some(func);
1✔
116
        self
1✔
117
    }
118

119
    /// Sets the post request shutdown function for the extension.
120
    ///
121
    /// This function can be useful if you need to do any final cleanup at the
122
    /// very end of a request, after all other resources have been released. For
123
    /// example, if your extension creates any persistent resources that last
124
    /// beyond a single request, you could use this function to clean those up.
125
    /// # Arguments
126
    ///
127
    /// * `func` - The function to be called when shutdown is requested.
128
    pub fn post_deactivate_function(mut self, func: unsafe extern "C" fn() -> i32) -> Self {
1✔
129
        self.post_deactivate_func = Some(func);
1✔
130
        self
1✔
131
    }
132

133
    /// Sets the extension information function for the extension.
134
    ///
135
    /// # Arguments
136
    ///
137
    /// * `func` - The function to be called to retrieve the information about
138
    ///   the extension.
139
    pub fn info_function(mut self, func: InfoFunc) -> Self {
1✔
140
        self.info_func = Some(func);
1✔
141
        self
1✔
142
    }
143

144
    /// Adds a function to the extension.
145
    ///
146
    /// # Arguments
147
    ///
148
    /// * `func` - The function to be added to the extension.
149
    pub fn function(mut self, func: FunctionBuilder<'static>) -> Self {
4✔
150
        self.functions.push(func);
12✔
151
        self
4✔
152
    }
153

154
    /// Adds a constant to the extension.
155
    ///
156
    /// # Arguments
157
    ///
158
    /// * `const` - Tuple containing the name, value and doc comments for the
159
    ///   constant. This is a tuple to support the [`wrap_constant`] macro.
160
    ///
161
    /// [`wrap_constant`]: crate::wrap_constant
162
    pub fn constant(
1✔
163
        mut self,
164
        r#const: (&str, impl IntoConst + Send + 'static, DocComments),
165
    ) -> Self {
166
        let (name, val, docs) = r#const;
4✔
167
        self.constants.push((
3✔
168
            name.into(),
3✔
169
            Box::new(val) as Box<dyn IntoConst + Send>,
2✔
170
            docs,
1✔
171
        ));
172
        self
1✔
173
    }
174

175
    /// Adds a class to the extension.
176
    ///
177
    /// # Panics
178
    ///
179
    /// * Panics if a constant could not be registered.
180
    pub fn class<T: RegisteredClass>(mut self) -> Self {
×
181
        self.classes.push(|| {
×
182
            let mut builder = ClassBuilder::new(T::CLASS_NAME);
×
183
            for (method, flags) in T::method_builders() {
×
184
                builder = builder.method(method, flags);
×
185
            }
186
            if let Some(parent) = T::EXTENDS {
×
187
                builder = builder.extends(parent);
×
188
            }
189
            for interface in T::IMPLEMENTS {
×
190
                builder = builder.implements(*interface);
×
191
            }
192
            for (name, value, docs) in T::constants() {
×
193
                builder = builder
×
194
                    .dyn_constant(*name, *value, docs)
×
195
                    .expect("Failed to register constant");
×
196
            }
197
            for (name, prop_info) in T::get_properties() {
×
198
                builder = builder.property(name, prop_info.flags, prop_info.docs);
×
199
            }
200
            if let Some(modifier) = T::BUILDER_MODIFIER {
×
201
                builder = modifier(builder);
×
202
            }
203

204
            builder
×
205
                .object_override::<T>()
×
206
                .registration(|ce| {
×
207
                    T::get_metadata().set_ce(ce);
×
208
                })
209
                .docs(T::DOC_COMMENTS)
×
210
        });
211
        self
×
212
    }
213

214
    /// Adds an enum to the extension.
215
    #[cfg(feature = "enum")]
NEW
216
    pub fn enumeration<T>(mut self) -> Self
×
217
    where
218
        T: RegisteredClass + RegisteredEnum,
219
    {
NEW
220
        self.enums.push(|| {
×
NEW
221
            let mut builder = EnumBuilder::new(T::CLASS_NAME);
×
NEW
222
            for case in T::CASES {
×
NEW
223
                builder = builder.case(case);
×
224
            }
NEW
225
            for (method, flags) in T::method_builders() {
×
NEW
226
                builder = builder.method(method, flags);
×
227
            }
228

NEW
229
            builder
×
NEW
230
                .registration(|ce| {
×
NEW
231
                    T::get_metadata().set_ce(ce);
×
232
                })
NEW
233
                .docs(T::DOC_COMMENTS)
×
234
        });
235

NEW
236
        self
×
237
    }
238
}
239

240
/// Artifacts from the [`ModuleBuilder`] that should be revisited inside the
241
/// extension startup function.
242
pub struct ModuleStartup {
243
    constants: Vec<(String, Box<dyn IntoConst + Send>)>,
244
    classes: Vec<fn() -> ClassBuilder>,
245
    #[cfg(feature = "enum")]
246
    enums: Vec<fn() -> EnumBuilder>,
247
}
248

249
impl ModuleStartup {
250
    /// Completes startup of the module. Should only be called inside the module
251
    /// startup function.
252
    ///
253
    /// # Errors
254
    ///
255
    /// * Returns an error if a constant could not be registered.
256
    ///
257
    /// # Panics
258
    ///
259
    /// * Panics if a class could not be registered.
260
    pub fn startup(self, _ty: i32, mod_num: i32) -> Result<()> {
1✔
261
        for (name, val) in self.constants {
1✔
262
            val.register_constant(&name, mod_num)?;
×
263
        }
264

265
        self.classes.into_iter().map(|c| c()).for_each(|c| {
1✔
266
            c.register().expect("Failed to build class");
×
267
        });
268

269
        #[cfg(feature = "enum")]
NEW
270
        self.enums
×
271
            .into_iter()
NEW
272
            .map(|builder| builder())
×
NEW
273
            .for_each(|e| {
×
NEW
274
                e.register().expect("Failed to build enum");
×
275
            });
276

UNCOV
277
        Ok(())
×
278
    }
279
}
280

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

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

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

292
    fn try_from(builder: ModuleBuilder) -> Result<Self, Self::Error> {
2✔
293
        let mut functions = builder
4✔
294
            .functions
2✔
295
            .into_iter()
296
            .map(FunctionBuilder::build)
2✔
297
            .collect::<Result<Vec<_>>>()?;
298
        functions.push(FunctionEntry::end());
×
299
        let functions = Box::into_raw(functions.into_boxed_slice()) as *const FunctionEntry;
×
300

301
        let name = CString::new(builder.name)?.into_raw();
2✔
302
        let version = CString::new(builder.version)?.into_raw();
2✔
303

304
        let startup = ModuleStartup {
305
            constants: builder
×
306
                .constants
307
                .into_iter()
308
                .map(|(n, v, _)| (n, v))
309
                .collect(),
310
            classes: builder.classes,
×
311
            #[cfg(feature = "enum")]
NEW
312
            enums: builder.enums,
×
313
        };
314

315
        Ok((
316
            ModuleEntry {
317
                size: mem::size_of::<ModuleEntry>().try_into()?,
×
318
                zend_api: ZEND_MODULE_API_NO,
319
                zend_debug: u8::from(PHP_DEBUG),
2✔
320
                zts: u8::from(PHP_ZTS),
×
321
                ini_entry: ptr::null(),
×
322
                deps: ptr::null(),
×
323
                name,
×
324
                functions,
×
325
                module_startup_func: builder.startup_func,
×
326
                module_shutdown_func: builder.shutdown_func,
×
327
                request_startup_func: builder.request_startup_func,
×
328
                request_shutdown_func: builder.request_shutdown_func,
×
329
                info_func: builder.info_func,
×
330
                version,
×
331
                globals_size: 0,
332
                #[cfg(not(php_zts))]
333
                globals_ptr: ptr::null_mut(),
×
334
                #[cfg(php_zts)]
335
                globals_id_ptr: ptr::null_mut(),
336
                globals_ctor: None,
×
337
                globals_dtor: None,
×
338
                post_deactivate_func: builder.post_deactivate_func,
×
339
                module_started: 0,
340
                type_: 0,
341
                handle: ptr::null_mut(),
×
342
                module_number: 0,
343
                build_id: unsafe { ext_php_rs_php_build_id() },
×
344
            },
345
            startup,
×
346
        ))
347
    }
348
}
349

350
#[cfg(test)]
351
mod tests {
352
    use crate::test::{
353
        test_deactivate_function, test_function, test_info_function, test_startup_shutdown_function,
354
    };
355

356
    use super::*;
357

358
    #[test]
359
    fn test_new() {
360
        let builder = ModuleBuilder::new("test", "1.0");
361
        assert_eq!(builder.name, "test");
362
        assert_eq!(builder.version, "1.0");
363
        assert!(builder.functions.is_empty());
364
        assert!(builder.constants.is_empty());
365
        assert!(builder.classes.is_empty());
366
        assert!(builder.startup_func.is_none());
367
        assert!(builder.shutdown_func.is_none());
368
        assert!(builder.request_startup_func.is_none());
369
        assert!(builder.request_shutdown_func.is_none());
370
        assert!(builder.post_deactivate_func.is_none());
371
        assert!(builder.info_func.is_none());
372
        #[cfg(feature = "enum")]
373
        assert!(builder.enums.is_empty());
374
    }
375

376
    #[test]
377
    fn test_startup_function() {
378
        let builder =
379
            ModuleBuilder::new("test", "1.0").startup_function(test_startup_shutdown_function);
380
        assert!(builder.startup_func.is_some());
381
    }
382

383
    #[test]
384
    fn test_shutdown_function() {
385
        let builder =
386
            ModuleBuilder::new("test", "1.0").shutdown_function(test_startup_shutdown_function);
387
        assert!(builder.shutdown_func.is_some());
388
    }
389

390
    #[test]
391
    fn test_request_startup_function() {
392
        let builder = ModuleBuilder::new("test", "1.0")
393
            .request_startup_function(test_startup_shutdown_function);
394
        assert!(builder.request_startup_func.is_some());
395
    }
396

397
    #[test]
398
    fn test_request_shutdown_function() {
399
        let builder = ModuleBuilder::new("test", "1.0")
400
            .request_shutdown_function(test_startup_shutdown_function);
401
        assert!(builder.request_shutdown_func.is_some());
402
    }
403

404
    #[test]
405
    fn test_set_post_deactivate_function() {
406
        let builder =
407
            ModuleBuilder::new("test", "1.0").post_deactivate_function(test_deactivate_function);
408
        assert!(builder.post_deactivate_func.is_some());
409
    }
410

411
    #[test]
412
    fn test_set_info_function() {
413
        let builder = ModuleBuilder::new("test", "1.0").info_function(test_info_function);
414
        assert!(builder.info_func.is_some());
415
    }
416

417
    #[test]
418
    fn test_add_function() {
419
        let builder =
420
            ModuleBuilder::new("test", "1.0").function(FunctionBuilder::new("test", test_function));
421
        assert_eq!(builder.functions.len(), 1);
422
    }
423

424
    #[test]
425
    #[cfg(feature = "embed")]
426
    fn test_add_constant() {
427
        let builder =
428
            ModuleBuilder::new("test", "1.0").constant(("TEST_CONST", 42, DocComments::default()));
429
        assert_eq!(builder.constants.len(), 1);
430
        assert_eq!(builder.constants[0].0, "TEST_CONST");
431
        // TODO: Check if the value is 42
432
        assert_eq!(builder.constants[0].2, DocComments::default());
433
    }
434
}
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