ext_php_rs/builders/
module.rs

1use std::{convert::TryFrom, ffi::CString, mem, ptr};
2
3use super::{ClassBuilder, FunctionBuilder};
4#[cfg(feature = "enum")]
5use crate::{builders::enum_builder::EnumBuilder, enum_::RegisteredEnum};
6use crate::{
7    class::RegisteredClass,
8    constant::IntoConst,
9    describe::DocComments,
10    error::Result,
11    ffi::{ext_php_rs_php_build_id, ZEND_MODULE_API_NO},
12    zend::{FunctionEntry, ModuleEntry},
13    PHP_DEBUG, PHP_ZTS,
14};
15
16/// Builds a Zend module extension to be registered with PHP. Must be called
17/// from within an external function called `get_module`, returning a mutable
18/// pointer to a `ModuleEntry`.
19///
20/// ```rust,no_run
21/// use ext_php_rs::{
22///     builders::ModuleBuilder,
23///     zend::ModuleEntry,
24///     info_table_start, info_table_end, info_table_row
25/// };
26///
27/// #[no_mangle]
28/// pub extern "C" fn php_module_info(_module: *mut ModuleEntry) {
29///     info_table_start!();
30///     info_table_row!("column 1", "column 2");
31///     info_table_end!();
32/// }
33///
34/// #[no_mangle]
35/// pub extern "C" fn get_module() -> *mut ModuleEntry {
36///     let (entry, _) = ModuleBuilder::new("ext-name", "ext-version")
37///         .info_function(php_module_info)
38///         .try_into()
39///         .unwrap();
40///     entry.into_raw()
41/// }
42/// ```
43#[must_use]
44#[derive(Debug, Default)]
45pub struct ModuleBuilder<'a> {
46    pub(crate) name: String,
47    pub(crate) version: String,
48    pub(crate) functions: Vec<FunctionBuilder<'a>>,
49    pub(crate) constants: Vec<(String, Box<dyn IntoConst + Send>, DocComments)>,
50    pub(crate) classes: Vec<fn() -> ClassBuilder>,
51    #[cfg(feature = "enum")]
52    pub(crate) enums: Vec<fn() -> EnumBuilder>,
53    startup_func: Option<StartupShutdownFunc>,
54    shutdown_func: Option<StartupShutdownFunc>,
55    request_startup_func: Option<StartupShutdownFunc>,
56    request_shutdown_func: Option<StartupShutdownFunc>,
57    post_deactivate_func: Option<unsafe extern "C" fn() -> i32>,
58    info_func: Option<InfoFunc>,
59}
60
61impl ModuleBuilder<'_> {
62    /// Creates a new module builder with a given name and version.
63    ///
64    /// # Arguments
65    ///
66    /// * `name` - The name of the extension.
67    /// * `version` - The current version of the extension.
68    pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
69        Self {
70            name: name.into(),
71            version: version.into(),
72            functions: vec![],
73            constants: vec![],
74            classes: vec![],
75            ..Default::default()
76        }
77    }
78
79    /// Overrides module name.
80    ///
81    /// # Arguments
82    ///
83    /// * `name` - The name of the extension.
84    pub fn name(mut self, name: impl Into<String>) -> Self {
85        self.name = name.into();
86        self
87    }
88
89    /// Overrides module version.
90    ///
91    /// # Arguments
92    ///
93    /// * `version` - The current version of the extension.
94    pub fn version(mut self, version: impl Into<String>) -> Self {
95        self.version = version.into();
96        self
97    }
98
99    /// Sets the startup function for the extension.
100    ///
101    /// # Arguments
102    ///
103    /// * `func` - The function to be called on startup.
104    pub fn startup_function(mut self, func: StartupShutdownFunc) -> Self {
105        self.startup_func = Some(func);
106        self
107    }
108
109    /// Sets the shutdown function for the extension.
110    ///
111    /// # Arguments
112    ///
113    /// * `func` - The function to be called on shutdown.
114    pub fn shutdown_function(mut self, func: StartupShutdownFunc) -> Self {
115        self.shutdown_func = Some(func);
116        self
117    }
118
119    /// Sets the request startup function for the extension.
120    ///
121    /// # Arguments
122    ///
123    /// * `func` - The function to be called when startup is requested.
124    pub fn request_startup_function(mut self, func: StartupShutdownFunc) -> Self {
125        self.request_startup_func = Some(func);
126        self
127    }
128
129    /// Sets the request shutdown function for the extension.
130    ///
131    /// # Arguments
132    ///
133    /// * `func` - The function to be called when shutdown is requested.
134    pub fn request_shutdown_function(mut self, func: StartupShutdownFunc) -> Self {
135        self.request_shutdown_func = Some(func);
136        self
137    }
138
139    /// Sets the post request shutdown function for the extension.
140    ///
141    /// This function can be useful if you need to do any final cleanup at the
142    /// very end of a request, after all other resources have been released. For
143    /// example, if your extension creates any persistent resources that last
144    /// beyond a single request, you could use this function to clean those up.
145    /// # Arguments
146    ///
147    /// * `func` - The function to be called when shutdown is requested.
148    pub fn post_deactivate_function(mut self, func: unsafe extern "C" fn() -> i32) -> Self {
149        self.post_deactivate_func = Some(func);
150        self
151    }
152
153    /// Sets the extension information function for the extension.
154    ///
155    /// # Arguments
156    ///
157    /// * `func` - The function to be called to retrieve the information about
158    ///   the extension.
159    pub fn info_function(mut self, func: InfoFunc) -> Self {
160        self.info_func = Some(func);
161        self
162    }
163
164    /// Adds a function to the extension.
165    ///
166    /// # Arguments
167    ///
168    /// * `func` - The function to be added to the extension.
169    pub fn function(mut self, func: FunctionBuilder<'static>) -> Self {
170        self.functions.push(func);
171        self
172    }
173
174    /// Adds a constant to the extension.
175    ///
176    /// # Arguments
177    ///
178    /// * `const` - Tuple containing the name, value and doc comments for the
179    ///   constant. This is a tuple to support the [`wrap_constant`] macro.
180    ///
181    /// [`wrap_constant`]: crate::wrap_constant
182    pub fn constant(
183        mut self,
184        r#const: (&str, impl IntoConst + Send + 'static, DocComments),
185    ) -> Self {
186        let (name, val, docs) = r#const;
187        self.constants.push((
188            name.into(),
189            Box::new(val) as Box<dyn IntoConst + Send>,
190            docs,
191        ));
192        self
193    }
194
195    /// Adds a class to the extension.
196    ///
197    /// # Panics
198    ///
199    /// * Panics if a constant could not be registered.
200    pub fn class<T: RegisteredClass>(mut self) -> Self {
201        self.classes.push(|| {
202            let mut builder = ClassBuilder::new(T::CLASS_NAME);
203            for (method, flags) in T::method_builders() {
204                builder = builder.method(method, flags);
205            }
206            if let Some(parent) = T::EXTENDS {
207                builder = builder.extends(parent);
208            }
209            for interface in T::IMPLEMENTS {
210                builder = builder.implements(*interface);
211            }
212            for (name, value, docs) in T::constants() {
213                builder = builder
214                    .dyn_constant(*name, *value, docs)
215                    .expect("Failed to register constant");
216            }
217            for (name, prop_info) in T::get_properties() {
218                builder = builder.property(name, prop_info.flags, prop_info.docs);
219            }
220            if let Some(modifier) = T::BUILDER_MODIFIER {
221                builder = modifier(builder);
222            }
223
224            builder
225                .object_override::<T>()
226                .registration(|ce| {
227                    T::get_metadata().set_ce(ce);
228                })
229                .docs(T::DOC_COMMENTS)
230        });
231        self
232    }
233
234    /// Adds an enum to the extension.
235    #[cfg(feature = "enum")]
236    pub fn enumeration<T>(mut self) -> Self
237    where
238        T: RegisteredClass + RegisteredEnum,
239    {
240        self.enums.push(|| {
241            let mut builder = EnumBuilder::new(T::CLASS_NAME);
242            for case in T::CASES {
243                builder = builder.case(case);
244            }
245            for (method, flags) in T::method_builders() {
246                builder = builder.method(method, flags);
247            }
248
249            builder
250                .registration(|ce| {
251                    T::get_metadata().set_ce(ce);
252                })
253                .docs(T::DOC_COMMENTS)
254        });
255
256        self
257    }
258}
259
260/// Artifacts from the [`ModuleBuilder`] that should be revisited inside the
261/// extension startup function.
262pub struct ModuleStartup {
263    constants: Vec<(String, Box<dyn IntoConst + Send>)>,
264    classes: Vec<fn() -> ClassBuilder>,
265    #[cfg(feature = "enum")]
266    enums: Vec<fn() -> EnumBuilder>,
267}
268
269impl ModuleStartup {
270    /// Completes startup of the module. Should only be called inside the module
271    /// startup function.
272    ///
273    /// # Errors
274    ///
275    /// * Returns an error if a constant could not be registered.
276    ///
277    /// # Panics
278    ///
279    /// * Panics if a class could not be registered.
280    pub fn startup(self, _ty: i32, mod_num: i32) -> Result<()> {
281        for (name, val) in self.constants {
282            val.register_constant(&name, mod_num)?;
283        }
284
285        self.classes.into_iter().map(|c| c()).for_each(|c| {
286            c.register().expect("Failed to build class");
287        });
288
289        #[cfg(feature = "enum")]
290        self.enums
291            .into_iter()
292            .map(|builder| builder())
293            .for_each(|e| {
294                e.register().expect("Failed to build enum");
295            });
296
297        Ok(())
298    }
299}
300
301/// A function to be called when the extension is starting up or shutting down.
302pub type StartupShutdownFunc = unsafe extern "C" fn(_type: i32, _module_number: i32) -> i32;
303
304/// A function to be called when `phpinfo();` is called.
305pub type InfoFunc = unsafe extern "C" fn(zend_module: *mut ModuleEntry);
306
307/// Builds a [`ModuleEntry`] and [`ModuleStartup`] from a [`ModuleBuilder`].
308/// This is the entry point for the module to be registered with PHP.
309impl TryFrom<ModuleBuilder<'_>> for (ModuleEntry, ModuleStartup) {
310    type Error = crate::error::Error;
311
312    fn try_from(builder: ModuleBuilder) -> Result<Self, Self::Error> {
313        let mut functions = builder
314            .functions
315            .into_iter()
316            .map(FunctionBuilder::build)
317            .collect::<Result<Vec<_>>>()?;
318        functions.push(FunctionEntry::end());
319        let functions = Box::into_raw(functions.into_boxed_slice()) as *const FunctionEntry;
320
321        let name = CString::new(builder.name)?.into_raw();
322        let version = CString::new(builder.version)?.into_raw();
323
324        let startup = ModuleStartup {
325            constants: builder
326                .constants
327                .into_iter()
328                .map(|(n, v, _)| (n, v))
329                .collect(),
330            classes: builder.classes,
331            #[cfg(feature = "enum")]
332            enums: builder.enums,
333        };
334
335        Ok((
336            ModuleEntry {
337                size: mem::size_of::<ModuleEntry>().try_into()?,
338                zend_api: ZEND_MODULE_API_NO,
339                zend_debug: u8::from(PHP_DEBUG),
340                zts: u8::from(PHP_ZTS),
341                ini_entry: ptr::null(),
342                deps: ptr::null(),
343                name,
344                functions,
345                module_startup_func: builder.startup_func,
346                module_shutdown_func: builder.shutdown_func,
347                request_startup_func: builder.request_startup_func,
348                request_shutdown_func: builder.request_shutdown_func,
349                info_func: builder.info_func,
350                version,
351                globals_size: 0,
352                #[cfg(not(php_zts))]
353                globals_ptr: ptr::null_mut(),
354                #[cfg(php_zts)]
355                globals_id_ptr: ptr::null_mut(),
356                globals_ctor: None,
357                globals_dtor: None,
358                post_deactivate_func: builder.post_deactivate_func,
359                module_started: 0,
360                type_: 0,
361                handle: ptr::null_mut(),
362                module_number: 0,
363                build_id: unsafe { ext_php_rs_php_build_id() },
364            },
365            startup,
366        ))
367    }
368}
369
370#[cfg(test)]
371mod tests {
372    use crate::test::{
373        test_deactivate_function, test_function, test_info_function, test_startup_shutdown_function,
374    };
375
376    use super::*;
377
378    #[test]
379    fn test_new() {
380        let builder = ModuleBuilder::new("test", "1.0");
381        assert_eq!(builder.name, "test");
382        assert_eq!(builder.version, "1.0");
383        assert!(builder.functions.is_empty());
384        assert!(builder.constants.is_empty());
385        assert!(builder.classes.is_empty());
386        assert!(builder.startup_func.is_none());
387        assert!(builder.shutdown_func.is_none());
388        assert!(builder.request_startup_func.is_none());
389        assert!(builder.request_shutdown_func.is_none());
390        assert!(builder.post_deactivate_func.is_none());
391        assert!(builder.info_func.is_none());
392        #[cfg(feature = "enum")]
393        assert!(builder.enums.is_empty());
394    }
395
396    #[test]
397    fn test_name() {
398        let builder = ModuleBuilder::new("test", "1.0").name("new_test");
399        assert_eq!(builder.name, "new_test");
400    }
401
402    #[test]
403    fn test_version() {
404        let builder = ModuleBuilder::new("test", "1.0").version("2.0");
405        assert_eq!(builder.version, "2.0");
406    }
407
408    #[test]
409    fn test_startup_function() {
410        let builder =
411            ModuleBuilder::new("test", "1.0").startup_function(test_startup_shutdown_function);
412        assert!(builder.startup_func.is_some());
413    }
414
415    #[test]
416    fn test_shutdown_function() {
417        let builder =
418            ModuleBuilder::new("test", "1.0").shutdown_function(test_startup_shutdown_function);
419        assert!(builder.shutdown_func.is_some());
420    }
421
422    #[test]
423    fn test_request_startup_function() {
424        let builder = ModuleBuilder::new("test", "1.0")
425            .request_startup_function(test_startup_shutdown_function);
426        assert!(builder.request_startup_func.is_some());
427    }
428
429    #[test]
430    fn test_request_shutdown_function() {
431        let builder = ModuleBuilder::new("test", "1.0")
432            .request_shutdown_function(test_startup_shutdown_function);
433        assert!(builder.request_shutdown_func.is_some());
434    }
435
436    #[test]
437    fn test_set_post_deactivate_function() {
438        let builder =
439            ModuleBuilder::new("test", "1.0").post_deactivate_function(test_deactivate_function);
440        assert!(builder.post_deactivate_func.is_some());
441    }
442
443    #[test]
444    fn test_set_info_function() {
445        let builder = ModuleBuilder::new("test", "1.0").info_function(test_info_function);
446        assert!(builder.info_func.is_some());
447    }
448
449    #[test]
450    fn test_add_function() {
451        let builder =
452            ModuleBuilder::new("test", "1.0").function(FunctionBuilder::new("test", test_function));
453        assert_eq!(builder.functions.len(), 1);
454    }
455
456    #[test]
457    #[cfg(feature = "embed")]
458    fn test_add_constant() {
459        let builder =
460            ModuleBuilder::new("test", "1.0").constant(("TEST_CONST", 42, DocComments::default()));
461        assert_eq!(builder.constants.len(), 1);
462        assert_eq!(builder.constants[0].0, "TEST_CONST");
463        // TODO: Check if the value is 42
464        assert_eq!(builder.constants[0].2, DocComments::default());
465    }
466}