Skip to content
Draft
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
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ name = "observer"
crate-type = ["cdylib"]
required-features = ["observer"]

[[example]]
name = "zend_extension"
crate-type = ["cdylib"]
required-features = ["observer"]

[[test]]
name = "guide_tests"
path = "tests/guide.rs"
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ RUN rustup component add rustfmt
RUN --mount=type=bind,target=/src,rw <<EOF
set -e
cargo clean
cargo build
cargo build --features observer
cp target/debug/build/ext-php-rs-*/out/bindings.rs /docsrs_bindings.rs
rustfmt /docsrs_bindings.rs
EOF
Expand Down
8 changes: 7 additions & 1 deletion allowed_bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -378,5 +378,11 @@ bind! {
zend_execute,
zend_get_executed_scope,
zend_destroy_static_vars,
destroy_op_array
destroy_op_array,
zend_extension,
zend_extension_version_info,
zend_register_extension,
zend_get_resource_handle,
zend_get_op_array_extension_handle,
zend_get_op_array_extension_handles
}
3 changes: 3 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,9 @@ fn main() -> Result<()> {
let mut defines = provider.get_defines()?;
add_php_version_defines(&mut defines, &info)?;

#[cfg(feature = "observer")]
defines.push(("EXT_PHP_RS_OBSERVER", "1"));

check_php_version(&info)?;
build_wrapper(&defines, &includes)?;

Expand Down
91 changes: 91 additions & 0 deletions docsrs_bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3853,3 +3853,94 @@ pub type zend_observer_error_cb = ::std::option::Option<
unsafe extern "C" {
pub fn zend_observer_error_register(callback: zend_observer_error_cb);
}
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct _zend_extension_version_info {
pub zend_extension_api_no: ::std::os::raw::c_int,
pub build_id: *const ::std::os::raw::c_char,
}
pub type zend_extension_version_info = _zend_extension_version_info;
pub type zend_extension = _zend_extension;
pub type startup_func_t = ::std::option::Option<
unsafe extern "C" fn(extension: *mut zend_extension) -> ::std::os::raw::c_int,
>;
pub type shutdown_func_t =
::std::option::Option<unsafe extern "C" fn(extension: *mut zend_extension)>;
pub type activate_func_t = ::std::option::Option<unsafe extern "C" fn()>;
pub type deactivate_func_t = ::std::option::Option<unsafe extern "C" fn()>;
pub type message_handler_func_t = ::std::option::Option<
unsafe extern "C" fn(message: ::std::os::raw::c_int, arg: *mut ::std::os::raw::c_void),
>;
pub type op_array_handler_func_t =
::std::option::Option<unsafe extern "C" fn(op_array: *mut zend_op_array)>;
pub type statement_handler_func_t =
::std::option::Option<unsafe extern "C" fn(frame: *mut zend_execute_data)>;
pub type fcall_begin_handler_func_t =
::std::option::Option<unsafe extern "C" fn(frame: *mut zend_execute_data)>;
pub type fcall_end_handler_func_t =
::std::option::Option<unsafe extern "C" fn(frame: *mut zend_execute_data)>;
pub type op_array_ctor_func_t =
::std::option::Option<unsafe extern "C" fn(op_array: *mut zend_op_array)>;
pub type op_array_dtor_func_t =
::std::option::Option<unsafe extern "C" fn(op_array: *mut zend_op_array)>;
pub type op_array_persist_calc_func_t =
::std::option::Option<unsafe extern "C" fn(op_array: *mut zend_op_array) -> usize>;
pub type op_array_persist_func_t = ::std::option::Option<
unsafe extern "C" fn(op_array: *mut zend_op_array, mem: *mut ::std::os::raw::c_void) -> usize,
>;
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct _zend_extension {
pub name: *const ::std::os::raw::c_char,
pub version: *const ::std::os::raw::c_char,
pub author: *const ::std::os::raw::c_char,
pub URL: *const ::std::os::raw::c_char,
pub copyright: *const ::std::os::raw::c_char,
pub startup: startup_func_t,
pub shutdown: shutdown_func_t,
pub activate: activate_func_t,
pub deactivate: deactivate_func_t,
pub message_handler: message_handler_func_t,
pub op_array_handler: op_array_handler_func_t,
pub statement_handler: statement_handler_func_t,
pub fcall_begin_handler: fcall_begin_handler_func_t,
pub fcall_end_handler: fcall_end_handler_func_t,
pub op_array_ctor: op_array_ctor_func_t,
pub op_array_dtor: op_array_dtor_func_t,
pub api_no_check: ::std::option::Option<
unsafe extern "C" fn(api_no: ::std::os::raw::c_int) -> ::std::os::raw::c_int,
>,
pub build_id_check: ::std::option::Option<
unsafe extern "C" fn(build_id: *const ::std::os::raw::c_char) -> ::std::os::raw::c_int,
>,
pub op_array_persist_calc: op_array_persist_calc_func_t,
pub op_array_persist: op_array_persist_func_t,
pub reserved5: *mut ::std::os::raw::c_void,
pub reserved6: *mut ::std::os::raw::c_void,
pub reserved7: *mut ::std::os::raw::c_void,
pub reserved8: *mut ::std::os::raw::c_void,
pub handle: *mut ::std::os::raw::c_void,
pub resource_number: ::std::os::raw::c_int,
}
unsafe extern "C" {
pub fn zend_get_resource_handle(
module_name: *const ::std::os::raw::c_char,
) -> ::std::os::raw::c_int;
}
unsafe extern "C" {
pub fn zend_get_op_array_extension_handle(
module_name: *const ::std::os::raw::c_char,
) -> ::std::os::raw::c_int;
}
unsafe extern "C" {
pub fn zend_get_op_array_extension_handles(
module_name: *const ::std::os::raw::c_char,
handles: ::std::os::raw::c_int,
) -> ::std::os::raw::c_int;
}
unsafe extern "C" {
pub fn zend_register_extension(
new_extension: *mut zend_extension,
handle: *mut ::std::os::raw::c_void,
);
}
51 changes: 51 additions & 0 deletions examples/zend_extension.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//! Example: Zend Extension hooks for low-level profiling.
//!
//! Build: `cargo build --example zend_extension --features observer`

#![allow(missing_docs, clippy::must_use_candidate)]
#![cfg_attr(windows, feature(abi_vectorcall))]

use ext_php_rs::ffi::zend_op_array;
use ext_php_rs::prelude::*;
use ext_php_rs::zend::ExecuteData;
use std::sync::atomic::{AtomicU64, Ordering};

pub struct StatementProfiler {
compiled_functions: AtomicU64,
executed_statements: AtomicU64,
}

impl StatementProfiler {
fn new() -> Self {
Self {
compiled_functions: AtomicU64::new(0),
executed_statements: AtomicU64::new(0),
}
}
}

impl ZendExtensionHandler for StatementProfiler {
fn op_array_handler(&self, _op_array: &mut zend_op_array) {
self.compiled_functions.fetch_add(1, Ordering::Relaxed);
}

fn statement_handler(&self, _execute_data: &ExecuteData) {
self.executed_statements.fetch_add(1, Ordering::Relaxed);
}

fn activate(&self) {
self.executed_statements.store(0, Ordering::Relaxed);
}
}

#[php_function]
pub fn zend_ext_compiled_count() -> u64 {
0
}

#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module
.zend_extension_handler(StatementProfiler::new)
.function(wrap_function!(zend_ext_compiled_count))
}
82 changes: 81 additions & 1 deletion guide/src/advanced/observer.md
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,84 @@ The backtrace is lazy - only captured when called, so there's zero cost if unuse
| `file` | `Option<String>` | Source file |
| `line` | `u32` | Line number |

