ext_php_rs/builders/
module.rs

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