Skip to main content

ext_php_rs/builders/
module.rs

1use std::{convert::TryFrom, ffi::CString, mem, ptr};
2
3use super::{ClassBuilder, FunctionBuilder, class::first_duplicate};
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::{
13        FunctionEntry, ModuleAllocations, ModuleEntry, ModuleGlobal, ModuleGlobals,
14        SapiRequestInfo,
15        sapi_activate::{self, ActivateCallback},
16    },
17};
18#[cfg(feature = "enum")]
19use crate::{builders::enum_builder::EnumBuilder, enum_::RegisteredEnum};
20
21/// Builds a Zend module extension to be registered with PHP. Must be called
22/// from within an external function called `get_module`, returning a mutable
23/// pointer to a `ModuleEntry`.
24///
25/// ```rust,no_run
26/// use ext_php_rs::{
27///     builders::ModuleBuilder,
28///     zend::{ModuleEntry, StaticModuleEntry},
29///     info_table_start, info_table_end, info_table_row
30/// };
31///
32/// #[unsafe(no_mangle)]
33/// pub extern "C" fn php_module_info(_module: *mut ModuleEntry) {
34///     info_table_start!();
35///     info_table_row!("column 1", "column 2");
36///     info_table_end!();
37/// }
38///
39/// #[unsafe(no_mangle)]
40/// pub extern "C" fn get_module() -> *mut ModuleEntry {
41///     static MODULE: StaticModuleEntry = StaticModuleEntry::new();
42///     MODULE.get_or_init(|| {
43///         let (entry, _startup, owned) = ModuleBuilder::new("ext-name", "ext-version")
44///             .info_function(php_module_info)
45///             .try_into()
46///             .unwrap();
47///         (entry, owned)
48///     })
49/// }
50/// ```
51#[must_use]
52#[derive(Debug)]
53pub struct ModuleBuilder<'a> {
54    pub(crate) name: String,
55    pub(crate) version: String,
56    pub(crate) functions: Vec<FunctionBuilder<'a>>,
57    pub(crate) constants: Vec<(String, Box<dyn IntoConst + Send>, DocComments)>,
58    pub(crate) classes: Vec<fn() -> ClassBuilder>,
59    pub(crate) interfaces: Vec<fn() -> ClassBuilder>,
60    #[cfg(feature = "enum")]
61    pub(crate) enums: Vec<fn() -> EnumBuilder>,
62    startup_func: Option<StartupShutdownFunc>,
63    shutdown_func: Option<StartupShutdownFunc>,
64    request_startup_func: Option<StartupShutdownFunc>,
65    request_shutdown_func: Option<StartupShutdownFunc>,
66    post_deactivate_func: Option<unsafe extern "C" fn() -> i32>,
67    sapi_activate: Option<ActivateCallback>,
68    info_func: Option<InfoFunc>,
69    globals_size: usize,
70    #[cfg(php_zts)]
71    globals_id_ptr: *mut i32,
72    #[cfg(not(php_zts))]
73    globals_ptr: *mut std::ffi::c_void,
74    globals_ctor: Option<unsafe extern "C" fn(*mut std::ffi::c_void)>,
75    globals_dtor: Option<unsafe extern "C" fn(*mut std::ffi::c_void)>,
76}
77
78impl Default for ModuleBuilder<'_> {
79    fn default() -> Self {
80        Self {
81            name: String::new(),
82            version: String::new(),
83            functions: vec![],
84            constants: vec![],
85            classes: vec![],
86            interfaces: vec![],
87            #[cfg(feature = "enum")]
88            enums: vec![],
89            startup_func: None,
90            shutdown_func: None,
91            request_startup_func: None,
92            request_shutdown_func: None,
93            post_deactivate_func: None,
94            sapi_activate: None,
95            info_func: None,
96            globals_size: 0,
97            #[cfg(php_zts)]
98            globals_id_ptr: ptr::null_mut(),
99            #[cfg(not(php_zts))]
100            globals_ptr: ptr::null_mut(),
101            globals_ctor: None,
102            globals_dtor: None,
103        }
104    }
105}
106
107impl ModuleBuilder<'_> {
108    /// Creates a new module builder with a given name and version.
109    ///
110    /// # Arguments
111    ///
112    /// * `name` - The name of the extension.
113    /// * `version` - The current version of the extension.
114    pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
115        Self {
116            name: name.into(),
117            version: version.into(),
118            ..Default::default()
119        }
120    }
121
122    /// Overrides module name.
123    ///
124    /// # Arguments
125    ///
126    /// * `name` - The name of the extension.
127    pub fn name(mut self, name: impl Into<String>) -> Self {
128        self.name = name.into();
129        self
130    }
131
132    /// Overrides module version.
133    ///
134    /// # Arguments
135    ///
136    /// * `version` - The current version of the extension.
137    pub fn version(mut self, version: impl Into<String>) -> Self {
138        self.version = version.into();
139        self
140    }
141
142    /// Sets the startup function for the extension.
143    ///
144    /// # Arguments
145    ///
146    /// * `func` - The function to be called on startup.
147    pub fn startup_function(mut self, func: StartupShutdownFunc) -> Self {
148        self.startup_func = Some(func);
149        self
150    }
151
152    /// Sets the shutdown function for the extension.
153    ///
154    /// # Arguments
155    ///
156    /// * `func` - The function to be called on shutdown.
157    pub fn shutdown_function(mut self, func: StartupShutdownFunc) -> Self {
158        self.shutdown_func = Some(func);
159        self
160    }
161
162    /// Sets the request startup function for the extension.
163    ///
164    /// # Arguments
165    ///
166    /// * `func` - The function to be called when startup is requested.
167    pub fn request_startup_function(mut self, func: StartupShutdownFunc) -> Self {
168        self.request_startup_func = Some(func);
169        self
170    }
171
172    /// Sets the request shutdown function for the extension.
173    ///
174    /// # Arguments
175    ///
176    /// * `func` - The function to be called when shutdown is requested.
177    pub fn request_shutdown_function(mut self, func: StartupShutdownFunc) -> Self {
178        self.request_shutdown_func = Some(func);
179        self
180    }
181
182    /// Sets the post request shutdown function for the extension.
183    ///
184    /// This function can be useful if you need to do any final cleanup at the
185    /// very end of a request, after all other resources have been released. For
186    /// example, if your extension creates any persistent resources that last
187    /// beyond a single request, you could use this function to clean those up.
188    /// # Arguments
189    ///
190    /// * `func` - The function to be called when shutdown is requested.
191    pub fn post_deactivate_function(mut self, func: unsafe extern "C" fn() -> i32) -> Self {
192        self.post_deactivate_func = Some(func);
193        self
194    }
195
196    /// Runs `callback` at the start of every request, with the request
197    /// information that the SAPI gives.
198    ///
199    /// The callback runs from `sapi_module.activate`, after the SAPI handler.
200    /// Unlike the request startup function, it also runs for each request of a
201    /// `FrankenPHP` worker. At this point the response headers are empty, so
202    /// [`set_header`](crate::zend::set_header) and
203    /// [`set_response_code`](crate::zend::set_response_code) work. `$_SERVER`
204    /// does not exist yet and no PHP code can run.
205    ///
206    /// A panic in `callback` is printed and ignored. A module loaded with
207    /// `dl()` refuses to start when it has this callback. Calling this method
208    /// again replaces the callback.
209    ///
210    /// ```ignore
211    /// use ext_php_rs::prelude::*;
212    /// use ext_php_rs::zend::{SapiRequestInfo, set_header, set_response_code};
213    ///
214    /// #[php_module]
215    /// pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
216    ///     module.sapi_activate_function(|info: &SapiRequestInfo| {
217    ///         if info.request_uri() == Some("/old") {
218    ///             let _ = set_header("Location: /new");
219    ///             let _ = set_response_code(301);
220    ///         }
221    ///     })
222    /// }
223    /// ```
224    pub fn sapi_activate_function<F>(mut self, callback: F) -> Self
225    where
226        F: Fn(&SapiRequestInfo) + Send + Sync + 'static,
227    {
228        self.sapi_activate = Some(ActivateCallback::new(callback));
229        self
230    }
231
232    /// Sets the extension information function for the extension.
233    ///
234    /// # Arguments
235    ///
236    /// * `func` - The function to be called to retrieve the information about
237    ///   the extension.
238    pub fn info_function(mut self, func: InfoFunc) -> Self {
239        self.info_func = Some(func);
240        self
241    }
242
243    /// Registers a module globals struct with this extension.
244    ///
245    /// PHP will allocate per-thread storage (ZTS) or use the static's inline
246    /// storage (non-ZTS), calling GINIT/GSHUTDOWN callbacks automatically.
247    ///
248    /// Only one globals struct per module is supported (PHP limitation).
249    /// Calling this a second time will overwrite the previous registration.
250    ///
251    /// # Arguments
252    ///
253    /// * `handle` - A static [`ModuleGlobals`] that will hold the globals.
254    ///
255    /// # Examples
256    ///
257    /// ```ignore
258    /// use ext_php_rs::prelude::*;
259    /// use ext_php_rs::zend::{ModuleGlobal, ModuleGlobals};
260    ///
261    /// #[derive(Default)]
262    /// struct MyGlobals { counter: i64 }
263    /// impl ModuleGlobal for MyGlobals {}
264    ///
265    /// static MY_GLOBALS: ModuleGlobals<MyGlobals> = ModuleGlobals::new();
266    ///
267    /// #[php_module]
268    /// pub fn module(module: ModuleBuilder) -> ModuleBuilder {
269    ///     module.globals(&MY_GLOBALS)
270    /// }
271    /// ```
272    pub fn globals<T: ModuleGlobal>(mut self, handle: &'static ModuleGlobals<T>) -> Self {
273        use crate::zend::module_globals::{ginit_callback, gshutdown_callback};
274
275        self.globals_size = std::mem::size_of::<T>();
276        #[cfg(php_zts)]
277        {
278            self.globals_id_ptr = handle.id_ptr();
279        }
280        #[cfg(not(php_zts))]
281        {
282            self.globals_ptr = handle.data_ptr();
283        }
284        self.globals_ctor = Some(ginit_callback::<T>);
285        self.globals_dtor = Some(gshutdown_callback::<T>);
286        self
287    }
288
289    /// Registers a function call observer for profiling or tracing.
290    ///
291    /// The factory function is called once, at the first MINIT, to create
292    /// a singleton observer instance shared across all requests and threads.
293    /// When the SAPI starts the module again in the same process, the hook is
294    /// registered again with the same instance.
295    /// The observer must be `Send + Sync` as it may be accessed concurrently
296    /// in ZTS builds.
297    ///
298    /// # Arguments
299    ///
300    /// * `factory` - A function that creates an observer instance
301    ///
302    /// # Example
303    ///
304    /// ```ignore
305    /// use ext_php_rs::prelude::*;
306    /// use ext_php_rs::zend::{FcallObserver, FcallInfo, ExecuteData};
307    /// use ext_php_rs::types::Zval;
308    ///
309    /// struct MyProfiler;
310    ///
311    /// impl FcallObserver for MyProfiler {
312    ///     fn should_observe(&self, info: &FcallInfo) -> bool {
313    ///         !info.is_internal
314    ///     }
315    ///     fn begin(&self, _: &ExecuteData) {}
316    ///     fn end(&self, _: &ExecuteData, _: Option<&Zval>) {}
317    /// }
318    ///
319    /// #[php_module]
320    /// pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
321    ///     module.fcall_observer(|| MyProfiler)
322    /// }
323    /// ```
324    ///
325    /// # Panics
326    ///
327    /// Panics if called more than once on the same module.
328    #[cfg(feature = "observer")]
329    pub fn fcall_observer<F, O>(self, factory: F) -> Self
330    where
331        F: Fn() -> O + Send + Sync + 'static,
332        O: crate::zend::FcallObserver + Send + Sync,
333    {
334        let boxed_factory: Box<
335            dyn Fn() -> Box<dyn crate::zend::FcallObserver + Send + Sync> + Send + Sync,
336        > = Box::new(move || Box::new(factory()));
337        crate::zend::observer::register_fcall_observer_factory(boxed_factory);
338        self
339    }
340
341    /// Registers an error observer for monitoring PHP errors.
342    ///
343    /// The factory function is called once, at the first MINIT, to create
344    /// a singleton observer instance shared across all requests.
345    /// When the SAPI starts the module again in the same process, the hook is
346    /// registered again with the same instance.
347    /// The observer must be `Send + Sync` for ZTS builds.
348    ///
349    /// # Arguments
350    ///
351    /// * `factory` - A function that creates an observer instance
352    ///
353    /// # Example
354    ///
355    /// ```ignore
356    /// use ext_php_rs::prelude::*;
357    ///
358    /// struct MyErrorLogger;
359    ///
360    /// impl ErrorObserver for MyErrorLogger {
361    ///     fn should_observe(&self, error_type: ErrorType) -> bool {
362    ///         ErrorType::Fatal.contains(error_type)
363    ///     }
364    ///
365    ///     fn on_error(&self, error: &ErrorInfo) {
366    ///         eprintln!("[{}:{}] {}",
367    ///             error.filename.unwrap_or("<unknown>"),
368    ///             error.lineno,
369    ///             error.message
370    ///         );
371    ///     }
372    /// }
373    ///
374    /// #[php_module]
375    /// pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
376    ///     module.error_observer(MyErrorLogger)
377    /// }
378    /// ```
379    ///
380    /// # Panics
381    ///
382    /// Panics if called more than once on the same module.
383    #[cfg(feature = "observer")]
384    pub fn error_observer<F, O>(self, factory: F) -> Self
385    where
386        F: Fn() -> O + Send + Sync + 'static,
387        O: crate::zend::ErrorObserver + Send + Sync,
388    {
389        let boxed_factory: Box<
390            dyn Fn() -> Box<dyn crate::zend::ErrorObserver + Send + Sync> + Send + Sync,
391        > = Box::new(move || Box::new(factory()));
392        crate::zend::error_observer::register_error_observer_factory(boxed_factory);
393        self
394    }
395
396    /// Registers an exception observer for monitoring thrown PHP exceptions.
397    ///
398    /// The factory function is called once, at the first MINIT, to create
399    /// a singleton observer instance shared across all requests.
400    /// When the SAPI starts the module again in the same process, the hook is
401    /// registered again with the same instance.
402    /// The observer must be `Send + Sync` for ZTS builds.
403    ///
404    /// The observer is called at throw time, before any catch blocks are
405    /// evaluated.
406    ///
407    /// # Arguments
408    ///
409    /// * `factory` - A function that creates an observer instance
410    ///
411    /// # Example
412    ///
413    /// ```ignore
414    /// use ext_php_rs::prelude::*;
415    ///
416    /// struct MyExceptionLogger;
417    ///
418    /// impl ExceptionObserver for MyExceptionLogger {
419    ///     fn on_exception(&self, exception: &ExceptionInfo) {
420    ///         eprintln!("[EXCEPTION] {}: {} at {}:{}",
421    ///             exception.class_name,
422    ///             exception.message.as_deref().unwrap_or("<no message>"),
423    ///             exception.file.as_deref().unwrap_or("<unknown>"),
424    ///             exception.line
425    ///         );
426    ///     }
427    /// }
428    ///
429    /// #[php_module]
430    /// pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
431    ///     module.exception_observer(|| MyExceptionLogger)
432    /// }
433    /// ```
434    ///
435    /// # Panics
436    ///
437    /// Panics if called more than once on the same module.
438    #[cfg(feature = "observer")]
439    pub fn exception_observer<F, O>(self, factory: F) -> Self
440    where
441        F: Fn() -> O + Send + Sync + 'static,
442        O: crate::zend::ExceptionObserver + Send + Sync,
443    {
444        let boxed_factory: Box<
445            dyn Fn() -> Box<dyn crate::zend::ExceptionObserver + Send + Sync> + Send + Sync,
446        > = Box::new(move || Box::new(factory()));
447        crate::zend::exception_observer::register_exception_observer_factory(boxed_factory);
448        self
449    }
450
451    /// Adds a function to the extension.
452    ///
453    /// # Arguments
454    ///
455    /// * `func` - The function to be added to the extension.
456    pub fn function(mut self, func: FunctionBuilder<'static>) -> Self {
457        self.functions.push(func);
458        self
459    }
460
461    /// Adds a constant to the extension.
462    ///
463    /// # Arguments
464    ///
465    /// * `const` - Tuple containing the name, value and doc comments for the
466    ///   constant. This is a tuple to support the [`wrap_constant`] macro.
467    ///
468    /// [`wrap_constant`]: crate::wrap_constant
469    pub fn constant(
470        mut self,
471        r#const: (&str, impl IntoConst + Send + 'static, DocComments),
472    ) -> Self {
473        let (name, val, docs) = r#const;
474        self.constants.push((
475            name.into(),
476            Box::new(val) as Box<dyn IntoConst + Send>,
477            docs,
478        ));
479        self
480    }
481
482    /// Adds a interface to the extension.
483    ///
484    /// # Panics
485    ///
486    /// * Panics if a constant could not be registered.
487    #[expect(
488        clippy::expect_used,
489        reason = "registration runs in MINIT, where a failure is an extension bug the engine cannot recover from"
490    )]
491    pub fn interface<T: RegisteredClass>(mut self) -> Self {
492        self.interfaces.push(|| {
493            let mut builder = ClassBuilder::new(T::CLASS_NAME);
494            for (method, flags) in T::method_builders() {
495                builder = builder.method(method, flags);
496            }
497            for interface in T::IMPLEMENTS {
498                builder = builder.implements(*interface);
499            }
500            for (name, value, docs, flags) in T::constants() {
501                builder = builder
502                    .dyn_constant(*name, *value, docs, *flags)
503                    .expect("Failed to register constant");
504            }
505
506            if let Some(modifier) = T::BUILDER_MODIFIER {
507                builder = modifier(builder);
508            }
509
510            builder = builder.flags(ClassFlags::Interface);
511            // Note: interfaces should NOT have object_override because they cannot be
512            // instantiated
513            builder
514                .registration(|ce, arg_info| {
515                    T::get_metadata().set_ce(ce);
516                    T::get_metadata().set_arg_info(arg_info);
517                })
518                .docs(T::DOC_COMMENTS)
519        });
520        self
521    }
522
523    /// Adds a class to the extension.
524    ///
525    /// # Panics
526    ///
527    /// * Panics if a constant could not be registered.
528    #[expect(
529        clippy::expect_used,
530        reason = "registration runs in MINIT, where a failure is an extension bug the engine cannot recover from"
531    )]
532    pub fn class<T: RegisteredClass>(mut self) -> Self {
533        self.classes.push(|| {
534            let mut builder = ClassBuilder::new(T::CLASS_NAME);
535            for (method, flags) in T::method_builders() {
536                builder = builder.method(method, flags);
537            }
538            // Methods from #[php_impl_interface] trait implementations.
539            // Uses the inventory crate for cross-crate method discovery.
540            for (method, flags) in T::interface_method_implementations() {
541                builder = builder.method(method, flags);
542            }
543            if let Some(parent) = T::EXTENDS {
544                builder = builder.extends(parent);
545            }
546            // Interfaces declared via #[php(implements(...))] attribute
547            for interface in T::IMPLEMENTS {
548                builder = builder.implements(*interface);
549            }
550            // Interfaces from #[php_impl_interface] trait implementations.
551            // Uses the inventory crate for cross-crate interface discovery.
552            for interface in T::interface_implementations() {
553                builder = builder.implements(interface);
554            }
555            for (name, value, docs, flags) in T::constants() {
556                builder = builder
557                    .dyn_constant(*name, *value, docs, *flags)
558                    .expect("Failed to register constant");
559            }
560            for desc in T::get_metadata().all_properties() {
561                let default_stub = if desc.nullable {
562                    Some("null".into())
563                } else {
564                    None
565                };
566                builder = builder.property(crate::builders::ClassProperty {
567                    name: desc.name.into(),
568                    flags: desc.flags,
569                    default: None,
570                    docs: desc.docs,
571                    ty: Some(desc.ty),
572                    nullable: desc.nullable,
573                    readonly: desc.readonly,
574                    default_stub,
575                });
576            }
577            for (name, flags, default, docs) in T::static_properties() {
578                let default_stub = default.map(crate::convert::IntoZvalDyn::stub_value);
579                let default_fn = default.map(|v| {
580                    Box::new(move || v.as_zval(true))
581                        as Box<dyn FnOnce() -> crate::error::Result<crate::types::Zval>>
582                });
583                builder = builder.property(crate::builders::ClassProperty {
584                    name: (*name).into(),
585                    flags: *flags,
586                    default: default_fn,
587                    docs,
588                    ty: None,
589                    nullable: false,
590                    readonly: false,
591                    default_stub,
592                });
593            }
594            if let Some(modifier) = T::BUILDER_MODIFIER {
595                builder = modifier(builder);
596            }
597
598            builder
599                .flags(T::FLAGS)
600                .object_override::<T>()
601                .registration(|ce, arg_info| {
602                    T::get_metadata().set_ce(ce);
603                    T::get_metadata().set_arg_info(arg_info);
604                })
605                .docs(T::DOC_COMMENTS)
606        });
607        self
608    }
609
610    /// Adds an enum to the extension.
611    #[cfg(feature = "enum")]
612    pub fn enumeration<T>(mut self) -> Self
613    where
614        T: RegisteredClass + RegisteredEnum,
615    {
616        self.enums.push(|| {
617            let mut builder = EnumBuilder::new(T::CLASS_NAME);
618            for case in T::CASES {
619                builder = builder.case(case);
620            }
621            for (method, flags) in T::method_builders() {
622                builder = builder.method(method, flags);
623            }
624
625            builder
626                .registration(|ce, arg_info| {
627                    T::get_metadata().set_ce(ce);
628                    T::get_metadata().set_arg_info(arg_info);
629                })
630                .docs(T::DOC_COMMENTS)
631        });
632
633        self
634    }
635}
636
637#[cfg(feature = "observer")]
638impl<'a> ModuleBuilder<'a> {
639    /// Register a [`ZendExtensionHandler`] and configure which hooks PHP should
640    /// call.
641    ///
642    /// Returns a [`ZendExtensionBuilder`] that exposes three opt-in methods
643    /// (`hook_statements`, `hook_fcalls`, `hook_op_array_compile`). Call
644    /// [`ZendExtensionBuilder::finish`] to return to `ModuleBuilder`.
645    ///
646    /// # Example
647    ///
648    /// ```ignore
649    /// module
650    ///     .zend_extension(|| MyProfiler::new())
651    ///     .hook_statements()
652    ///     .finish()
653    /// ```
654    ///
655    /// # Panics
656    ///
657    /// Panics if called more than once on the same module.
658    ///
659    /// [`ZendExtensionHandler`]: crate::zend::ZendExtensionHandler
660    /// [`ZendExtensionBuilder`]: crate::zend::ZendExtensionBuilder
661    /// [`ZendExtensionBuilder::finish`]: crate::zend::ZendExtensionBuilder::finish
662    pub fn zend_extension<F, H>(
663        self,
664        factory: F,
665    ) -> crate::zend::zend_extension::ZendExtensionBuilder<'a>
666    where
667        F: Fn() -> H + Send + Sync + 'static,
668        H: crate::zend::ZendExtensionHandler,
669    {
670        crate::zend::zend_extension::ZendExtensionBuilder::new(self, factory)
671    }
672}
673
674/// Artifacts from the [`ModuleBuilder`] that should be revisited inside the
675/// extension startup function.
676pub struct ModuleStartup {
677    #[cfg(feature = "observer")]
678    name: String,
679    #[cfg(feature = "observer")]
680    version: String,
681    constants: Vec<(String, Box<dyn IntoConst + Send>)>,
682    classes: Vec<fn() -> ClassBuilder>,
683    interfaces: Vec<fn() -> ClassBuilder>,
684    #[cfg(feature = "enum")]
685    enums: Vec<fn() -> EnumBuilder>,
686}
687
688impl ModuleStartup {
689    /// Completes startup of the module. Should only be called inside the module
690    /// startup function, which runs again when the SAPI starts the module again
691    /// in the same process (e.g. a `FrankenPHP` worker restart): everything is
692    /// registered anew each time.
693    ///
694    /// # Errors
695    ///
696    /// * Returns an error if a constant, interface, class or enum could not be
697    ///   registered. The generated MINIT then returns `FAILURE` and PHP refuses
698    ///   to start the module.
699    pub fn startup(&self, ty: i32, mod_num: i32) -> Result<()> {
700        for (name, val) in &self.constants {
701            val.register_constant(name, mod_num)?;
702        }
703
704        // Interfaces must be registered before classes so that classes can implement
705        // them
706        self.interfaces.iter().try_for_each(|c| c().register())?;
707
708        self.classes.iter().try_for_each(|c| c().register())?;
709
710        #[cfg(feature = "enum")]
711        self.enums
712            .iter()
713            .try_for_each(|builder| builder().register())?;
714
715        // Initialize observer systems if registered
716        #[cfg(feature = "observer")]
717        unsafe {
718            crate::zend::observer::observer_startup();
719            crate::zend::error_observer::error_observer_startup();
720            crate::zend::exception_observer::exception_observer_startup();
721            crate::zend::zend_extension::zend_extension_startup(&self.name, &self.version);
722        }
723
724        // SAFETY: this runs in MINIT, and the hook goes in last so that no
725        // failed step leaves it installed.
726        unsafe { sapi_activate::install(ty) }
727    }
728}
729
730/// A function to be called when the extension is starting up or shutting down.
731pub type StartupShutdownFunc = unsafe extern "C" fn(_type: i32, _module_number: i32) -> i32;
732
733/// A function to be called when `phpinfo();` is called.
734pub type InfoFunc = unsafe extern "C" fn(zend_module: *mut ModuleEntry);
735
736impl ModuleBuilder<'_> {
737    /// Checks that no two functions share a PHP name. The engine compares
738    /// function names ignoring ASCII case.
739    fn validate(&self) -> Result<()> {
740        match first_duplicate(self.functions.iter().map(|f| f.name.as_str()), true) {
741            Some(name) => Err(crate::error::Error::DuplicateFunction {
742                function: name.to_owned(),
743            }),
744            None => Ok(()),
745        }
746    }
747}
748
749/// Builds a [`ModuleEntry`] and [`ModuleStartup`] from a [`ModuleBuilder`].
750/// This is the entry point for the module to be registered with PHP.
751impl TryFrom<ModuleBuilder<'_>> for (ModuleEntry, ModuleStartup, ModuleAllocations) {
752    type Error = crate::error::Error;
753
754    fn try_from(builder: ModuleBuilder) -> Result<Self, Self::Error> {
755        builder.validate()?;
756        let mut arg_info = Vec::with_capacity(builder.functions.len());
757        let mut function_table = Vec::with_capacity(builder.functions.len() + 1);
758        for function in builder.functions {
759            let (entry, args) = function.build()?;
760            function_table.push(entry);
761            arg_info.push(args);
762        }
763        function_table.push(FunctionEntry::end());
764        let function_table = function_table.into_boxed_slice();
765        let functions = function_table.as_ptr();
766
767        #[cfg(feature = "observer")]
768        let ext_name = builder.name.clone();
769        #[cfg(feature = "observer")]
770        let ext_version = builder.version.clone();
771
772        sapi_activate::set_callback(builder.sapi_activate);
773
774        let owned_name = CString::new(builder.name)?;
775        let owned_version = CString::new(builder.version)?;
776        let name = owned_name.as_ptr();
777        let version = owned_version.as_ptr();
778
779        let startup = ModuleStartup {
780            #[cfg(feature = "observer")]
781            name: ext_name,
782            #[cfg(feature = "observer")]
783            version: ext_version,
784            constants: builder
785                .constants
786                .into_iter()
787                .map(|(n, v, _)| (n, v))
788                .collect(),
789            classes: builder.classes,
790            interfaces: builder.interfaces,
791            #[cfg(feature = "enum")]
792            enums: builder.enums,
793        };
794
795        #[cfg(not(php_zts))]
796        let module_entry = ModuleEntry {
797            size: mem::size_of::<ModuleEntry>().try_into()?,
798            zend_api: ZEND_MODULE_API_NO,
799            zend_debug: u8::from(PHP_DEBUG),
800            zts: u8::from(PHP_ZTS),
801            ini_entry: ptr::null(),
802            deps: ptr::null(),
803            name,
804            functions,
805            module_startup_func: builder.startup_func,
806            module_shutdown_func: builder.shutdown_func,
807            request_startup_func: builder.request_startup_func,
808            request_shutdown_func: builder.request_shutdown_func,
809            info_func: builder.info_func,
810            version,
811            globals_size: builder.globals_size,
812            globals_ptr: builder.globals_ptr,
813            globals_ctor: builder.globals_ctor,
814            globals_dtor: builder.globals_dtor,
815            post_deactivate_func: builder.post_deactivate_func,
816            module_started: 0,
817            type_: 0,
818            handle: ptr::null_mut(),
819            module_number: 0,
820            build_id: unsafe { ext_php_rs_php_build_id() },
821        };
822
823        #[cfg(php_zts)]
824        let module_entry = ModuleEntry {
825            size: mem::size_of::<ModuleEntry>().try_into()?,
826            zend_api: ZEND_MODULE_API_NO,
827            zend_debug: u8::from(PHP_DEBUG),
828            zts: u8::from(PHP_ZTS),
829            ini_entry: ptr::null(),
830            deps: ptr::null(),
831            name,
832            functions,
833            module_startup_func: builder.startup_func,
834            module_shutdown_func: builder.shutdown_func,
835            request_startup_func: builder.request_startup_func,
836            request_shutdown_func: builder.request_shutdown_func,
837            info_func: builder.info_func,
838            version,
839            globals_size: builder.globals_size,
840            globals_id_ptr: builder.globals_id_ptr,
841            globals_ctor: builder.globals_ctor,
842            globals_dtor: builder.globals_dtor,
843            post_deactivate_func: builder.post_deactivate_func,
844            module_started: 0,
845            type_: 0,
846            handle: ptr::null_mut(),
847            module_number: 0,
848            build_id: unsafe { ext_php_rs_php_build_id() },
849        };
850
851        Ok((
852            module_entry,
853            startup,
854            ModuleAllocations {
855                functions: function_table,
856                arg_info: arg_info.into_boxed_slice(),
857                name: owned_name,
858                version: owned_version,
859            },
860        ))
861    }
862}
863
864#[cfg(test)]
865mod tests {
866    use crate::test::{
867        test_deactivate_function, test_function, test_info_function, test_startup_shutdown_function,
868    };
869
870    use super::*;
871
872    #[test]
873    fn test_new() {
874        let builder = ModuleBuilder::new("test", "1.0");
875        assert_eq!(builder.name, "test");
876        assert_eq!(builder.version, "1.0");
877        assert!(builder.functions.is_empty());
878        assert!(builder.constants.is_empty());
879        assert_eq!(builder.classes.len(), 0);
880        assert_eq!(builder.interfaces.len(), 0);
881        assert!(builder.startup_func.is_none());
882        assert!(builder.shutdown_func.is_none());
883        assert!(builder.request_startup_func.is_none());
884        assert!(builder.request_shutdown_func.is_none());
885        assert!(builder.post_deactivate_func.is_none());
886        assert!(builder.info_func.is_none());
887        #[cfg(feature = "enum")]
888        assert_eq!(builder.enums.len(), 0);
889    }
890
891    #[test]
892    fn functions_that_differ_only_by_case_fail_the_module_build() {
893        let builder = ModuleBuilder::new("test", "1.0")
894            .function(FunctionBuilder::new("say_hello", test_function))
895            .function(FunctionBuilder::new("Say_Hello", test_function));
896        let built = <(ModuleEntry, ModuleStartup, ModuleAllocations)>::try_from(builder);
897        assert!(matches!(
898            built,
899            Err(crate::error::Error::DuplicateFunction { function }) if function == "Say_Hello"
900        ));
901    }
902
903    #[test]
904    fn test_name() {
905        let builder = ModuleBuilder::new("test", "1.0").name("new_test");
906        assert_eq!(builder.name, "new_test");
907    }
908
909    #[test]
910    fn test_version() {
911        let builder = ModuleBuilder::new("test", "1.0").version("2.0");
912        assert_eq!(builder.version, "2.0");
913    }
914
915    #[test]
916    fn test_startup_function() {
917        let builder =
918            ModuleBuilder::new("test", "1.0").startup_function(test_startup_shutdown_function);
919        assert!(builder.startup_func.is_some());
920    }
921
922    #[test]
923    fn test_shutdown_function() {
924        let builder =
925            ModuleBuilder::new("test", "1.0").shutdown_function(test_startup_shutdown_function);
926        assert!(builder.shutdown_func.is_some());
927    }
928
929    #[test]
930    fn test_request_startup_function() {
931        let builder = ModuleBuilder::new("test", "1.0")
932            .request_startup_function(test_startup_shutdown_function);
933        assert!(builder.request_startup_func.is_some());
934    }
935
936    #[test]
937    fn test_request_shutdown_function() {
938        let builder = ModuleBuilder::new("test", "1.0")
939            .request_shutdown_function(test_startup_shutdown_function);
940        assert!(builder.request_shutdown_func.is_some());
941    }
942
943    #[test]
944    fn test_set_post_deactivate_function() {
945        let builder =
946            ModuleBuilder::new("test", "1.0").post_deactivate_function(test_deactivate_function);
947        assert!(builder.post_deactivate_func.is_some());
948    }
949
950    #[test]
951    fn test_set_info_function() {
952        let builder = ModuleBuilder::new("test", "1.0").info_function(test_info_function);
953        assert!(builder.info_func.is_some());
954    }
955
956    #[test]
957    fn test_add_function() {
958        let builder =
959            ModuleBuilder::new("test", "1.0").function(FunctionBuilder::new("test", test_function));
960        assert_eq!(builder.functions.len(), 1);
961    }
962
963    #[test]
964    #[cfg(feature = "embed")]
965    fn test_add_constant() {
966        let builder =
967            ModuleBuilder::new("test", "1.0").constant(("TEST_CONST", 42, DocComments::default()));
968        assert_eq!(builder.constants.len(), 1);
969        assert_eq!(builder.constants[0].0, "TEST_CONST");
970        // TODO: Check if the value is 42
971        assert_eq!(builder.constants[0].2, DocComments::default());
972    }
973}