## Zend Extension Handler

For low-level engine hooks beyond the Observer API (e.g., per-statement profiling,
bytecode instrumentation, or `op_array` lifecycle tracking), you can register a
`ZendExtensionHandler`. This registers your extension as a `zend_extension` alongside
the regular PHP extension — the same mechanism used by OPcache, Xdebug, and
dd-trace-php.

```rust,ignore
use ext_php_rs::prelude::*;
use ext_php_rs::ffi::zend_op_array;
use ext_php_rs::zend::ExecuteData;
use std::sync::atomic::{AtomicU64, Ordering};

struct StatementProfiler {
statement_count: AtomicU64,
}

impl StatementProfiler {
fn new() -> Self {
Self {
statement_count: AtomicU64::new(0),
}
}
}

impl ZendExtensionHandler for StatementProfiler {
fn op_array_handler(&self, _op_array: &mut zend_op_array) {
// Called after each function/method is compiled.
// Use to instrument bytecode or attach per-function metadata.
}

fn statement_handler(&self, _execute_data: &ExecuteData) {
// Called for each executed statement.
self.statement_count.fetch_add(1, Ordering::Relaxed);
}

fn activate(&self) {
// Per-request activation, distinct from RINIT.
self.statement_count.store(0, Ordering::Relaxed);
}
}

#[php_module]
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
module.zend_extension_handler(StatementProfiler::new)
}
```

### The `ZendExtensionHandler` Trait

All methods have default no-op implementations. Override only what you need.

| Method | Description |
|--------|-------------|
| `op_array_handler(&self, op_array: &mut zend_op_array)` | Called after compilation of each function/method. Use to instrument bytecode. |
| `statement_handler(&self, execute_data: &ExecuteData)` | Called for each executed statement. Use for line-level profiling or code coverage. |
| `fcall_begin_handler(&self, execute_data: &ExecuteData)` | Called at the beginning of each function call (legacy hook). |
| `fcall_end_handler(&self, execute_data: &ExecuteData)` | Called at the end of each function call (legacy hook). |
| `op_array_ctor(&self, op_array: &mut zend_op_array)` | Called when a new `op_array` is constructed. Use to attach per-function data. |
| `op_array_dtor(&self, op_array: &mut zend_op_array)` | Called when an `op_array` is destroyed. Use to clean up per-function data. |
| `message_handler(&self, message: i32, arg: *mut c_void)` | Called when another `zend_extension` sends a message. |
| `activate(&self)` | Per-request activation (distinct from RINIT). |
| `deactivate(&self)` | Per-request deactivation (distinct from RSHUTDOWN). |

