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    /// Registers an error observer for monitoring PHP errors.
217    ///
218    /// The factory function is called once during MINIT to create
219    /// a singleton observer instance shared across all requests.
220    /// The observer must be `Send + Sync` for ZTS builds.
221    ///
222    /// # Arguments
223    ///
224    /// * `factory` - A function that creates an observer instance
225    ///
226    /// # Example
227    ///
228    /// ```ignore
229    /// use ext_php_rs::prelude::*;
230    ///
231    /// struct MyErrorLogger;
232    ///
233    /// impl ErrorObserver for MyErrorLogger {
234    ///     fn should_observe(&self, error_type: ErrorType) -> bool {
235    ///         ErrorType::FATAL.contains(error_type)
236    ///     }
237    ///
238    ///     fn on_error(&self, error: &ErrorInfo) {
239    ///         eprintln!("[{}:{}] {}",
240    ///             error.filename.unwrap_or("<unknown>"),
241    ///             error.lineno,
242    ///             error.message
243    ///         );
244    ///     }
245    /// }
246    ///
247    /// #[php_module]
248    /// pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
249    ///     module.error_observer(MyErrorLogger)
250    /// }
251    /// ```
252    ///
253    /// # Panics
254    ///
255    /// Panics if called more than once on the same module.
256    #[cfg(feature = "observer")]
257    pub fn error_observer<F, O>(self, factory: F) -> Self
258    where
259        F: Fn() -> O + Send + Sync + 'static,
260        O: crate::zend::ErrorObserver + Send + Sync,
261    {
262        let boxed_factory: Box<
263            dyn Fn() -> Box<dyn crate::zend::ErrorObserver + Send + Sync> + Send + Sync,
264        > = Box::new(move || Box::new(factory()));
265        crate::zend::error_observer::register_error_observer_factory(boxed_factory);
266        self
267    }
268
269    /// Registers an exception observer for monitoring thrown PHP exceptions.
270    ///
271    /// The factory function is called once during MINIT to create
272    /// a singleton observer instance shared across all requests.
273    /// The observer must be `Send + Sync` for ZTS builds.
274    ///
275    /// The observer is called at throw time, before any catch blocks are evaluated.
276    ///
277    /// # Arguments
278    ///
279    /// * `factory` - A function that creates an observer instance
280    ///
281    /// # Example
282    ///
283    /// ```ignore
284    /// use ext_php_rs::prelude::*;
285    ///
286    /// struct MyExceptionLogger;
287    ///
288    /// impl ExceptionObserver for MyExceptionLogger {
289    ///     fn on_exception(&self, exception: &ExceptionInfo) {
290    ///         eprintln!("[EXCEPTION] {}: {} at {}:{}",
291    ///             exception.class_name,
292    ///             exception.message.as_deref().unwrap_or("<no message>"),
293    ///             exception.file.as_deref().unwrap_or("<unknown>"),
294    ///             exception.line
295    ///         );
296    ///     }
297    /// }
298    ///
299    /// #[php_module]
300    /// pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
301    ///     module.exception_observer(|| MyExceptionLogger)
302    /// }
303    /// ```
304    ///
305    /// # Panics
306    ///
307    /// Panics if called more than once on the same module.
308    #[cfg(feature = "observer")]
309    pub fn exception_observer<F, O>(self, factory: F) -> Self
310    where
311        F: Fn() -> O + Send + Sync + 'static,
312        O: crate::zend::ExceptionObserver + Send + Sync,
313    {
314        let boxed_factory: Box<
315            dyn Fn() -> Box<dyn crate::zend::ExceptionObserver + Send + Sync> + Send + Sync,
316        > = Box::new(move || Box::new(factory()));
317        crate::zend::exception_observer::register_exception_observer_factory(boxed_factory);
318        self
319    }
320
321    /// Adds a function to the extension.
322    ///
323    /// # Arguments
324    ///
325    /// * `func` - The function to be added to the extension.
326    pub fn function(mut self, func: FunctionBuilder<'static>) -> Self {
327        self.functions.push(func);
328        self
329    }
330
331    /// Adds a constant to the extension.
332    ///
333    /// # Arguments
334    ///
335    /// * `const` - Tuple containing the name, value and doc comments for the
336    ///   constant. This is a tuple to support the [`wrap_constant`] macro.
337    ///
338    /// [`wrap_constant`]: crate::wrap_constant
339    pub fn constant(
340        mut self,
341        r#const: (&str, impl IntoConst + Send + 'static, DocComments),
342    ) -> Self {
343        let (name, val, docs) = r#const;
344        self.constants.push((
345            name.into(),
346            Box::new(val) as Box<dyn IntoConst + Send>,
347            docs,
348        ));
349        self
350    }
351
352    /// Adds a interface to the extension.
353    ///
354    /// # Panics
355    ///
356    /// * Panics if a constant could not be registered.
357    pub fn interface<T: RegisteredClass>(mut self) -> Self {
358        self.interfaces.push(|| {
359            let mut builder = ClassBuilder::new(T::CLASS_NAME);
360            for (method, flags) in T::method_builders() {
361                builder = builder.method(method, flags);
362            }
363            for interface in T::IMPLEMENTS {
364                builder = builder.implements(*interface);
365            }
366            for (name, value, docs) in T::constants() {
367                builder = builder
368                    .dyn_constant(*name, *value, docs)
369                    .expect("Failed to register constant");
370            }
371
372            if let Some(modifier) = T::BUILDER_MODIFIER {
373                builder = modifier(builder);
374            }
375
376            builder = builder.flags(ClassFlags::Interface);
377            // Note: interfaces should NOT have object_override because they cannot be instantiated
378            builder
379                .registration(|ce| {
380                    T::get_metadata().set_ce(ce);
381                })
382                .docs(T::DOC_COMMENTS)
383        });
384        self
385    }
386
387    /// Adds a class to the extension.
388    ///
389    /// # Panics
390    ///
391    /// * Panics if a constant could not be registered.
392    pub fn class<T: RegisteredClass>(mut self) -> Self {
393        self.classes.push(|| {
394            let mut builder = ClassBuilder::new(T::CLASS_NAME);
395            for (method, flags) in T::method_builders() {
396                builder = builder.method(method, flags);
397            }
398            // Methods from #[php_impl_interface] trait implementations.
399            // Uses the inventory crate for cross-crate method discovery.
400            for (method, flags) in T::interface_method_implementations() {
401                builder = builder.method(method, flags);
402            }
403            if let Some(parent) = T::EXTENDS {
404                builder = builder.extends(parent);
405            }
406            // Interfaces declared via #[php(implements(...))] attribute
407            for interface in T::IMPLEMENTS {
408                builder = builder.implements(*interface);
409            }
410            // Interfaces from #[php_impl_interface] trait implementations.
411            // Uses the inventory crate for cross-crate interface discovery.
412            for interface in T::interface_implementations() {
413                builder = builder.implements(interface);
414            }
415            for (name, value, docs) in T::constants() {
416                builder = builder
417                    .dyn_constant(*name, *value, docs)
418                    .expect("Failed to register constant");
419            }
420            for (name, prop_info) in T::get_properties() {
421                builder = builder.property(name, prop_info.flags, None, prop_info.docs);
422            }
423            for (name, flags, default, docs) in T::static_properties() {
424                let default_fn = default.map(|v| {
425                    Box::new(move || v.as_zval(true))
426                        as Box<dyn FnOnce() -> crate::error::Result<crate::types::Zval>>
427                });
428                builder = builder.property(*name, *flags, default_fn, docs);
429            }
430            if let Some(modifier) = T::BUILDER_MODIFIER {
431                builder = modifier(builder);
432            }
433
434            builder
435                .flags(T::FLAGS)
436                .object_override::<T>()
437                .registration(|ce| {
438                    T::get_metadata().set_ce(ce);
439                })
440                .docs(T::DOC_COMMENTS)
441        });
442        self
443    }
444
445    /// Adds an enum to the extension.
446    #[cfg(feature = "enum")]
447    pub fn enumeration<T>(mut self) -> Self
448    where
449        T: RegisteredClass + RegisteredEnum,
450    {
451        self.enums.push(|| {
452            let mut builder = EnumBuilder::new(T::CLASS_NAME);
453            for case in T::CASES {
454                builder = builder.case(case);
455            }
456            for (method, flags) in T::method_builders() {
457                builder = builder.method(method, flags);
458            }
459
460            builder
461                .registration(|ce| {
462                    T::get_metadata().set_ce(ce);
463                })
464                .docs(T::DOC_COMMENTS)
465        });
466
467        self
468    }
469}
470
471/// Artifacts from the [`ModuleBuilder`] that should be revisited inside the
472/// extension startup function.
473pub struct ModuleStartup {
474    constants: Vec<(String, Box<dyn IntoConst + Send>)>,
475    classes: Vec<fn() -> ClassBuilder>,
476    interfaces: Vec<fn() -> ClassBuilder>,
477    #[cfg(feature = "enum")]
478    enums: Vec<fn() -> EnumBuilder>,
479}
480
481impl ModuleStartup {
482    /// Completes startup of the module. Should only be called inside the module
483    /// startup function.
484    ///
485    /// # Errors
486    ///
487    /// * Returns an error if a constant could not be registered.
488    ///
489    /// # Panics
490    ///
491    /// * Panics if a class could not be registered.
492    pub fn startup(self, _ty: i32, mod_num: i32) -> Result<()> {
493        for (name, val) in self.constants {
494            val.register_constant(&name, mod_num)?;
495        }
496
497        // Interfaces must be registered before classes so that classes can implement
498        // them
499        self.interfaces.into_iter().map(|c| c()).for_each(|c| {
500            c.register().expect("Failed to build interface");
501        });
502
503        self.classes.into_iter().map(|c| c()).for_each(|c| {
504            c.register().expect("Failed to build class");
505        });
506
507        #[cfg(feature = "enum")]
508        self.enums
509            .into_iter()
510            .map(|builder| builder())
511            .for_each(|e| {
512                e.register().expect("Failed to build enum");
513            });
514
515        // Initialize observer systems if registered
516        #[cfg(feature = "observer")]
517        unsafe {
518            crate::zend::observer::observer_startup();
519            crate::zend::error_observer::error_observer_startup();
520            crate::zend::exception_observer::exception_observer_startup();
521        }
522
523        Ok(())
524    }
525}
526
527/// A function to be called when the extension is starting up or shutting down.
528pub type StartupShutdownFunc = unsafe extern "C" fn(_type: i32, _module_number: i32) -> i32;
529
530/// A function to be called when `phpinfo();` is called.
531pub type InfoFunc = unsafe extern "C" fn(zend_module: *mut ModuleEntry);
532
533/// Builds a [`ModuleEntry`] and [`ModuleStartup`] from a [`ModuleBuilder`].
534/// This is the entry point for the module to be registered with PHP.
535impl TryFrom<ModuleBuilder<'_>> for (ModuleEntry, ModuleStartup) {
536    type Error = crate::error::Error;
537
538    fn try_from(builder: ModuleBuilder) -> Result<Self, Self::Error> {
539        let mut functions = builder
540            .functions
541            .into_iter()
542            .map(FunctionBuilder::build)
543            .collect::<Result<Vec<_>>>()?;
544        functions.push(FunctionEntry::end());
545        let functions = Box::into_raw(functions.into_boxed_slice()) as *const FunctionEntry;
546
547        let name = CString::new(builder.name)?.into_raw();
548        let version = CString::new(builder.version)?.into_raw();
549
550        let startup = ModuleStartup {
551            constants: builder
552                .constants
553                .into_iter()
554                .map(|(n, v, _)| (n, v))
555                .collect(),
556            classes: builder.classes,
557            interfaces: builder.interfaces,
558            #[cfg(feature = "enum")]
559            enums: builder.enums,
560        };
561
562        #[cfg(not(php_zts))]
563        let module_entry = ModuleEntry {
564            size: mem::size_of::<ModuleEntry>().try_into()?,
565            zend_api: ZEND_MODULE_API_NO,
566            zend_debug: u8::from(PHP_DEBUG),
567            zts: u8::from(PHP_ZTS),
568            ini_entry: ptr::null(),
569            deps: ptr::null(),
570            name,
571            functions,
572            module_startup_func: builder.startup_func,
573            module_shutdown_func: builder.shutdown_func,
574            request_startup_func: builder.request_startup_func,
575            request_shutdown_func: builder.request_shutdown_func,
576            info_func: builder.info_func,
577            version,
578            globals_size: 0,
579            globals_ptr: ptr::null_mut(),
580            globals_ctor: None,
581            globals_dtor: None,
582            post_deactivate_func: builder.post_deactivate_func,
583            module_started: 0,
584            type_: 0,
585            handle: ptr::null_mut(),
586            module_number: 0,
587            build_id: unsafe { ext_php_rs_php_build_id() },
588        };
589
590        #[cfg(php_zts)]
591        let module_entry = ModuleEntry {
592            size: mem::size_of::<ModuleEntry>().try_into()?,
593            zend_api: ZEND_MODULE_API_NO,
594            zend_debug: u8::from(PHP_DEBUG),
595            zts: u8::from(PHP_ZTS),
596            ini_entry: ptr::null(),
597            deps: ptr::null(),
598            name,
599            functions,
600            module_startup_func: builder.startup_func,
601            module_shutdown_func: builder.shutdown_func,
602            request_startup_func: builder.request_startup_func,
603            request_shutdown_func: builder.request_shutdown_func,
604            info_func: builder.info_func,
605            version,
606            globals_size: 0,
607            globals_id_ptr: ptr::null_mut(),
608            globals_ctor: None,
609            globals_dtor: None,
610            post_deactivate_func: builder.post_deactivate_func,
611            module_started: 0,
612            type_: 0,
613            handle: ptr::null_mut(),
614            module_number: 0,
615            build_id: unsafe { ext_php_rs_php_build_id() },
616        };
617
618        Ok((module_entry, startup))
619    }
620}
621
622#[cfg(test)]
623mod tests {
624    use crate::test::{
625        test_deactivate_function, test_function, test_info_function, test_startup_shutdown_function,
626    };
627
628    use super::*;
629
630    #[test]
631    fn test_new() {
632        let builder = ModuleBuilder::new("test", "1.0");
633        assert_eq!(builder.name, "test");
634        assert_eq!(builder.version, "1.0");
635        assert!(builder.functions.is_empty());
636        assert!(builder.constants.is_empty());
637        assert!(builder.classes.is_empty());
638        assert!(builder.interfaces.is_empty());
639        assert!(builder.startup_func.is_none());
640        assert!(builder.shutdown_func.is_none());
641        assert!(builder.request_startup_func.is_none());
642        assert!(builder.request_shutdown_func.is_none());
643        assert!(builder.post_deactivate_func.is_none());
644        assert!(builder.info_func.is_none());
645        #[cfg(feature = "enum")]
646        assert!(builder.enums.is_empty());
647    }
648
649    #[test]
650    fn test_name() {
651        let builder = ModuleBuilder::new("test", "1.0").name("new_test");
652        assert_eq!(builder.name, "new_test");
653    }
654
655    #[test]
656    fn test_version() {
657        let builder = ModuleBuilder::new("test", "1.0").version("2.0");
658        assert_eq!(builder.version, "2.0");
659    }
660
661    #[test]
662    fn test_startup_function() {
663        let builder =
664            ModuleBuilder::new("test", "1.0").startup_function(test_startup_shutdown_function);
665        assert!(builder.startup_func.is_some());
666    }
667
668    #[test]
669    fn test_shutdown_function() {
670        let builder =
671            ModuleBuilder::new("test", "1.0").shutdown_function(test_startup_shutdown_function);
672        assert!(builder.shutdown_func.is_some());
673    }
674
675    #[test]
676    fn test_request_startup_function() {
677        let builder = ModuleBuilder::new("test", "1.0")
678            .request_startup_function(test_startup_shutdown_function);
679        assert!(builder.request_startup_func.is_some());
680    }
681
682    #[test]
683    fn test_request_shutdown_function() {
684        let builder = ModuleBuilder::new("test", "1.0")
685            .request_shutdown_function(test_startup_shutdown_function);
686        assert!(builder.request_shutdown_func.is_some());
687    }
688
689    #[test]
690    fn test_set_post_deactivate_function() {
691        let builder =
692            ModuleBuilder::new("test", "1.0").post_deactivate_function(test_deactivate_function);
693        assert!(builder.post_deactivate_func.is_some());
694    }
695
696    #[test]
697    fn test_set_info_function() {
698        let builder = ModuleBuilder::new("test", "1.0").info_function(test_info_function);
699        assert!(builder.info_func.is_some());
700    }
701
702    #[test]
703    fn test_add_function() {
704        let builder =
705            ModuleBuilder::new("test", "1.0").function(FunctionBuilder::new("test", test_function));
706        assert_eq!(builder.functions.len(), 1);
707    }
708
709    #[test]
710    #[cfg(feature = "embed")]
711    fn test_add_constant() {
712        let builder =
713            ModuleBuilder::new("test", "1.0").constant(("TEST_CONST", 42, DocComments::default()));
714        assert_eq!(builder.constants.len(), 1);
715        assert_eq!(builder.constants[0].0, "TEST_CONST");
716        // TODO: Check if the value is 42
717        assert_eq!(builder.constants[0].2, DocComments::default());
718    }
719}