Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions guide/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
126 changes: 126 additions & 0 deletions guide/src/advanced/module_globals.md
Original file line number Diff line number Diff line change
@@ -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<T>` 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<MyGlobals> = 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<MyGlobals> = 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<MyGlobals> = 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<u64> }
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.
104 changes: 91 additions & 13 deletions src/builders/module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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,
Expand All @@ -61,6 +61,41 @@ pub struct ModuleBuilder<'a> {
request_shutdown_func: Option<StartupShutdownFunc>,
post_deactivate_func: Option<unsafe extern "C" fn() -> i32>,
info_func: Option<InfoFunc>,
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<unsafe extern "C" fn(*mut std::ffi::c_void)>,
globals_dtor: Option<unsafe extern "C" fn(*mut std::ffi::c_void)>,
}

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<'_> {
Expand All @@ -74,9 +109,6 @@ impl ModuleBuilder<'_> {
Self {
name: name.into(),
version: version.into(),
functions: vec![],
constants: vec![],
classes: vec![],
..Default::default()
}
}
Expand Down Expand Up @@ -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<MyGlobals> = ModuleGlobals::new();
///
/// #[php_module]
/// pub fn module(module: ModuleBuilder) -> ModuleBuilder {
/// module.globals(&MY_GLOBALS)
/// }
/// ```
pub fn globals<T: ModuleGlobal>(mut self, handle: &'static ModuleGlobals<T>) -> Self {
use crate::zend::module_globals::{ginit_callback, gshutdown_callback};

self.globals_size = std::mem::size_of::<T>();
#[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::<T>);
self.globals_dtor = Some(gshutdown_callback::<T>);
self
}

/// Registers a function call observer for profiling or tracing.
///
/// The factory function is called once globally during MINIT to create
Expand Down Expand Up @@ -604,10 +682,10 @@ impl TryFrom<ModuleBuilder<'_>> 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,
Expand All @@ -632,10 +710,10 @@ impl TryFrom<ModuleBuilder<'_>> 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,
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
6 changes: 6 additions & 0 deletions src/wrapper.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
3 changes: 3 additions & 0 deletions src/wrapper.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions src/zend/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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::*;
Expand Down
Loading
Loading