Skip to main content

ext_php_rs/builders/
module.rs

1use std::{convert::TryFrom, ffi::CString, mem, ptr};
2
3use super::{ClassBuilder, FunctionBuilder};
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, ModuleEntry},
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, _) = ModuleBuilder::new("ext-name", "ext-version")
40///             .info_function(php_module_info)
41///             .try_into()
42///             .unwrap();
43///         entry
44///     })
45/// }
46/// ```
47#[must_use]
48#[derive(Debug, Default)]
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}
65
66impl ModuleBuilder<'_> {
67    /// Creates a new module builder with a given name and version.
68    ///
69    /// # Arguments
70    ///
71    /// * `name` - The name of the extension.
72    /// * `version` - The current version of the extension.
73    pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
74        Self {
75            name: name.into(),
76            version: version.into(),
77            functions: vec![],
78            constants: vec![],
79            classes: vec![],
80            ..Default::default()
81        }
82    }
83
84    /// Overrides module name.
85    ///
86    /// # Arguments
87    ///
88    /// * `name` - The name of the extension.
89    pub fn name(mut self, name: impl Into<String>) -> Self {
90        self.name = name.into();
91        self
92    }
93
94    /// Overrides module version.
95    ///
96    /// # Arguments
97    ///
98    /// * `version` - The current version of the extension.
99    pub fn version(mut self, version: impl Into<String>) -> Self {
100        self.version = version.into();
101        self
102    }
103
104    /// Sets the startup function for the extension.
105    ///
106    /// # Arguments
107    ///
108    /// * `func` - The function to be called on startup.
109    pub fn startup_function(mut self, func: StartupShutdownFunc) -> Self {
110        self.startup_func = Some(func);
111        self
112    }
113
114    /// Sets the shutdown function for the extension.
115    ///
116    /// # Arguments
117    ///
118    /// * `func` - The function to be called on shutdown.
119    pub fn shutdown_function(mut self, func: StartupShutdownFunc) -> Self {
120        self.shutdown_func = Some(func);
121        self
122    }
123
124    /// Sets the request startup function for the extension.
125    ///
126    /// # Arguments
127    ///
128    /// * `func` - The function to be called when startup is requested.
129    pub fn request_startup_function(mut self, func: StartupShutdownFunc) -> Self {
130        self.request_startup_func = Some(func);
131        self
132    }
133
134    /// Sets the request shutdown function for the extension.
135    ///
136    /// # Arguments
137    ///
138    /// * `func` - The function to be called when shutdown is requested.
139    pub fn request_shutdown_function(mut self, func: StartupShutdownFunc) -> Self {
140        self.request_shutdown_func = Some(func);
141        self
142    }
143
144    /// Sets the post request shutdown function for the extension.
145    ///
146    /// This function can be useful if you need to do any final cleanup at the
147    /// very end of a request, after all other resources have been released. For
148    /// example, if your extension creates any persistent resources that last
149    /// beyond a single request, you could use this function to clean those up.
150    /// # Arguments
151    ///
152    /// * `func` - The function to be called when shutdown is requested.
153    pub fn post_deactivate_function(mut self, func: unsafe extern "C" fn() -> i32) -> Self {
154        self.post_deactivate_func = Some(func);
155        self
156    }
157
158    /// Sets the extension information function for the extension.
159    ///
160    /// # Arguments
161    ///
162    /// * `func` - The function to be called to retrieve the information about
163    ///   the extension.
164    pub fn info_function(mut self, func: InfoFunc) -> Self {
165        self.info_func = Some(func);
166        self
167    }
168
169    /// Registers a function call observer for profiling or tracing.
170    ///
171    /// The factory function is called once globally during MINIT to create
172    /// a singleton observer instance shared across all requests and threads.
173    /// The observer must be `Send + Sync` as it may be accessed concurrently
174    /// in ZTS builds.
175    ///
176    /// # Arguments
177    ///
178    /// * `factory` - A function that creates an observer instance
179    ///
180    /// # Example
181    ///
182    /// ```ignore
183    /// use ext_php_rs::prelude::*;
184    /// use ext_php_rs::zend::{FcallObserver, FcallInfo, ExecuteData};
185    /// use ext_php_rs::types::Zval;
186    ///
187    /// struct MyProfiler;
188    ///
189    /// impl FcallObserver for MyProfiler {
190    ///     fn should_observe(&self, info: &FcallInfo) -> bool {
191    ///         !info.is_internal
192    ///     }
193    ///     fn begin(&self, _: &ExecuteData) {}
194    ///     fn end(&self, _: &ExecuteData, _: Option<&Zval>) {}
195    /// }
196    ///
197    /// #[php_module]
198    /// pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
199    ///     module.fcall_observer(|| MyProfiler)
200    /// }
201    /// ```
202    ///
203    /// # Panics
204    ///
205    /// Panics if called more than once on the same module.
206    #[cfg(feature = "observer")]
207    pub fn fcall_observer<F, O>(self, factory: F) -> Self
208    where
209        F: Fn() -> O + Send + Sync + 'static,
210        O: crate::zend::FcallObserver + Send + Sync,
211    {
212        let boxed_factory: Box<
213            dyn Fn() -> Box<dyn crate::zend::FcallObserver + Send + Sync> + Send + Sync,
214        > = Box::new(move || Box::new(factory()));
215        crate::zend::observer::register_fcall_observer_factory(boxed_factory);
216        self
217    }
218
219    /// Registers an error observer for monitoring PHP errors.
220    ///
221    /// The factory function is called once during MINIT to create
222    /// a singleton observer instance shared across all requests.
223    /// The observer must be `Send + Sync` for ZTS builds.
224    ///
225    /// # Arguments
226    ///
227    /// * `factory` - A function that creates an observer instance
228    ///
229    /// # Example
230    ///
231    /// ```ignore
232    /// use ext_php_rs::prelude::*;
233    ///
234    /// struct MyErrorLogger;
235    ///
236    /// impl ErrorObserver for MyErrorLogger {
237    ///     fn should_observe(&self, error_type: ErrorType) -> bool {
238    ///         ErrorType::FATAL.contains(error_type)
239    ///     }
240    ///
241    ///     fn on_error(&self, error: &ErrorInfo) {
242    ///         eprintln!("[{}:{}] {}",
243    ///             error.filename.unwrap_or("<unknown>"),
244    ///             error.lineno,
245    ///             error.message
246    ///         );
247    ///     }
248    /// }
249    ///
250    /// #[php_module]
251    /// pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
252    ///     module.error_observer(MyErrorLogger)
253    /// }
254    /// ```
255    ///
256    /// # Panics
257    ///
258    /// Panics if called more than once on the same module.
259    #[cfg(feature = "observer")]
260    pub fn error_observer<F, O>(self, factory: F) -> Self
261    where
262        F: Fn() -> O + Send + Sync + 'static,
263        O: crate::zend::ErrorObserver + Send + Sync,
264    {
265        let boxed_factory: Box<
266            dyn Fn() -> Box<dyn crate::zend::ErrorObserver + Send + Sync> + Send + Sync,
267        > = Box::new(move || Box::new(factory()));
268        crate::zend::error_observer::register_error_observer_factory(boxed_factory);
269        self
270    }
271
272    /// Registers an exception observer for monitoring thrown PHP exceptions.
273    ///
274    /// The factory function is called once during MINIT to create
275    /// a singleton observer instance shared across all requests.
276    /// The observer must be `Send + Sync` for ZTS builds.
277    ///
278    /// The observer is called at throw time, before any catch blocks are
279    /// evaluated.
280    ///
281    /// # Arguments
282    ///
283    /// * `factory` - A function that creates an observer instance
284    ///
285    /// # Example
286    ///
287    /// ```ignore
288    /// use ext_php_rs::prelude::*;
289    ///
290    /// struct MyExceptionLogger;
291    ///
292    /// impl ExceptionObserver for MyExceptionLogger {
293    ///     fn on_exception(&self, exception: &ExceptionInfo) {
294    ///         eprintln!("[EXCEPTION] {}: {} at {}:{}",
295    ///             exception.class_name,
296    ///             exception.message.as_deref().unwrap_or("<no message>"),
297    ///             exception.file.as_deref().unwrap_or("<unknown>"),
298    ///             exception.line
299    ///         );
300    ///     }
301    /// }
302    ///
303    /// #[php_module]
304    /// pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
305    ///     module.exception_observer(|| MyExceptionLogger)
306    /// }
307    /// ```
308    ///
309    /// # Panics
310    ///
311    /// Panics if called more than once on the same module.
312    #[cfg(feature = "observer")]
313    pub fn exception_observer<F, O>(self, factory: F) -> Self
314    where
315        F: Fn() -> O + Send + Sync + 'static,
316        O: crate::zend::ExceptionObserver + Send + Sync,
317    {
318        let boxed_factory: Box<
319            dyn Fn() -> Box<dyn crate::zend::ExceptionObserver + Send + Sync> + Send + Sync,
320        > = Box::new(move || Box::new(factory()));
321        crate::zend::exception_observer::register_exception_observer_factory(boxed_factory);
322        self
323    }
324
325    /// Adds a function to the extension.
326    ///
327    /// # Arguments
328    ///
329    /// * `func` - The function to be added to the extension.
330    pub fn function(mut self, func: FunctionBuilder<'static>) -> Self {
331        self.functions.push(func);
332        self
333    }
334
335    /// Adds a constant to the extension.
336    ///
337    /// # Arguments
338    ///
339    /// * `const` - Tuple containing the name, value and doc comments for the
340    ///   constant. This is a tuple to support the [`wrap_constant`] macro.
341    ///
342    /// [`wrap_constant`]: crate::wrap_constant
343    pub fn constant(
344        mut self,
345        r#const: (&str, impl IntoConst + Send + 'static, DocComments),
346    ) -> Self {
347        let (name, val, docs) = r#const;
348        self.constants.push((
349            name.into(),
350            Box::new(val) as Box<dyn IntoConst + Send>,
351            docs,
352        ));
353        self
354    }
355
356    /// Adds a interface to the extension.
357    ///
358    /// # Panics
359    ///
360    /// * Panics if a constant could not be registered.
361    pub fn interface<T: RegisteredClass>(mut self) -> Self {
362        self.interfaces.push(|| {
363            let mut builder = ClassBuilder::new(T::CLASS_NAME);
364            for (method, flags) in T::method_builders() {
365                builder = builder.method(method, flags);
366            }
367            for interface in T::IMPLEMENTS {
368                builder = builder.implements(*interface);
369            }
370            for (name, value, docs) in T::constants() {
371                builder = builder
372                    .dyn_constant(*name, *value, docs)
373                    .expect("Failed to register constant");
374            }
375
376            if let Some(modifier) = T::BUILDER_MODIFIER {
377                builder = modifier(builder);
378            }
379
380            builder = builder.flags(ClassFlags::Interface);
381            // Note: interfaces should NOT have object_override because they cannot be
382            // instantiated
383            builder
384                .registration(|ce| {
385                    T::get_metadata().set_ce(ce);
386                })
387                .docs(T::DOC_COMMENTS)
388        });
389        self
390    }
391
392    /// Adds a class to the extension.
393    ///
394    /// # Panics
395    ///
396    /// * Panics if a constant could not be registered.
397    pub fn class<T: RegisteredClass>(mut self) -> Self {
398        self.classes.push(|| {
399            let mut builder = ClassBuilder::new(T::CLASS_NAME);
400            for (method, flags) in T::method_builders() {
401                builder = builder.method(method, flags);
402            }
403            // Methods from #[php_impl_interface] trait implementations.
404            // Uses the inventory crate for cross-crate method discovery.
405            for (method, flags) in T::interface_method_implementations() {
406                builder = builder.method(method, flags);
407            }
408            if let Some(parent) = T::EXTENDS {
409                builder = builder.extends(parent);
410            }
411            // Interfaces declared via #[php(implements(...))] attribute
412            for interface in T::IMPLEMENTS {
413                builder = builder.implements(*interface);
414            }
415            // Interfaces from #[php_impl_interface] trait implementations.
416            // Uses the inventory crate for cross-crate interface discovery.
417            for interface in T::interface_implementations() {
418                builder = builder.implements(interface);
419            }
420            for (name, value, docs) in T::constants() {
421                builder = builder
422                    .dyn_constant(*name, *value, docs)
423                    .expect("Failed to register constant");
424            }
425            for (name, prop_info) in T::get_properties() {
426                let default_stub = if prop_info.nullable {
427                    Some("null".into())
428                } else {
429                    None
430                };
431                builder = builder.property(crate::builders::ClassProperty {
432                    name: name.into(),
433                    flags: prop_info.flags,
434                    default: None,
435                    docs: prop_info.docs,
436                    ty: Some(prop_info.ty),
437                    nullable: prop_info.nullable,
438                    readonly: prop_info.readonly,
439                    default_stub,
440                });
441            }
442            for (name, flags, default, docs) in T::static_properties() {
443                let default_stub = default.map(crate::convert::IntoZvalDyn::stub_value);
444                let default_fn = default.map(|v| {
445                    Box::new(move || v.as_zval(true))
446                        as Box<dyn FnOnce() -> crate::error::Result<crate::types::Zval>>
447                });
448                builder = builder.property(crate::builders::ClassProperty {
449                    name: (*name).into(),
450                    flags: *flags,
451                    default: default_fn,
452                    docs,
453                    ty: None,
454                    nullable: false,
455                    readonly: false,
456                    default_stub,
457                });
458            }
459            if let Some(modifier) = T::BUILDER_MODIFIER {
460                builder = modifier(builder);
461            }
462
463            builder
464                .flags(T::FLAGS)
465                .object_override::<T>()
466                .registration(|ce| {
467                    T::get_metadata().set_ce(ce);
468                })
469                .docs(T::DOC_COMMENTS)
470        });
471        self
472    }
473
474    /// Adds an enum to the extension.
475    #[cfg(feature = "enum")]
476    pub fn enumeration<T>(mut self) -> Self
477    where
478        T: RegisteredClass + RegisteredEnum,
479    {
480        self.enums.push(|| {
481            let mut builder = EnumBuilder::new(T::CLASS_NAME);
482            for case in T::CASES {
483                builder = builder.case(case);
484            }
485            for (method, flags) in T::method_builders() {
486                builder = builder.method(method, flags);
487            }
488
489            builder
490                .registration(|ce| {
491                    T::get_metadata().set_ce(ce);
492                })
493                .docs(T::DOC_COMMENTS)
494        });
495
496        self
497    }
498}
499
500/// Artifacts from the [`ModuleBuilder`] that should be revisited inside the
501/// extension startup function.
502pub struct ModuleStartup {
503    constants: Vec<(String, Box<dyn IntoConst + Send>)>,
504    classes: Vec<fn() -> ClassBuilder>,
505    interfaces: Vec<fn() -> ClassBuilder>,
506    #[cfg(feature = "enum")]
507    enums: Vec<fn() -> EnumBuilder>,
508}
509
510impl ModuleStartup {
511    /// Completes startup of the module. Should only be called inside the module
512    /// startup function.
513    ///
514    /// # Errors
515    ///
516    /// * Returns an error if a constant could not be registered.
517    ///
518    /// # Panics
519    ///
520    /// * Panics if a class could not be registered.
521    pub fn startup(self, _ty: i32, mod_num: i32) -> Result<()> {
522        for (name, val) in self.constants {
523            val.register_constant(&name, mod_num)?;
524        }
525
526        // Interfaces must be registered before classes so that classes can implement
527        // them
528        self.interfaces.into_iter().map(|c| c()).for_each(|c| {
529            c.register().expect("Failed to build interface");
530        });
531
532        self.classes.into_iter().map(|c| c()).for_each(|c| {
533            c.register().expect("Failed to build class");
534        });
535
536        #[cfg(feature = "enum")]
537        self.enums
538            .into_iter()
539            .map(|builder| builder())
540            .for_each(|e| {
541                e.register().expect("Failed to build enum");
542            });
543
544        // Initialize observer systems if registered
545        #[cfg(feature = "observer")]
546        unsafe {
547            crate::zend::observer::observer_startup();
548            crate::zend::error_observer::error_observer_startup();
549            crate::zend::exception_observer::exception_observer_startup();
550        }
551
552        Ok(())
553    }
554}
555
556/// A function to be called when the extension is starting up or shutting down.
557pub type StartupShutdownFunc = unsafe extern "C" fn(_type: i32, _module_number: i32) -> i32;
558
559/// A function to be called when `phpinfo();` is called.
560pub type InfoFunc = unsafe extern "C" fn(zend_module: *mut ModuleEntry);
561
562/// Builds a [`ModuleEntry`] and [`ModuleStartup`] from a [`ModuleBuilder`].
563/// This is the entry point for the module to be registered with PHP.
564impl TryFrom<ModuleBuilder<'_>> for (ModuleEntry, ModuleStartup) {
565    type Error = crate::error::Error;
566
567    fn try_from(builder: ModuleBuilder) -> Result<Self, Self::Error> {
568        let mut functions = builder
569            .functions
570            .into_iter()
571            .map(FunctionBuilder::build)
572            .collect::<Result<Vec<_>>>()?;
573        functions.push(FunctionEntry::end());
574        let functions = Box::into_raw(functions.into_boxed_slice()) as *const FunctionEntry;
575
576        let name = CString::new(builder.name)?.into_raw();
577        let version = CString::new(builder.version)?.into_raw();
578
579        let startup = ModuleStartup {
580            constants: builder
581                .constants
582                .into_iter()
583                .map(|(n, v, _)| (n, v))
584                .collect(),
585            classes: builder.classes,
586            interfaces: builder.interfaces,
587            #[cfg(feature = "enum")]
588            enums: builder.enums,
589        };
590
591        #[cfg(not(php_zts))]
592        let module_entry = ModuleEntry {
593            size: mem::size_of::<ModuleEntry>().try_into()?,
594            zend_api: ZEND_MODULE_API_NO,
595            zend_debug: u8::from(PHP_DEBUG),
596            zts: u8::from(PHP_ZTS),
597            ini_entry: ptr::null(),
598            deps: ptr::null(),
599            name,
600            functions,
601            module_startup_func: builder.startup_func,
602            module_shutdown_func: builder.shutdown_func,
603            request_startup_func: builder.request_startup_func,
604            request_shutdown_func: builder.request_shutdown_func,
605            info_func: builder.info_func,
606            version,
607            globals_size: 0,
608            globals_ptr: ptr::null_mut(),
609            globals_ctor: None,
610            globals_dtor: None,
611            post_deactivate_func: builder.post_deactivate_func,
612            module_started: 0,
613            type_: 0,
614            handle: ptr::null_mut(),
615            module_number: 0,
616            build_id: unsafe { ext_php_rs_php_build_id() },
617        };
618
619        #[cfg(php_zts)]
620        let module_entry = ModuleEntry {
621            size: mem::size_of::<ModuleEntry>().try_into()?,
622            zend_api: ZEND_MODULE_API_NO,
623            zend_debug: u8::from(PHP_DEBUG),
624            zts: u8::from(PHP_ZTS),
625            ini_entry: ptr::null(),
626            deps: ptr::null(),
627            name,
628            functions,
629            module_startup_func: builder.startup_func,
630            module_shutdown_func: builder.shutdown_func,
631            request_startup_func: builder.request_startup_func,
632            request_shutdown_func: builder.request_shutdown_func,
633            info_func: builder.info_func,
634            version,
635            globals_size: 0,
636            globals_id_ptr: ptr::null_mut(),
637            globals_ctor: None,
638            globals_dtor: None,
639            post_deactivate_func: builder.post_deactivate_func,
640            module_started: 0,
641            type_: 0,
642            handle: ptr::null_mut(),
643            module_number: 0,
644            build_id: unsafe { ext_php_rs_php_build_id() },
645        };
646
647        Ok((module_entry, startup))
648    }
649}
650
651#[cfg(test)]
652mod tests {
653    use crate::test::{
654        test_deactivate_function, test_function, test_info_function, test_startup_shutdown_function,
655    };
656
657    use super::*;
658
659    #[test]
660    fn test_new() {
661        let builder = ModuleBuilder::new("test", "1.0");
662        assert_eq!(builder.name, "test");
663        assert_eq!(builder.version, "1.0");
664        assert!(builder.functions.is_empty());
665        assert!(builder.constants.is_empty());
666        assert!(builder.classes.is_empty());
667        assert!(builder.interfaces.is_empty());
668        assert!(builder.startup_func.is_none());
669        assert!(builder.shutdown_func.is_none());
670        assert!(builder.request_startup_func.is_none());
671        assert!(builder.request_shutdown_func.is_none());
672        assert!(builder.post_deactivate_func.is_none());
673        assert!(builder.info_func.is_none());
674        #[cfg(feature = "enum")]
675        assert!(builder.enums.is_empty());
676    }
677
678    #[test]
679    fn test_name() {
680        let builder = ModuleBuilder::new("test", "1.0").name("new_test");
681        assert_eq!(builder.name, "new_test");
682    }
683
684    #[test]
685    fn test_version() {
686        let builder = ModuleBuilder::new("test", "1.0").version("2.0");
687        assert_eq!(builder.version, "2.0");
688    }
689
690    #[test]
691    fn test_startup_function() {
692        let builder =
693            ModuleBuilder::new("test", "1.0").startup_function(test_startup_shutdown_function);
694        assert!(builder.startup_func.is_some());
695    }
696
697    #[test]
698    fn test_shutdown_function() {
699        let builder =
700            ModuleBuilder::new("test", "1.0").shutdown_function(test_startup_shutdown_function);
701        assert!(builder.shutdown_func.is_some());
702    }
703
704    #[test]
705    fn test_request_startup_function() {
706        let builder = ModuleBuilder::new("test", "1.0")
707            .request_startup_function(test_startup_shutdown_function);
708        assert!(builder.request_startup_func.is_some());
709    }
710
711    #[test]
712    fn test_request_shutdown_function() {
713        let builder = ModuleBuilder::new("test", "1.0")
714            .request_shutdown_function(test_startup_shutdown_function);
715        assert!(builder.request_shutdown_func.is_some());
716    }
717
718    #[test]
719    fn test_set_post_deactivate_function() {
720        let builder =
721            ModuleBuilder::new("test", "1.0").post_deactivate_function(test_deactivate_function);
722        assert!(builder.post_deactivate_func.is_some());
723    }
724
725    #[test]
726    fn test_set_info_function() {
727        let builder = ModuleBuilder::new("test", "1.0").info_function(test_info_function);
728        assert!(builder.info_func.is_some());
729    }
730
731    #[test]
732    fn test_add_function() {
733        let builder =
734            ModuleBuilder::new("test", "1.0").function(FunctionBuilder::new("test", test_function));
735        assert_eq!(builder.functions.len(), 1);
736    }
737
738    #[test]
739    #[cfg(feature = "embed")]
740    fn test_add_constant() {
741        let builder =
742            ModuleBuilder::new("test", "1.0").constant(("TEST_CONST", 42, DocComments::default()));
743        assert_eq!(builder.constants.len(), 1);
744        assert_eq!(builder.constants[0].0, "TEST_CONST");
745        // TODO: Check if the value is 42
746        assert_eq!(builder.constants[0].2, DocComments::default());
747    }
748}