### Zend Extension vs Observer API

| Feature | Observer API (`FcallObserver`) | Zend Extension (`ZendExtensionHandler`) |
|---------|------|------|
| Function call hooks | `begin` / `end` with return value | `fcall_begin_handler` / `fcall_end_handler` (legacy) |
| Filtering | `should_observe` (cached per function) | No built-in filtering |
| Statement-level hooks | Not available | `statement_handler` |
| Bytecode access | Not available | `op_array_handler`, `op_array_ctor`, `op_array_dtor` |
| Request lifecycle | Not available | `activate` / `deactivate` |
| Best for | Function-level profiling, tracing | Statement-level profiling, code coverage, bytecode instrumentation |

Both can be registered on the same module simultaneously.

## Using All Observers

You can register all observers on the same module:
Expand All @@ -290,6 +368,7 @@ pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
.fcall_observer(MyProfiler::new)
.error_observer(MyErrorTracker::new)
.exception_observer(MyExceptionTracker::new)
.zend_extension_handler(MyStatementProfiler::new)
}
```

Expand Down Expand Up @@ -324,4 +403,5 @@ Use thread-safe primitives like `AtomicU64`, `Mutex`, or `RwLock` for mutable st
- Only one fcall observer can be registered per extension
- Only one error observer can be registered per extension
- Only one exception observer can be registered per extension
- Observers are registered globally for the entire PHP process
- Only one zend extension handler can be registered per extension
- Observers and handlers are registered globally for the entire PHP process
63 changes: 63 additions & 0 deletions src/builders/module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,55 @@ impl ModuleBuilder<'_> {
self
}

/// Registers a zend extension handler for low-level engine hooks.
///
/// Enables dual registration as both a regular PHP extension and a
/// `zend_extension`, giving access to hooks like `op_array_handler`
/// and `statement_handler` for building profilers and APMs.
///
/// The factory function is called once during MINIT to create
/// a singleton handler instance. The handler must be `Send + Sync`
/// for ZTS builds.
///
/// # Arguments
///
/// * `factory` - A function that creates a handler instance
///
/// # Example
///
/// ```ignore
/// use ext_php_rs::prelude::*;
/// use ext_php_rs::ffi::zend_op_array;
///
/// struct MyProfiler;
///
/// impl ZendExtensionHandler for MyProfiler {
/// fn op_array_handler(&self, _op_array: &mut zend_op_array) {}
/// fn statement_handler(&self, _execute_data: &ExecuteData) {}
/// }
///
/// #[php_module]
/// pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
/// module.zend_extension_handler(|| MyProfiler)
/// }
/// ```
///
/// # Panics
///
/// Panics if called more than once on the same module.
#[cfg(feature = "observer")]
pub fn zend_extension_handler<F, H>(self, factory: F) -> Self
where
F: Fn() -> H + Send + Sync + 'static,
H: crate::zend::ZendExtensionHandler,
{
let boxed_factory: Box<
dyn Fn() -> Box<dyn crate::zend::ZendExtensionHandler> + Send + Sync,
> = Box::new(move || Box::new(factory()));
crate::zend::zend_extension::register_zend_extension_factory(boxed_factory);
self
}

/// Adds a function to the extension.
///
/// # Arguments
Expand Down Expand Up @@ -471,6 +520,10 @@ impl ModuleBuilder<'_> {
/// Artifacts from the [`ModuleBuilder`] that should be revisited inside the
/// extension startup function.
pub struct ModuleStartup {
#[cfg(feature = "observer")]
name: String,
#[cfg(feature = "observer")]
version: String,
constants: Vec<(String, Box<dyn IntoConst + Send>)>,
classes: Vec<fn() -> ClassBuilder>,
interfaces: Vec<fn() -> ClassBuilder>,
Expand Down Expand Up @@ -518,6 +571,7 @@ impl ModuleStartup {
crate::zend::observer::observer_startup();
crate::zend::error_observer::error_observer_startup();
crate::zend::exception_observer::exception_observer_startup();
crate::zend::zend_extension::zend_extension_startup(&self.name, &self.version);
}

Ok(())
Expand All @@ -544,10 +598,19 @@ impl TryFrom<ModuleBuilder<'_>> for (ModuleEntry, ModuleStartup) {
functions.push(FunctionEntry::end());
let functions = Box::into_raw(functions.into_boxed_slice()) as *const FunctionEntry;

#[cfg(feature = "observer")]
let ext_name = builder.name.clone();
#[cfg(feature = "observer")]
let ext_version = builder.version.clone();

let name = CString::new(builder.name)?.into_raw();
let version = CString::new(builder.version)?.into_raw();

let startup = ModuleStartup {
#[cfg(feature = "observer")]
name: ext_name,
#[cfg(feature = "observer")]
version: ext_version,
constants: builder
.constants
.into_iter()
Expand Down
Loading
Loading