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 globally during MINIT to create
250    /// a singleton observer instance shared across all requests and threads.
251    /// The observer must be `Send + Sync` as it may be accessed concurrently
252    /// in ZTS builds.
253    ///
254    /// # Arguments
255    ///
256    /// * `factory` - A function that creates an observer instance
257    ///
258    /// # Example
259    ///
260    /// ```ignore
261    /// use ext_php_rs::prelude::*;
262    /// use ext_php_rs::zend::{FcallObserver, FcallInfo, ExecuteData};
263    /// use ext_php_rs::types::Zval;
264    ///
265    /// struct MyProfiler;
266    ///
267    /// impl FcallObserver for MyProfiler {
268    ///     fn should_observe(&self, info: &FcallInfo) -> bool {
269    ///         !info.is_internal
270    ///     }
271    ///     fn begin(&self, _: &ExecuteData) {}
272    ///     fn end(&self, _: &ExecuteData, _: Option<&Zval>) {}
273    /// }
274    ///
275    /// #[php_module]
276    /// pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
277    ///     module.fcall_observer(|| MyProfiler)
278    /// }
279    /// ```
280    ///
281    /// # Panics
282    ///
283    /// Panics if called more than once on the same module.
284    #[cfg(feature = "observer")]
285    pub fn fcall_observer<F, O>(self, factory: F) -> Self
286    where
287        F: Fn() -> O + Send + Sync + 'static,
288        O: crate::zend::FcallObserver + Send + Sync,
289    {
290        let boxed_factory: Box<
291            dyn Fn() -> Box<dyn crate::zend::FcallObserver + Send + Sync> + Send + Sync,
292        > = Box::new(move || Box::new(factory()));
293        crate::zend::observer::register_fcall_observer_factory(boxed_factory);
294        self
295    }
296
297    /// Registers an error observer for monitoring PHP errors.
298    ///
299    /// The factory function is called once during MINIT to create
300    /// a singleton observer instance shared across all requests.
301    /// The observer must be `Send + Sync` for ZTS builds.
302    ///
303    /// # Arguments
304    ///
305    /// * `factory` - A function that creates an observer instance
306    ///
307    /// # Example
308    ///
309    /// ```ignore
310    /// use ext_php_rs::prelude::*;
311    ///
312    /// struct MyErrorLogger;
313    ///
314    /// impl ErrorObserver for MyErrorLogger {
315    ///     fn should_observe(&self, error_type: ErrorType) -> bool {
316    ///         ErrorType::Fatal.contains(error_type)
317    ///     }
318    ///
319    ///     fn on_error(&self, error: &ErrorInfo) {
320    ///         eprintln!("[{}:{}] {}",
321    ///             error.filename.unwrap_or("<unknown>"),
322    ///             error.lineno,
323    ///             error.message
324    ///         );
325    ///     }
326    /// }
327    ///
328    /// #[php_module]
329    /// pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
330    ///     module.error_observer(MyErrorLogger)
331    /// }
332    /// ```
333    ///
334    /// # Panics
335    ///
336    /// Panics if called more than once on the same module.
337    #[cfg(feature = "observer")]
338    pub fn error_observer<F, O>(self, factory: F) -> Self
339    where
340        F: Fn() -> O + Send + Sync + 'static,
341        O: crate::zend::ErrorObserver + Send + Sync,
342    {
343        let boxed_factory: Box<
344            dyn Fn() -> Box<dyn crate::zend::ErrorObserver + Send + Sync> + Send + Sync,
345        > = Box::new(move || Box::new(factory()));
346        crate::zend::error_observer::register_error_observer_factory(boxed_factory);
347        self
348    }
349
350    /// Registers an exception observer for monitoring thrown PHP exceptions.
351    ///
352    /// The factory function is called once during MINIT to create
353    /// a singleton observer instance shared across all requests.
354    /// The observer must be `Send + Sync` for ZTS builds.
355    ///
356    /// The observer is called at throw time, before any catch blocks are
357    /// evaluated.
358    ///
359    /// # Arguments
360    ///
361    /// * `factory` - A function that creates an observer instance
362    ///
363    /// # Example
364    ///
365    /// ```ignore
366    /// use ext_php_rs::prelude::*;
367    ///
368    /// struct MyExceptionLogger;
369    ///
370    /// impl ExceptionObserver for MyExceptionLogger {
371    ///     fn on_exception(&self, exception: &ExceptionInfo) {
372    ///         eprintln!("[EXCEPTION] {}: {} at {}:{}",
373    ///             exception.class_name,
374    ///             exception.message.as_deref().unwrap_or("<no message>"),
375    ///             exception.file.as_deref().unwrap_or("<unknown>"),
376    ///             exception.line
377    ///         );
378    ///     }
379    /// }
380    ///
381    /// #[php_module]
382    /// pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
383    ///     module.exception_observer(|| MyExceptionLogger)
384    /// }
385    /// ```
386    ///
387    /// # Panics
388    ///
389    /// Panics if called more than once on the same module.
390    #[cfg(feature = "observer")]
391    pub fn exception_observer<F, O>(self, factory: F) -> Self
392    where
393        F: Fn() -> O + Send + Sync + 'static,
394        O: crate::zend::ExceptionObserver + Send + Sync,
395    {
396        let boxed_factory: Box<
397            dyn Fn() -> Box<dyn crate::zend::ExceptionObserver + Send + Sync> + Send + Sync,
398        > = Box::new(move || Box::new(factory()));
399        crate::zend::exception_observer::register_exception_observer_factory(boxed_factory);
400        self
401    }
402
403    /// Adds a function to the extension.
404    ///
405    /// # Arguments
406    ///
407    /// * `func` - The function to be added to the extension.
408    pub fn function(mut self, func: FunctionBuilder<'static>) -> Self {
409        self.functions.push(func);
410        self
411    }
412
413    /// Adds a constant to the extension.
414    ///
415    /// # Arguments
416    ///
417    /// * `const` - Tuple containing the name, value and doc comments for the
418    ///   constant. This is a tuple to support the [`wrap_constant`] macro.
419    ///
420    /// [`wrap_constant`]: crate::wrap_constant
421    pub fn constant(
422        mut self,
423        r#const: (&str, impl IntoConst + Send + 'static, DocComments),
424    ) -> Self {
425        let (name, val, docs) = r#const;
426        self.constants.push((
427            name.into(),
428            Box::new(val) as Box<dyn IntoConst + Send>,
429            docs,
430        ));
431        self
432    }
433
434    /// Adds a interface to the extension.
435    ///
436    /// # Panics
437    ///
438    /// * Panics if a constant could not be registered.
439    #[expect(
440        clippy::expect_used,
441        reason = "registration runs in MINIT, where a failure is an extension bug the engine cannot recover from"
442    )]
443    pub fn interface<T: RegisteredClass>(mut self) -> Self {
444        self.interfaces.push(|| {
445            let mut builder = ClassBuilder::new(T::CLASS_NAME);
446            for (method, flags) in T::method_builders() {
447                builder = builder.method(method, flags);
448            }
449            for interface in T::IMPLEMENTS {
450                builder = builder.implements(*interface);
451            }
452            for (name, value, docs, flags) in T::constants() {
453                builder = builder
454                    .dyn_constant(*name, *value, docs, *flags)
455                    .expect("Failed to register constant");
456            }
457
458            if let Some(modifier) = T::BUILDER_MODIFIER {
459                builder = modifier(builder);
460            }
461
462            builder = builder.flags(ClassFlags::Interface);
463            // Note: interfaces should NOT have object_override because they cannot be
464            // instantiated
465            builder
466                .registration(|ce, arg_info| {
467                    T::get_metadata().set_ce(ce);
468                    T::get_metadata().set_arg_info(arg_info);
469                })
470                .docs(T::DOC_COMMENTS)
471        });
472        self
473    }
474
475    /// Adds a class to the extension.
476    ///
477    /// # Panics
478    ///
479    /// * Panics if a constant could not be registered.
480    #[expect(
481        clippy::expect_used,
482        reason = "registration runs in MINIT, where a failure is an extension bug the engine cannot recover from"
483    )]
484    pub fn class<T: RegisteredClass>(mut self) -> Self {
485        self.classes.push(|| {
486            let mut builder = ClassBuilder::new(T::CLASS_NAME);
487            for (method, flags) in T::method_builders() {
488                builder = builder.method(method, flags);
489            }
490            // Methods from #[php_impl_interface] trait implementations.
491            // Uses the inventory crate for cross-crate method discovery.
492            for (method, flags) in T::interface_method_implementations() {
493                builder = builder.method(method, flags);
494            }
495            if let Some(parent) = T::EXTENDS {
496                builder = builder.extends(parent);
497            }
498            // Interfaces declared via #[php(implements(...))] attribute
499            for interface in T::IMPLEMENTS {
500                builder = builder.implements(*interface);
501            }
502            // Interfaces from #[php_impl_interface] trait implementations.
503            // Uses the inventory crate for cross-crate interface discovery.
504            for interface in T::interface_implementations() {
505                builder = builder.implements(interface);
506            }
507            for (name, value, docs, flags) in T::constants() {
508                builder = builder
509                    .dyn_constant(*name, *value, docs, *flags)
510                    .expect("Failed to register constant");
511            }
512            for desc in T::get_metadata().all_properties() {
513                let default_stub = if desc.nullable {
514                    Some("null".into())
515                } else {
516                    None
517                };
518                builder = builder.property(crate::builders::ClassProperty {
519                    name: desc.name.into(),
520                    flags: desc.flags,
521                    default: None,
522                    docs: desc.docs,
523                    ty: Some(desc.ty),
524                    nullable: desc.nullable,
525                    readonly: desc.readonly,
526                    default_stub,
527                });
528            }
529            for (name, flags, default, docs) in T::static_properties() {
530                let default_stub = default.map(crate::convert::IntoZvalDyn::stub_value);
531                let default_fn = default.map(|v| {
532                    Box::new(move || v.as_zval(true))
533                        as Box<dyn FnOnce() -> crate::error::Result<crate::types::Zval>>
534                });
535                builder = builder.property(crate::builders::ClassProperty {
536                    name: (*name).into(),
537                    flags: *flags,
538                    default: default_fn,
539                    docs,
540                    ty: None,
541                    nullable: false,
542                    readonly: false,
543                    default_stub,
544                });
545            }
546            if let Some(modifier) = T::BUILDER_MODIFIER {
547                builder = modifier(builder);
548            }
549
550            builder
551                .flags(T::FLAGS)
552                .object_override::<T>()
553                .registration(|ce, arg_info| {
554                    T::get_metadata().set_ce(ce);
555                    T::get_metadata().set_arg_info(arg_info);
556                })
557                .docs(T::DOC_COMMENTS)
558        });
559        self
560    }
561
562    /// Adds an enum to the extension.
563    #[cfg(feature = "enum")]
564    pub fn enumeration<T>(mut self) -> Self
565    where
566        T: RegisteredClass + RegisteredEnum,
567    {
568        self.enums.push(|| {
569            let mut builder = EnumBuilder::new(T::CLASS_NAME);
570            for case in T::CASES {
571                builder = builder.case(case);
572            }
573            for (method, flags) in T::method_builders() {
574                builder = builder.method(method, flags);
575            }
576
577            builder
578                .registration(|ce, arg_info| {
579                    T::get_metadata().set_ce(ce);
580                    T::get_metadata().set_arg_info(arg_info);
581                })
582                .docs(T::DOC_COMMENTS)
583        });
584
585        self
586    }
587}
588
589#[cfg(feature = "observer")]
590impl<'a> ModuleBuilder<'a> {
591    /// Register a [`ZendExtensionHandler`] and configure which hooks PHP should
592    /// call.
593    ///
594    /// Returns a [`ZendExtensionBuilder`] that exposes three opt-in methods
595    /// (`hook_statements`, `hook_fcalls`, `hook_op_array_compile`). Call
596    /// [`ZendExtensionBuilder::finish`] to return to `ModuleBuilder`.
597    ///
598    /// # Example
599    ///
600    /// ```ignore
601    /// module
602    ///     .zend_extension(|| MyProfiler::new())
603    ///     .hook_statements()
604    ///     .finish()
605    /// ```
606    ///
607    /// # Panics
608    ///
609    /// Panics if called more than once on the same module.
610    ///
611    /// [`ZendExtensionHandler`]: crate::zend::ZendExtensionHandler
612    /// [`ZendExtensionBuilder`]: crate::zend::ZendExtensionBuilder
613    /// [`ZendExtensionBuilder::finish`]: crate::zend::ZendExtensionBuilder::finish
614    pub fn zend_extension<F, H>(
615        self,
616        factory: F,
617    ) -> crate::zend::zend_extension::ZendExtensionBuilder<'a>
618    where
619        F: Fn() -> H + Send + Sync + 'static,
620        H: crate::zend::ZendExtensionHandler,
621    {
622        crate::zend::zend_extension::ZendExtensionBuilder::new(self, factory)
623    }
624}
625
626/// Artifacts from the [`ModuleBuilder`] that should be revisited inside the
627/// extension startup function.
628pub struct ModuleStartup {
629    #[cfg(feature = "observer")]
630    name: String,
631    #[cfg(feature = "observer")]
632    version: String,
633    constants: Vec<(String, Box<dyn IntoConst + Send>)>,
634    classes: Vec<fn() -> ClassBuilder>,
635    interfaces: Vec<fn() -> ClassBuilder>,
636    #[cfg(feature = "enum")]
637    enums: Vec<fn() -> EnumBuilder>,
638}
639
640impl ModuleStartup {
641    /// Completes startup of the module. Should only be called inside the module
642    /// startup function.
643    ///
644    /// # Errors
645    ///
646    /// * Returns an error if a constant, interface, class or enum could not be
647    ///   registered. The generated MINIT then returns `FAILURE` and PHP refuses
648    ///   to start the module.
649    pub fn startup(self, _ty: i32, mod_num: i32) -> Result<()> {
650        for (name, val) in self.constants {
651            val.register_constant(&name, mod_num)?;
652        }
653
654        // Interfaces must be registered before classes so that classes can implement
655        // them
656        self.interfaces
657            .into_iter()
658            .try_for_each(|c| c().register())?;
659
660        self.classes.into_iter().try_for_each(|c| c().register())?;
661
662        #[cfg(feature = "enum")]
663        self.enums
664            .into_iter()
665            .try_for_each(|builder| builder().register())?;
666
667        // Initialize observer systems if registered
668        #[cfg(feature = "observer")]
669        unsafe {
670            crate::zend::observer::observer_startup();
671            crate::zend::error_observer::error_observer_startup();
672            crate::zend::exception_observer::exception_observer_startup();
673            crate::zend::zend_extension::zend_extension_startup(&self.name, &self.version);
674        }
675
676        Ok(())
677    }
678}
679
680/// A function to be called when the extension is starting up or shutting down.
681pub type StartupShutdownFunc = unsafe extern "C" fn(_type: i32, _module_number: i32) -> i32;
682
683/// A function to be called when `phpinfo();` is called.
684pub type InfoFunc = unsafe extern "C" fn(zend_module: *mut ModuleEntry);
685
686impl ModuleBuilder<'_> {
687    /// Checks that no two functions share a PHP name. The engine compares
688    /// function names ignoring ASCII case.
689    fn validate(&self) -> Result<()> {
690        match first_duplicate(self.functions.iter().map(|f| f.name.as_str()), true) {
691            Some(name) => Err(crate::error::Error::DuplicateFunction {
692                function: name.to_owned(),
693            }),
694            None => Ok(()),
695        }
696    }
697}
698
699/// Builds a [`ModuleEntry`] and [`ModuleStartup`] from a [`ModuleBuilder`].
700/// This is the entry point for the module to be registered with PHP.
701impl TryFrom<ModuleBuilder<'_>> for (ModuleEntry, ModuleStartup, ModuleAllocations) {
702    type Error = crate::error::Error;
703
704    fn try_from(builder: ModuleBuilder) -> Result<Self, Self::Error> {
705        builder.validate()?;
706        let mut arg_info = Vec::with_capacity(builder.functions.len());
707        let mut function_table = Vec::with_capacity(builder.functions.len() + 1);
708        for function in builder.functions {
709            let (entry, args) = function.build()?;
710            function_table.push(entry);
711            arg_info.push(args);
712        }
713        function_table.push(FunctionEntry::end());
714        let function_table = function_table.into_boxed_slice();
715        let functions = function_table.as_ptr();
716
717        #[cfg(feature = "observer")]
718        let ext_name = builder.name.clone();
719        #[cfg(feature = "observer")]
720        let ext_version = builder.version.clone();
721
722        let owned_name = CString::new(builder.name)?;
723        let owned_version = CString::new(builder.version)?;
724        let name = owned_name.as_ptr();
725        let version = owned_version.as_ptr();
726
727        let startup = ModuleStartup {
728            #[cfg(feature = "observer")]
729            name: ext_name,
730            #[cfg(feature = "observer")]
731            version: ext_version,
732            constants: builder
733                .constants
734                .into_iter()
735                .map(|(n, v, _)| (n, v))
736                .collect(),
737            classes: builder.classes,
738            interfaces: builder.interfaces,
739            #[cfg(feature = "enum")]
740            enums: builder.enums,
741        };
742
743        #[cfg(not(php_zts))]
744        let module_entry = ModuleEntry {
745            size: mem::size_of::<ModuleEntry>().try_into()?,
746            zend_api: ZEND_MODULE_API_NO,
747            zend_debug: u8::from(PHP_DEBUG),
748            zts: u8::from(PHP_ZTS),
749            ini_entry: ptr::null(),
750            deps: ptr::null(),
751            name,
752            functions,
753            module_startup_func: builder.startup_func,
754            module_shutdown_func: builder.shutdown_func,
755            request_startup_func: builder.request_startup_func,
756            request_shutdown_func: builder.request_shutdown_func,
757            info_func: builder.info_func,
758            version,
759            globals_size: builder.globals_size,
760            globals_ptr: builder.globals_ptr,
761            globals_ctor: builder.globals_ctor,
762            globals_dtor: builder.globals_dtor,
763            post_deactivate_func: builder.post_deactivate_func,
764            module_started: 0,
765            type_: 0,
766            handle: ptr::null_mut(),
767            module_number: 0,
768            build_id: unsafe { ext_php_rs_php_build_id() },
769        };
770
771        #[cfg(php_zts)]
772        let module_entry = ModuleEntry {
773            size: mem::size_of::<ModuleEntry>().try_into()?,
774            zend_api: ZEND_MODULE_API_NO,
775            zend_debug: u8::from(PHP_DEBUG),
776            zts: u8::from(PHP_ZTS),
777            ini_entry: ptr::null(),
778            deps: ptr::null(),
779            name,
780            functions,
781            module_startup_func: builder.startup_func,
782            module_shutdown_func: builder.shutdown_func,
783            request_startup_func: builder.request_startup_func,
784            request_shutdown_func: builder.request_shutdown_func,
785            info_func: builder.info_func,
786            version,
787            globals_size: builder.globals_size,
788            globals_id_ptr: builder.globals_id_ptr,
789            globals_ctor: builder.globals_ctor,
790            globals_dtor: builder.globals_dtor,
791            post_deactivate_func: builder.post_deactivate_func,
792            module_started: 0,
793            type_: 0,
794            handle: ptr::null_mut(),
795            module_number: 0,
796            build_id: unsafe { ext_php_rs_php_build_id() },
797        };
798
799        Ok((
800            module_entry,
801            startup,
802            ModuleAllocations {
803                functions: function_table,
804                arg_info: arg_info.into_boxed_slice(),
805                name: owned_name,
806                version: owned_version,
807            },
808        ))
809    }
810}
811
812#[cfg(test)]
813mod tests {
814    use crate::test::{
815        test_deactivate_function, test_function, test_info_function, test_startup_shutdown_function,
816    };
817
818    use super::*;
819
820    #[test]
821    fn test_new() {
822        let builder = ModuleBuilder::new("test", "1.0");
823        assert_eq!(builder.name, "test");
824        assert_eq!(builder.version, "1.0");
825        assert!(builder.functions.is_empty());
826        assert!(builder.constants.is_empty());
827        assert!(builder.classes.is_empty());
828        assert!(builder.interfaces.is_empty());
829        assert!(builder.startup_func.is_none());
830        assert!(builder.shutdown_func.is_none());
831        assert!(builder.request_startup_func.is_none());
832        assert!(builder.request_shutdown_func.is_none());
833        assert!(builder.post_deactivate_func.is_none());
834        assert!(builder.info_func.is_none());
835        #[cfg(feature = "enum")]
836        assert!(builder.enums.is_empty());
837    }
838
839    #[test]
840    fn functions_that_differ_only_by_case_fail_the_module_build() {
841        let builder = ModuleBuilder::new("test", "1.0")
842            .function(FunctionBuilder::new("say_hello", test_function))
843            .function(FunctionBuilder::new("Say_Hello", test_function));
844        let built = <(ModuleEntry, ModuleStartup, ModuleAllocations)>::try_from(builder);
845        assert!(matches!(
846            built,
847            Err(crate::error::Error::DuplicateFunction { function }) if function == "Say_Hello"
848        ));
849    }
850
851    #[test]
852    fn test_name() {
853        let builder = ModuleBuilder::new("test", "1.0").name("new_test");
854        assert_eq!(builder.name, "new_test");
855    }
856
857    #[test]
858    fn test_version() {
859        let builder = ModuleBuilder::new("test", "1.0").version("2.0");
860        assert_eq!(builder.version, "2.0");
861    }
862
863    #[test]
864    fn test_startup_function() {
865        let builder =
866            ModuleBuilder::new("test", "1.0").startup_function(test_startup_shutdown_function);
867        assert!(builder.startup_func.is_some());
868    }
869
870    #[test]
871    fn test_shutdown_function() {
872        let builder =
873            ModuleBuilder::new("test", "1.0").shutdown_function(test_startup_shutdown_function);
874        assert!(builder.shutdown_func.is_some());
875    }
876
877    #[test]
878    fn test_request_startup_function() {
879        let builder = ModuleBuilder::new("test", "1.0")
880            .request_startup_function(test_startup_shutdown_function);
881        assert!(builder.request_startup_func.is_some());
882    }
883
884    #[test]
885    fn test_request_shutdown_function() {
886        let builder = ModuleBuilder::new("test", "1.0")
887            .request_shutdown_function(test_startup_shutdown_function);
888        assert!(builder.request_shutdown_func.is_some());
889    }
890
891    #[test]
892    fn test_set_post_deactivate_function() {
893        let builder =
894            ModuleBuilder::new("test", "1.0").post_deactivate_function(test_deactivate_function);
895        assert!(builder.post_deactivate_func.is_some());
896    }
897
898    #[test]
899    fn test_set_info_function() {
900        let builder = ModuleBuilder::new("test", "1.0").info_function(test_info_function);
901        assert!(builder.info_func.is_some());
902    }
903
904    #[test]
905    fn test_add_function() {
906        let builder =
907            ModuleBuilder::new("test", "1.0").function(FunctionBuilder::new("test", test_function));
908        assert_eq!(builder.functions.len(), 1);
909    }
910
911    #[test]
912    #[cfg(feature = "embed")]
913    fn test_add_constant() {
914        let builder =
915            ModuleBuilder::new("test", "1.0").constant(("TEST_CONST", 42, DocComments::default()));
916        assert_eq!(builder.constants.len(), 1);
917        assert_eq!(builder.constants[0].0, "TEST_CONST");
918        // TODO: Check if the value is 42
919        assert_eq!(builder.constants[0].2, DocComments::default());
920    }
921}