diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md index 93c9901b1..af5fc323b 100644 --- a/guide/src/SUMMARY.md +++ b/guide/src/SUMMARY.md @@ -48,6 +48,7 @@ - [Observer API](./advanced/observer.md) - [Embedded PHP](./advanced/embedded_php.md) - [Custom SAPI](./advanced/custom_sapi.md) +- [Module Globals](./advanced/module_globals.md) - [Worker Mode](./advanced/worker_mode.md) - [Allowed Bindings](./advanced/allowed_bindings.md) diff --git a/guide/src/advanced/module_globals.md b/guide/src/advanced/module_globals.md new file mode 100644 index 000000000..2f9b982b0 --- /dev/null +++ b/guide/src/advanced/module_globals.md @@ -0,0 +1,126 @@ +# Module Globals + +PHP extensions can declare per-module global state that is automatically managed +by the engine. In ZTS (thread-safe) builds, PHP's TSRM allocates a separate copy +of the globals for each thread. In non-ZTS builds, the globals are a plain +static variable. + +ext-php-rs exposes this via `ModuleGlobals` and the `ModuleGlobal` trait. + +## Defining Globals + +Create a struct that implements `Default` and `ModuleGlobal`: + +```rust,ignore +use ext_php_rs::zend::{ModuleGlobal, ModuleGlobals}; + +#[derive(Default)] +struct MyGlobals { + request_count: i64, + max_depth: i32, +} + +impl ModuleGlobal for MyGlobals { + fn ginit(&mut self) { + self.max_depth = 512; + } +} +``` + +`Default::default()` initializes the struct, then `ginit()` runs for any +additional setup. In ZTS mode these callbacks fire once per thread; in non-ZTS +mode they fire once at module load. + +If you don't need custom initialization, leave the trait impl empty: + +```rust,ignore +# use ext_php_rs::zend::{ModuleGlobal, ModuleGlobals}; +#[derive(Default)] +struct SimpleGlobals { + counter: i64, +} + +impl ModuleGlobal for SimpleGlobals {} +``` + +## Registering Globals + +Declare a `static` and pass it to `ModuleBuilder::globals()`: + +```rust,ignore +use ext_php_rs::prelude::*; +use ext_php_rs::zend::{ModuleGlobal, ModuleGlobals}; + +# #[derive(Default)] +# struct MyGlobals { request_count: i64, max_depth: i32 } +# impl ModuleGlobal for MyGlobals {} +# +static MY_GLOBALS: ModuleGlobals = ModuleGlobals::new(); + +#[php_module] +pub fn module(module: ModuleBuilder) -> ModuleBuilder { + module.globals(&MY_GLOBALS) +} +``` + +Only one globals struct per module is supported (a PHP limitation). + +## Accessing Globals + +Use `get()` for shared access and `get_mut()` for mutable access: + +```rust,ignore +use ext_php_rs::prelude::*; +use ext_php_rs::zend::{ModuleGlobal, ModuleGlobals}; + +# #[derive(Default)] +# struct MyGlobals { request_count: i64, max_depth: i32 } +# impl ModuleGlobal for MyGlobals {} +# static MY_GLOBALS: ModuleGlobals = ModuleGlobals::new(); +# +#[php_function] +pub fn get_request_count() -> i64 { + MY_GLOBALS.get().request_count +} + +#[php_function] +pub fn increment_request_count() { + unsafe { MY_GLOBALS.get_mut() }.request_count += 1; +} +``` + +`get()` is safe because PHP runs one request per thread at a time. `get_mut()` +is `unsafe` because the caller must ensure exclusive access (which is guaranteed +within a single `#[php_function]` handler, but not from background Rust threads). + +## Advanced: Raw Pointer Access + +For power users who need direct pointer access (e.g., passing to C APIs or +building custom lock-free patterns), `as_ptr()` returns `*mut T`: + +```rust,ignore +# use ext_php_rs::zend::{ModuleGlobal, ModuleGlobals}; +# #[derive(Default)] +# struct MyGlobals { request_count: i64 } +# impl ModuleGlobal for MyGlobals {} +# static MY_GLOBALS: ModuleGlobals = ModuleGlobals::new(); +let ptr: *mut MyGlobals = MY_GLOBALS.as_ptr(); +``` + +## Cleanup + +Implement `gshutdown()` if your globals hold external resources: + +```rust,ignore +# use ext_php_rs::zend::ModuleGlobal; +# #[derive(Default)] +# struct MyGlobals { handle: Option } +impl ModuleGlobal for MyGlobals { + fn gshutdown(&mut self) { + self.handle.take(); + } +} +``` + +The struct is also dropped after `gshutdown()` returns, so standard `Drop` +implementations work as expected. diff --git a/src/builders/module.rs b/src/builders/module.rs index 82f7e38f7..d65480bee 100644 --- a/src/builders/module.rs +++ b/src/builders/module.rs @@ -9,7 +9,7 @@ use crate::{ error::Result, ffi::{ZEND_MODULE_API_NO, ext_php_rs_php_build_id}, flags::ClassFlags, - zend::{FunctionEntry, ModuleEntry}, + zend::{FunctionEntry, ModuleEntry, ModuleGlobal, ModuleGlobals}, }; #[cfg(feature = "enum")] use crate::{builders::enum_builder::EnumBuilder, enum_::RegisteredEnum}; @@ -45,7 +45,7 @@ use crate::{builders::enum_builder::EnumBuilder, enum_::RegisteredEnum}; /// } /// ``` #[must_use] -#[derive(Debug, Default)] +#[derive(Debug)] pub struct ModuleBuilder<'a> { pub(crate) name: String, pub(crate) version: String, @@ -61,6 +61,41 @@ pub struct ModuleBuilder<'a> { request_shutdown_func: Option, post_deactivate_func: Option i32>, info_func: Option, + globals_size: usize, + #[cfg(php_zts)] + globals_id_ptr: *mut i32, + #[cfg(not(php_zts))] + globals_ptr: *mut std::ffi::c_void, + globals_ctor: Option, + globals_dtor: Option, +} + +impl Default for ModuleBuilder<'_> { + fn default() -> Self { + Self { + name: String::new(), + version: String::new(), + functions: vec![], + constants: vec![], + classes: vec![], + interfaces: vec![], + #[cfg(feature = "enum")] + enums: vec![], + startup_func: None, + shutdown_func: None, + request_startup_func: None, + request_shutdown_func: None, + post_deactivate_func: None, + info_func: None, + globals_size: 0, + #[cfg(php_zts)] + globals_id_ptr: ptr::null_mut(), + #[cfg(not(php_zts))] + globals_ptr: ptr::null_mut(), + globals_ctor: None, + globals_dtor: None, + } + } } impl ModuleBuilder<'_> { @@ -74,9 +109,6 @@ impl ModuleBuilder<'_> { Self { name: name.into(), version: version.into(), - functions: vec![], - constants: vec![], - classes: vec![], ..Default::default() } } @@ -166,6 +198,52 @@ impl ModuleBuilder<'_> { self } + /// Registers a module globals struct with this extension. + /// + /// PHP will allocate per-thread storage (ZTS) or use the static's inline + /// storage (non-ZTS), calling GINIT/GSHUTDOWN callbacks automatically. + /// + /// Only one globals struct per module is supported (PHP limitation). + /// Calling this a second time will overwrite the previous registration. + /// + /// # Arguments + /// + /// * `handle` - A static [`ModuleGlobals`] that will hold the globals. + /// + /// # Examples + /// + /// ```ignore + /// use ext_php_rs::prelude::*; + /// use ext_php_rs::zend::{ModuleGlobal, ModuleGlobals}; + /// + /// #[derive(Default)] + /// struct MyGlobals { counter: i64 } + /// impl ModuleGlobal for MyGlobals {} + /// + /// static MY_GLOBALS: ModuleGlobals = ModuleGlobals::new(); + /// + /// #[php_module] + /// pub fn module(module: ModuleBuilder) -> ModuleBuilder { + /// module.globals(&MY_GLOBALS) + /// } + /// ``` + pub fn globals(mut self, handle: &'static ModuleGlobals) -> Self { + use crate::zend::module_globals::{ginit_callback, gshutdown_callback}; + + self.globals_size = std::mem::size_of::(); + #[cfg(php_zts)] + { + self.globals_id_ptr = handle.id_ptr(); + } + #[cfg(not(php_zts))] + { + self.globals_ptr = handle.data_ptr(); + } + self.globals_ctor = Some(ginit_callback::); + self.globals_dtor = Some(gshutdown_callback::); + self + } + /// Registers a function call observer for profiling or tracing. /// /// The factory function is called once globally during MINIT to create @@ -604,10 +682,10 @@ impl TryFrom> for (ModuleEntry, ModuleStartup) { request_shutdown_func: builder.request_shutdown_func, info_func: builder.info_func, version, - globals_size: 0, - globals_ptr: ptr::null_mut(), - globals_ctor: None, - globals_dtor: None, + globals_size: builder.globals_size, + globals_ptr: builder.globals_ptr, + globals_ctor: builder.globals_ctor, + globals_dtor: builder.globals_dtor, post_deactivate_func: builder.post_deactivate_func, module_started: 0, type_: 0, @@ -632,10 +710,10 @@ impl TryFrom> for (ModuleEntry, ModuleStartup) { request_shutdown_func: builder.request_shutdown_func, info_func: builder.info_func, version, - globals_size: 0, - globals_id_ptr: ptr::null_mut(), - globals_ctor: None, - globals_dtor: None, + globals_size: builder.globals_size, + globals_id_ptr: builder.globals_id_ptr, + globals_ctor: builder.globals_ctor, + globals_dtor: builder.globals_dtor, post_deactivate_func: builder.post_deactivate_func, module_started: 0, type_: 0, diff --git a/src/lib.rs b/src/lib.rs index 5e2a83eb0..3ce3a0bbc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -69,12 +69,12 @@ pub mod prelude { pub use crate::php_println; pub use crate::php_write; pub use crate::types::ZendCallable; - pub use crate::zend::BailoutGuard; #[cfg(feature = "observer")] pub use crate::zend::{ BacktraceFrame, ErrorInfo, ErrorObserver, ErrorType, ExceptionInfo, ExceptionObserver, FcallInfo, FcallObserver, }; + pub use crate::zend::{BailoutGuard, ModuleGlobal, ModuleGlobals}; pub use crate::{ ZvalConvert, php_class, php_const, php_extern, php_function, php_impl, php_impl_interface, php_interface, php_module, wrap_constant, wrap_function, zend_fastcall, diff --git a/src/wrapper.c b/src/wrapper.c index 940478c31..0135d9c85 100644 --- a/src/wrapper.c +++ b/src/wrapper.c @@ -91,6 +91,12 @@ php_file_globals *ext_php_rs_file_globals() { #endif } +#ifdef ZTS +void *ext_php_rs_tsrmg_bulk(int id) { + return TSRMG_BULK(id, void *); +} +#endif + sapi_module_struct *ext_php_rs_sapi_module() { return &sapi_module; } diff --git a/src/wrapper.h b/src/wrapper.h index f76b9687f..b16c8f7d9 100644 --- a/src/wrapper.h +++ b/src/wrapper.h @@ -53,6 +53,9 @@ zend_executor_globals *ext_php_rs_executor_globals(); php_core_globals *ext_php_rs_process_globals(); sapi_globals_struct *ext_php_rs_sapi_globals(); php_file_globals *ext_php_rs_file_globals(); +#ifdef ZTS +void *ext_php_rs_tsrmg_bulk(int id); +#endif sapi_module_struct *ext_php_rs_sapi_module(); bool ext_php_rs_zend_try_catch(void* (*callback)(void *), void *ctx, void **result); bool ext_php_rs_zend_first_try_catch(void* (*callback)(void *), void *ctx, void **result); diff --git a/src/zend/mod.rs b/src/zend/mod.rs index a30fcf717..a789cf95f 100644 --- a/src/zend/mod.rs +++ b/src/zend/mod.rs @@ -15,6 +15,7 @@ mod handlers; mod ini_entry_def; mod linked_list; mod module; +pub(crate) mod module_globals; #[cfg(feature = "observer")] pub(crate) mod observer; mod streams; @@ -49,6 +50,7 @@ pub use handlers::ZendObjectHandlers; pub use ini_entry_def::IniEntryDef; pub use linked_list::ZendLinkedList; pub use module::{ModuleEntry, StaticModuleEntry, cleanup_module_allocations}; +pub use module_globals::{ModuleGlobal, ModuleGlobals}; #[cfg(feature = "observer")] pub use observer::{FcallInfo, FcallObserver}; pub use streams::*; diff --git a/src/zend/module_globals.rs b/src/zend/module_globals.rs new file mode 100644 index 000000000..201440138 --- /dev/null +++ b/src/zend/module_globals.rs @@ -0,0 +1,314 @@ +use std::cell::UnsafeCell; +use std::marker::PhantomData; +#[cfg_attr(php_zts, allow(unused_imports))] +use std::mem::MaybeUninit; + +/// Trait for types used as PHP module globals. +/// +/// Requires [`Default`] for initialization. Override [`ginit`](ModuleGlobal::ginit) +/// and [`gshutdown`](ModuleGlobal::gshutdown) for custom per-thread (ZTS) or +/// per-module (non-ZTS) lifecycle logic. +/// +/// # Examples +/// +/// ``` +/// use ext_php_rs::zend::ModuleGlobal; +/// +/// #[derive(Default)] +/// struct MyGlobals { +/// request_count: i64, +/// max_depth: i32, +/// } +/// +/// impl ModuleGlobal for MyGlobals { +/// fn ginit(&mut self) { +/// self.max_depth = 512; +/// } +/// } +/// ``` +pub trait ModuleGlobal: Default + 'static { + /// Called after the struct is initialized with [`Default::default()`]. + /// + /// Use for setup that goes beyond what `Default` can express. + /// In ZTS mode, called once per thread. In non-ZTS mode, called once at module init. + fn ginit(&mut self) {} + + /// Called before the struct is dropped. + /// + /// Use for cleanup of external resources. + /// In ZTS mode, called once per thread. In non-ZTS mode, called once at module shutdown. + fn gshutdown(&mut self) {} +} + +unsafe extern "C" { + #[cfg(php_zts)] + fn ext_php_rs_tsrmg_bulk(id: i32) -> *mut std::ffi::c_void; +} + +/// Thread-safe handle to PHP module globals. +/// +/// Declare as a `static` and pass to [`ModuleBuilder::globals()`](crate::builders::ModuleBuilder::globals). +/// +/// In ZTS (thread-safe) builds, PHP's TSRM allocates per-thread storage and +/// manages the lifetime via GINIT/GSHUTDOWN callbacks. In non-ZTS builds, the +/// globals live inline in this struct as a plain static. +/// +/// # Examples +/// +/// ``` +/// use ext_php_rs::zend::{ModuleGlobal, ModuleGlobals}; +/// +/// #[derive(Default)] +/// struct MyGlobals { +/// counter: i64, +/// } +/// +/// impl ModuleGlobal for MyGlobals {} +/// +/// static MY_GLOBALS: ModuleGlobals = ModuleGlobals::new(); +/// ``` +pub struct ModuleGlobals { + #[cfg(php_zts)] + id: UnsafeCell, + #[cfg(not(php_zts))] + inner: UnsafeCell>, + _marker: PhantomData, +} + +// SAFETY: In ZTS mode, TSRM guarantees per-thread access. The `id` field is +// only written once during single-threaded module init (MINIT). +// In non-ZTS mode, PHP is single-threaded. +unsafe impl Sync for ModuleGlobals {} + +impl Default for ModuleGlobals { + fn default() -> Self { + Self::new() + } +} + +impl ModuleGlobals { + /// Creates an uninitialized globals handle. + /// + /// Must be passed to [`ModuleBuilder::globals()`](crate::builders::ModuleBuilder::globals) + /// for PHP to allocate and initialize the storage. + #[must_use] + pub const fn new() -> Self { + Self { + #[cfg(php_zts)] + id: UnsafeCell::new(0), + #[cfg(not(php_zts))] + inner: UnsafeCell::new(MaybeUninit::uninit()), + _marker: PhantomData, + } + } + + /// Returns a shared reference to the current thread's globals. + /// + /// Safe because PHP guarantees single-threaded request processing: + /// only one request handler runs per thread at a time, and module + /// globals are initialized before any request begins. + /// + /// # Panics + /// + /// Debug-asserts that the globals have been registered. In release builds, + /// calling this before module init is undefined behavior. + pub fn get(&self) -> &T { + unsafe { + #[cfg(php_zts)] + { + let id = *self.id.get(); + debug_assert!(id != 0, "ModuleGlobals accessed before registration"); + &*ext_php_rs_tsrmg_bulk(id).cast::() + } + #[cfg(not(php_zts))] + { + (*self.inner.get()).assume_init_ref() + } + } + } + + /// Returns a mutable reference to the current thread's globals. + /// + /// # Safety + /// + /// Caller must ensure exclusive access. Typically safe within `RINIT`/`RSHUTDOWN` + /// or from a `#[php_function]` handler (PHP runs one request per thread), but + /// NOT from background Rust threads. + #[allow(clippy::mut_from_ref)] + pub unsafe fn get_mut(&self) -> &mut T { + #[cfg(php_zts)] + unsafe { + let id = *self.id.get(); + debug_assert!(id != 0, "ModuleGlobals accessed before registration"); + &mut *ext_php_rs_tsrmg_bulk(id).cast::() + } + #[cfg(not(php_zts))] + unsafe { + (*self.inner.get()).assume_init_mut() + } + } + + /// Returns a raw pointer to the globals for the current thread. + /// + /// Escape hatch for power users who need direct access without + /// lifetime constraints. + pub fn as_ptr(&self) -> *mut T { + unsafe { + #[cfg(php_zts)] + { + ext_php_rs_tsrmg_bulk(*self.id.get()).cast::() + } + #[cfg(not(php_zts))] + { + (*self.inner.get()).as_mut_ptr() + } + } + } + + /// Returns a pointer to the internal ID storage (ZTS) or data storage (non-ZTS). + /// + /// Used by [`ModuleBuilder::globals()`](crate::builders::ModuleBuilder::globals) + /// to wire up the `zend_module_entry`. + #[cfg(php_zts)] + pub(crate) fn id_ptr(&self) -> *mut i32 { + self.id.get() + } + + /// Returns a pointer to the internal storage. + /// + /// Used by [`ModuleBuilder::globals()`](crate::builders::ModuleBuilder::globals) + /// to wire up the `zend_module_entry`. + #[cfg(not(php_zts))] + pub(crate) fn data_ptr(&self) -> *mut std::ffi::c_void { + self.inner.get().cast() + } +} + +/// GINIT callback invoked by PHP per-thread (ZTS) or once (non-ZTS). +/// +/// # Safety +/// +/// `globals` must point to uninitialized memory of at least `size_of::()` bytes. +/// Called by PHP's module initialization machinery. +pub(crate) unsafe extern "C" fn ginit_callback(globals: *mut std::ffi::c_void) { + unsafe { + let ptr = globals.cast::(); + ptr.write(T::default()); + (*ptr).ginit(); + } +} + +/// GSHUTDOWN callback invoked by PHP before freeing globals memory. +/// +/// # Safety +/// +/// `globals` must point to a valid, initialized `T`. +/// Called by PHP's module shutdown machinery. +pub(crate) unsafe extern "C" fn gshutdown_callback( + globals: *mut std::ffi::c_void, +) { + unsafe { + let ptr = globals.cast::(); + (*ptr).gshutdown(); + std::ptr::drop_in_place(ptr); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[derive(Default)] + struct TestGlobals { + value: i32, + initialized: bool, + } + + impl ModuleGlobal for TestGlobals { + fn ginit(&mut self) { + self.initialized = true; + self.value = 42; + } + + fn gshutdown(&mut self) { + self.initialized = false; + } + } + + #[test] + fn new_is_const() { + static _G: ModuleGlobals = ModuleGlobals::new(); + } + + #[test] + fn ginit_callback_initializes() { + let mut storage = MaybeUninit::::uninit(); + unsafe { + ginit_callback::(storage.as_mut_ptr().cast()); + let globals = storage.assume_init_ref(); + assert!(globals.initialized); + assert_eq!(globals.value, 42); + std::ptr::drop_in_place(storage.as_mut_ptr()); + } + } + + #[test] + fn gshutdown_callback_cleans_up() { + let mut storage = MaybeUninit::::uninit(); + unsafe { + ginit_callback::(storage.as_mut_ptr().cast()); + gshutdown_callback::(storage.as_mut_ptr().cast()); + } + } + + #[test] + #[cfg(not(php_zts))] + fn non_zts_get_after_init() { + let globals: ModuleGlobals = ModuleGlobals::new(); + unsafe { + ginit_callback::(globals.data_ptr()); + } + assert!(globals.get().initialized); + assert_eq!(globals.get().value, 42); + unsafe { + gshutdown_callback::(globals.data_ptr()); + } + } + + #[test] + #[cfg(not(php_zts))] + fn non_zts_get_mut() { + let globals: ModuleGlobals = ModuleGlobals::new(); + unsafe { + ginit_callback::(globals.data_ptr()); + globals.get_mut().value = 99; + } + assert_eq!(globals.get().value, 99); + unsafe { + gshutdown_callback::(globals.data_ptr()); + } + } + + #[test] + #[cfg(not(php_zts))] + fn non_zts_as_ptr() { + let globals: ModuleGlobals = ModuleGlobals::new(); + unsafe { + ginit_callback::(globals.data_ptr()); + } + let ptr = globals.as_ptr(); + assert_eq!(unsafe { (*ptr).value }, 42); + unsafe { + gshutdown_callback::(globals.data_ptr()); + } + } + + #[derive(Default)] + struct ZstGlobals; + impl ModuleGlobal for ZstGlobals {} + + #[test] + fn zst_size_is_zero() { + assert_eq!(std::mem::size_of::(), 0); + } +} diff --git a/tests/src/integration/mod.rs b/tests/src/integration/mod.rs index 18d6dfcec..cf96e02fc 100644 --- a/tests/src/integration/mod.rs +++ b/tests/src/integration/mod.rs @@ -13,6 +13,7 @@ pub mod globals; pub mod interface; pub mod iterator; pub mod magic_method; +pub mod module_globals; pub mod nullable; pub mod number; pub mod object; diff --git a/tests/src/integration/module_globals/mod.rs b/tests/src/integration/module_globals/mod.rs new file mode 100644 index 000000000..b981019e3 --- /dev/null +++ b/tests/src/integration/module_globals/mod.rs @@ -0,0 +1,63 @@ +use ext_php_rs::prelude::*; +use ext_php_rs::zend::{ModuleGlobal, ModuleGlobals}; + +#[derive(Default)] +struct TestModuleGlobals { + counter: i64, + max_depth: i32, + ginit_called: bool, +} + +impl ModuleGlobal for TestModuleGlobals { + fn ginit(&mut self) { + self.ginit_called = true; + self.max_depth = 512; + } +} + +static TEST_GLOBALS: ModuleGlobals = ModuleGlobals::new(); + +#[php_function] +pub fn test_module_globals_get_counter() -> i64 { + TEST_GLOBALS.get().counter +} + +#[php_function] +pub fn test_module_globals_increment_counter() { + unsafe { TEST_GLOBALS.get_mut() }.counter += 1; +} + +#[php_function] +pub fn test_module_globals_get_max_depth() -> i32 { + TEST_GLOBALS.get().max_depth +} + +#[php_function] +pub fn test_module_globals_ginit_called() -> bool { + TEST_GLOBALS.get().ginit_called +} + +#[php_function] +pub fn test_module_globals_reset_counter() { + unsafe { TEST_GLOBALS.get_mut() }.counter = 0; +} + +pub fn build_module(builder: ModuleBuilder) -> ModuleBuilder { + builder + .globals(&TEST_GLOBALS) + .function(wrap_function!(test_module_globals_get_counter)) + .function(wrap_function!(test_module_globals_increment_counter)) + .function(wrap_function!(test_module_globals_get_max_depth)) + .function(wrap_function!(test_module_globals_ginit_called)) + .function(wrap_function!(test_module_globals_reset_counter)) +} + +#[cfg(test)] +mod tests { + #[test] + fn module_globals_works() { + assert!(crate::integration::test::run_php( + "module_globals/module_globals.php" + )); + } +} diff --git a/tests/src/integration/module_globals/module_globals.php b/tests/src/integration/module_globals/module_globals.php new file mode 100644 index 000000000..41ae56264 --- /dev/null +++ b/tests/src/integration/module_globals/module_globals.php @@ -0,0 +1,22 @@ + ModuleBuilder { } module = integration::exception::build_module(module); module = integration::globals::build_module(module); + module = integration::module_globals::build_module(module); module = integration::iterator::build_module(module); module = integration::magic_method::build_module(module); module = integration::nullable::build_module(module);