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