Skip to main content

ext_php_rs/builders/
module.rs

1use std::{convert::TryFrom, ffi::CString, mem, ptr};
2
3use super::{ClassBuilder, FunctionBuilder};
4use crate::{
5    PHP_DEBUG, PHP_ZTS,
6    class::RegisteredClass,
7    constant::IntoConst,
8    describe::DocComments,
9    error::Result,
10    ffi::{ZEND_MODULE_API_NO, ext_php_rs_php_build_id},
11    flags::ClassFlags,
12    zend::{FunctionEntry, ModuleEntry},
13};
14#[cfg(feature = "enum")]
15use crate::{builders::enum_builder::EnumBuilder, enum_::RegisteredEnum};
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/// #[unsafe(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/// #[unsafe(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)]
46pub 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
63impl 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 {
71        Self {
72            name: name.into(),
73            version: version.into(),
74            functions: vec![],
75            constants: vec![],
76            classes: vec![],
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 {
87        self.name = name.into();
88        self
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 {
97        self.version = version.into();
98        self
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 {
107        self.startup_func = Some(func);
108        self
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 {
117        self.shutdown_func = Some(func);
118        self
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 {
127        self.request_startup_func = Some(func);
128        self
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 {
137        self.request_shutdown_func = Some(func);
138        self
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 {
151        self.post_deactivate_func = Some(func);
152        self
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 {
162        self.info_func = Some(func);
163        self
164    }
165
166    /// Registers a function call observer for profiling or tracing.
167    ///
168    /// The factory function is called once globally during MINIT to create
169    /// a singleton observer instance shared across all requests and threads.
170    /// The observer must be `Send + Sync` as it may be accessed concurrently
171    /// in ZTS builds.
172    ///
173    /// # Arguments
174    ///
175    /// * `factory` - A function that creates an observer instance
176    ///
177    /// # Example
178    ///
179    /// ```ignore
180    /// use ext_php_rs::prelude::*;
181    /// use ext_php_rs::zend::{FcallObserver, FcallInfo, ExecuteData};
182    /// use ext_php_rs::types::Zval;
183    ///
184    /// struct MyProfiler;
185    ///
186    /// impl FcallObserver for MyProfiler {
187    ///     fn should_observe(&self, info: &FcallInfo) -> bool {
188    ///         !info.is_internal
189    ///     }
190    ///     fn begin(&self, _: &ExecuteData) {}
191    ///     fn end(&self, _: &ExecuteData, _: Option<&Zval>) {}
192    /// }
193    ///
194    /// #[php_module]
195    /// pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
196    ///     module.fcall_observer(|| MyProfiler)
197    /// }
198    /// ```
199    ///
200    /// # Panics
201    ///
202    /// Panics if called more than once on the same module.
203    #[cfg(feature = "observer")]
204    pub fn fcall_observer<F, O>(self, factory: F) -> Self
205    where
206        F: Fn() -> O + Send + Sync + 'static,
207        O: crate::zend::FcallObserver + Send + Sync,
208    {
209        let boxed_factory: Box<
210            dyn Fn() -> Box<dyn crate::zend::FcallObserver + Send + Sync> + Send + Sync,
211        > = Box::new(move || Box::new(factory()));
212        crate::zend::observer::register_fcall_observer_factory(boxed_factory);
213        self
214    }
215
216    /// Adds a function to the extension.
217    ///
218    /// # Arguments
219    ///
220    /// * `func` - The function to be added to the extension.
221    pub fn function(mut self, func: FunctionBuilder<'static>) -> Self {
222        self.functions.push(func);
223        self
224    }
225
226    /// Adds a constant to the extension.
227    ///
228    /// # Arguments
229    ///
230    /// * `const` - Tuple containing the name, value and doc comments for the
231    ///   constant. This is a tuple to support the [`wrap_constant`] macro.
232    ///
233    /// [`wrap_constant`]: crate::wrap_constant
234    pub fn constant(
235        mut self,
236        r#const: (&str, impl IntoConst + Send + 'static, DocComments),
237    ) -> Self {
238        let (name, val, docs) = r#const;
239        self.constants.push((
240            name.into(),
241            Box::new(val) as Box<dyn IntoConst + Send>,
242            docs,
243        ));
244        self
245    }
246
247    /// Adds a interface to the extension.
248    ///
249    /// # Panics
250    ///
251    /// * Panics if a constant could not be registered.
252    pub fn interface<T: RegisteredClass>(mut self) -> Self {
253        self.interfaces.push(|| {
254            let mut builder = ClassBuilder::new(T::CLASS_NAME);
255            for (method, flags) in T::method_builders() {
256                builder = builder.method(method, flags);
257            }
258            for interface in T::IMPLEMENTS {
259                builder = builder.implements(*interface);
260            }
261            for (name, value, docs) in T::constants() {
262                builder = builder
263                    .dyn_constant(*name, *value, docs)
264                    .expect("Failed to register constant");
265            }
266
267            if let Some(modifier) = T::BUILDER_MODIFIER {
268                builder = modifier(builder);
269            }
270
271            builder = builder.flags(ClassFlags::Interface);
272            builder
273                .object_override::<T>()
274                .registration(|ce| {
275                    T::get_metadata().set_ce(ce);
276                })
277                .docs(T::DOC_COMMENTS)
278        });
279        self
280    }
281
282    /// Adds a class to the extension.
283    ///
284    /// # Panics
285    ///
286    /// * Panics if a constant could not be registered.
287    pub fn class<T: RegisteredClass>(mut self) -> Self {
288        self.classes.push(|| {
289            let mut builder = ClassBuilder::new(T::CLASS_NAME);
290            for (method, flags) in T::method_builders() {
291                builder = builder.method(method, flags);
292            }
293            if let Some(parent) = T::EXTENDS {
294                builder = builder.extends(parent);
295            }
296            for interface in T::IMPLEMENTS {
297                builder = builder.implements(*interface);
298            }
299            for (name, value, docs) in T::constants() {
300                builder = builder
301                    .dyn_constant(*name, *value, docs)
302                    .expect("Failed to register constant");
303            }
304            for (name, prop_info) in T::get_properties() {
305                builder = builder.property(name, prop_info.flags, None, prop_info.docs);
306            }
307            for (name, flags, default, docs) in T::static_properties() {
308                let default_fn = default.map(|v| {
309                    Box::new(move || v.as_zval(true))
310                        as Box<dyn FnOnce() -> crate::error::Result<crate::types::Zval>>
311                });
312                builder = builder.property(*name, *flags, default_fn, docs);
313            }
314            if let Some(modifier) = T::BUILDER_MODIFIER {
315                builder = modifier(builder);
316            }
317
318            builder
319                .flags(T::FLAGS)
320                .object_override::<T>()
321                .registration(|ce| {
322                    T::get_metadata().set_ce(ce);
323                })
324                .docs(T::DOC_COMMENTS)
325        });
326        self
327    }
328
329    /// Adds an enum to the extension.
330    #[cfg(feature = "enum")]
331    pub fn enumeration<T>(mut self) -> Self
332    where
333        T: RegisteredClass + RegisteredEnum,
334    {
335        self.enums.push(|| {
336            let mut builder = EnumBuilder::new(T::CLASS_NAME);
337            for case in T::CASES {
338                builder = builder.case(case);
339            }
340            for (method, flags) in T::method_builders() {
341                builder = builder.method(method, flags);
342            }
343
344            builder
345                .registration(|ce| {
346                    T::get_metadata().set_ce(ce);
347                })
348                .docs(T::DOC_COMMENTS)
349        });
350
351        self
352    }
353}
354
355/// Artifacts from the [`ModuleBuilder`] that should be revisited inside the
356/// extension startup function.
357pub struct ModuleStartup {
358    constants: Vec<(String, Box<dyn IntoConst + Send>)>,
359    classes: Vec<fn() -> ClassBuilder>,
360    interfaces: Vec<fn() -> ClassBuilder>,
361    #[cfg(feature = "enum")]
362    enums: Vec<fn() -> EnumBuilder>,
363}
364
365impl ModuleStartup {
366    /// Completes startup of the module. Should only be called inside the module
367    /// startup function.
368    ///
369    /// # Errors
370    ///
371    /// * Returns an error if a constant could not be registered.
372    ///
373    /// # Panics
374    ///
375    /// * Panics if a class could not be registered.
376    pub fn startup(self, _ty: i32, mod_num: i32) -> Result<()> {
377        for (name, val) in self.constants {
378            val.register_constant(&name, mod_num)?;
379        }
380
381        self.classes.into_iter().map(|c| c()).for_each(|c| {
382            c.register().expect("Failed to build class");
383        });
384
385        self.interfaces.into_iter().map(|c| c()).for_each(|c| {
386            c.register().expect("Failed to build interface");
387        });
388
389        #[cfg(feature = "enum")]
390        self.enums
391            .into_iter()
392            .map(|builder| builder())
393            .for_each(|e| {
394                e.register().expect("Failed to build enum");
395            });
396
397        // Initialize observer system if registered
398        #[cfg(feature = "observer")]
399        unsafe {
400            crate::zend::observer::observer_startup();
401        }
402
403        Ok(())
404    }
405}
406
407/// A function to be called when the extension is starting up or shutting down.
408pub type StartupShutdownFunc = unsafe extern "C" fn(_type: i32, _module_number: i32) -> i32;
409
410/// A function to be called when `phpinfo();` is called.
411pub type InfoFunc = unsafe extern "C" fn(zend_module: *mut ModuleEntry);
412
413/// Builds a [`ModuleEntry`] and [`ModuleStartup`] from a [`ModuleBuilder`].
414/// This is the entry point for the module to be registered with PHP.
415impl TryFrom<ModuleBuilder<'_>> for (ModuleEntry, ModuleStartup) {
416    type Error = crate::error::Error;
417
418    fn try_from(builder: ModuleBuilder) -> Result<Self, Self::Error> {
419        let mut functions = builder
420            .functions
421            .into_iter()
422            .map(FunctionBuilder::build)
423            .collect::<Result<Vec<_>>>()?;
424        functions.push(FunctionEntry::end());
425        let functions = Box::into_raw(functions.into_boxed_slice()) as *const FunctionEntry;
426
427        let name = CString::new(builder.name)?.into_raw();
428        let version = CString::new(builder.version)?.into_raw();
429
430        let startup = ModuleStartup {
431            constants: builder
432                .constants
433                .into_iter()
434                .map(|(n, v, _)| (n, v))
435                .collect(),
436            classes: builder.classes,
437            interfaces: builder.interfaces,
438            #[cfg(feature = "enum")]
439            enums: builder.enums,
440        };
441
442        #[cfg(not(php_zts))]
443        let module_entry = ModuleEntry {
444            size: mem::size_of::<ModuleEntry>().try_into()?,
445            zend_api: ZEND_MODULE_API_NO,
446            zend_debug: u8::from(PHP_DEBUG),
447            zts: u8::from(PHP_ZTS),
448            ini_entry: ptr::null(),
449            deps: ptr::null(),
450            name,
451            functions,
452            module_startup_func: builder.startup_func,
453            module_shutdown_func: builder.shutdown_func,
454            request_startup_func: builder.request_startup_func,
455            request_shutdown_func: builder.request_shutdown_func,
456            info_func: builder.info_func,
457            version,
458            globals_size: 0,
459            globals_ptr: ptr::null_mut(),
460            globals_ctor: None,
461            globals_dtor: None,
462            post_deactivate_func: builder.post_deactivate_func,
463            module_started: 0,
464            type_: 0,
465            handle: ptr::null_mut(),
466            module_number: 0,
467            build_id: unsafe { ext_php_rs_php_build_id() },
468        };
469
470        #[cfg(php_zts)]
471        let module_entry = ModuleEntry {
472            size: mem::size_of::<ModuleEntry>().try_into()?,
473            zend_api: ZEND_MODULE_API_NO,
474            zend_debug: u8::from(PHP_DEBUG),
475            zts: u8::from(PHP_ZTS),
476            ini_entry: ptr::null(),
477            deps: ptr::null(),
478            name,
479            functions,
480            module_startup_func: builder.startup_func,
481            module_shutdown_func: builder.shutdown_func,
482            request_startup_func: builder.request_startup_func,
483            request_shutdown_func: builder.request_shutdown_func,
484            info_func: builder.info_func,
485            version,
486            globals_size: 0,
487            globals_id_ptr: ptr::null_mut(),
488            globals_ctor: None,
489            globals_dtor: None,
490            post_deactivate_func: builder.post_deactivate_func,
491            module_started: 0,
492            type_: 0,
493            handle: ptr::null_mut(),
494            module_number: 0,
495            build_id: unsafe { ext_php_rs_php_build_id() },
496        };
497
498        Ok((module_entry, startup))
499    }
500}
501
502#[cfg(test)]
503mod tests {
504    use crate::test::{
505        test_deactivate_function, test_function, test_info_function, test_startup_shutdown_function,
506    };
507
508    use super::*;
509
510    #[test]
511    fn test_new() {
512        let builder = ModuleBuilder::new("test", "1.0");
513        assert_eq!(builder.name, "test");
514        assert_eq!(builder.version, "1.0");
515        assert!(builder.functions.is_empty());
516        assert!(builder.constants.is_empty());
517        assert!(builder.classes.is_empty());
518        assert!(builder.interfaces.is_empty());
519        assert!(builder.startup_func.is_none());
520        assert!(builder.shutdown_func.is_none());
521        assert!(builder.request_startup_func.is_none());
522        assert!(builder.request_shutdown_func.is_none());
523        assert!(builder.post_deactivate_func.is_none());
524        assert!(builder.info_func.is_none());
525        #[cfg(feature = "enum")]
526        assert!(builder.enums.is_empty());
527    }
528
529    #[test]
530    fn test_name() {
531        let builder = ModuleBuilder::new("test", "1.0").name("new_test");
532        assert_eq!(builder.name, "new_test");
533    }
534
535    #[test]
536    fn test_version() {
537        let builder = ModuleBuilder::new("test", "1.0").version("2.0");
538        assert_eq!(builder.version, "2.0");
539    }
540
541    #[test]
542    fn test_startup_function() {
543        let builder =
544            ModuleBuilder::new("test", "1.0").startup_function(test_startup_shutdown_function);
545        assert!(builder.startup_func.is_some());
546    }
547
548    #[test]
549    fn test_shutdown_function() {
550        let builder =
551            ModuleBuilder::new("test", "1.0").shutdown_function(test_startup_shutdown_function);
552        assert!(builder.shutdown_func.is_some());
553    }
554
555    #[test]
556    fn test_request_startup_function() {
557        let builder = ModuleBuilder::new("test", "1.0")
558            .request_startup_function(test_startup_shutdown_function);
559        assert!(builder.request_startup_func.is_some());
560    }
561
562    #[test]
563    fn test_request_shutdown_function() {
564        let builder = ModuleBuilder::new("test", "1.0")
565            .request_shutdown_function(test_startup_shutdown_function);
566        assert!(builder.request_shutdown_func.is_some());
567    }
568
569    #[test]
570    fn test_set_post_deactivate_function() {
571        let builder =
572            ModuleBuilder::new("test", "1.0").post_deactivate_function(test_deactivate_function);
573        assert!(builder.post_deactivate_func.is_some());
574    }
575
576    #[test]
577    fn test_set_info_function() {
578        let builder = ModuleBuilder::new("test", "1.0").info_function(test_info_function);
579        assert!(builder.info_func.is_some());
580    }
581
582    #[test]
583    fn test_add_function() {
584        let builder =
585            ModuleBuilder::new("test", "1.0").function(FunctionBuilder::new("test", test_function));
586        assert_eq!(builder.functions.len(), 1);
587    }
588
589    #[test]
590    #[cfg(feature = "embed")]
591    fn test_add_constant() {
592        let builder =
593            ModuleBuilder::new("test", "1.0").constant(("TEST_CONST", 42, DocComments::default()));
594        assert_eq!(builder.constants.len(), 1);
595        assert_eq!(builder.constants[0].0, "TEST_CONST");
596        // TODO: Check if the value is 42
597        assert_eq!(builder.constants[0].2, DocComments::default());
598    }
599}