ext-php-rs 0.15.11

Bindings for the Zend API to build PHP extensions natively in Rust.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
//! Zend Extension API bindings for low-level engine hooks.
//!
//! Enables building profilers, APMs, and code coverage tools by registering
//! as a `zend_extension` alongside the regular PHP extension.
//!
//! # Example
//!
//! ```ignore
//! use ext_php_rs::prelude::*;
//! use ext_php_rs::ffi::zend_op_array;
//!
//! struct MyProfiler;
//!
//! impl ZendExtensionHandler for MyProfiler {
//!     fn on_op_array_compiled(&self, _op_array: &mut zend_op_array) {}
//!     fn on_statement(&self, _execute_data: &ExecuteData) {}
//! }
//! ```

use std::ffi::{CString, c_void};
use std::sync::OnceLock;

use crate::ffi;
use crate::zend::ExecuteData;

/// Trait for handling low-level Zend Engine hooks via `zend_extension`.
///
/// All methods have default no-op implementations. Override only what you need.
///
/// # Thread Safety
///
/// Handler must be `Send + Sync`. Use thread-safe primitives for mutable state.
pub trait ZendExtensionHandler: Send + Sync + 'static {
    /// Called after compilation of each function/method `op_array`.
    /// Enabled by [`ZendExtensionBuilder::hook_op_array_compile`].
    fn on_op_array_compiled(&self, _op_array: &mut ffi::zend_op_array) {}

    /// Called for each executed statement.
    /// Enabled by [`ZendExtensionBuilder::hook_statements`].
    fn on_statement(&self, _execute_data: &ExecuteData) {}

    /// Called at the beginning of each function call (legacy hook).
    /// Enabled by [`ZendExtensionBuilder::hook_fcalls`].
    fn on_fcall_begin(&self, _execute_data: &ExecuteData) {}

    /// Called at the end of each function call (legacy hook).
    /// Enabled by [`ZendExtensionBuilder::hook_fcalls`].
    fn on_fcall_end(&self, _execute_data: &ExecuteData) {}

    /// Called when a new `op_array` is constructed.
    fn on_op_array_ctor(&self, _op_array: &mut ffi::zend_op_array) {}

    /// Called when an `op_array` is destroyed.
    fn on_op_array_dtor(&self, _op_array: &mut ffi::zend_op_array) {}

    /// Called when another `zend_extension` sends a message.
    fn on_message(&self, _message: i32, _arg: *mut c_void) {}

    /// Per-request activation (distinct from RINIT).
    fn on_activate(&self) {}

    /// Per-request deactivation (distinct from RSHUTDOWN).
    fn on_deactivate(&self) {}
}

// ============================================================================
// Config + statics
// ============================================================================

pub(crate) struct ZendExtensionConfig {
    pub(crate) factory: Box<dyn Fn() -> Box<dyn ZendExtensionHandler> + Send + Sync>,
    pub(crate) hook_op_array: bool,
    pub(crate) hook_statements: bool,
    pub(crate) hook_fcalls: bool,
}

static ZEND_EXT_CONFIG: OnceLock<ZendExtensionConfig> = OnceLock::new();
static ZEND_EXT_INSTANCE: OnceLock<Box<dyn ZendExtensionHandler>> = OnceLock::new();
static EXT_NAME: OnceLock<CString> = OnceLock::new();
static EXT_VERSION: OnceLock<CString> = OnceLock::new();

fn get_handler() -> Option<&'static dyn ZendExtensionHandler> {
    ZEND_EXT_INSTANCE.get().map(std::convert::AsRef::as_ref)
}

// PHP compiler flags (from zend_compile.h) that cause the compiler to emit
// the special opcodes consumed by zend_extension hooks.
const ZEND_COMPILE_EXTENDED_STMT: u32 = 1 << 0;
const ZEND_COMPILE_EXTENDED_FCALL: u32 = 1 << 1;
const ZEND_COMPILE_HANDLE_OP_ARRAY: u32 = 1 << 2;

fn compile_flags_for(cfg: &ZendExtensionConfig) -> u32 {
    let mut flags = 0u32;
    if cfg.hook_op_array {
        flags |= ZEND_COMPILE_HANDLE_OP_ARRAY;
    }
    if cfg.hook_statements {
        flags |= ZEND_COMPILE_EXTENDED_STMT;
    }
    if cfg.hook_fcalls {
        flags |= ZEND_COMPILE_EXTENDED_FCALL;
    }
    flags
}

// ============================================================================
// Builder
// ============================================================================

/// Builder for a `zend_extension` registration.
///
/// Returned by [`crate::builders::ModuleBuilder::zend_extension`]. Call
/// [`Self::finish`] to return to the outer
/// [`ModuleBuilder`](crate::builders::ModuleBuilder) after selecting opt-in
/// hooks.
///
/// Each opt-in method enables one family of [`ZendExtensionHandler`] hooks.
/// [`Self::hook_statements`] and [`Self::hook_fcalls`] also flip the matching
/// `ZEND_COMPILE_*` flag so PHP emits the extra opcodes the hook depends on,
/// which costs every compiled script. [`Self::hook_op_array_compile`] has no
/// compile-time cost because its flag is already set by PHP's defaults; it
/// only controls whether the dispatcher callback runs.
#[must_use = "call .finish() to return to ModuleBuilder"]
pub struct ZendExtensionBuilder<'a> {
    module: Option<crate::builders::ModuleBuilder<'a>>,
    factory: Box<dyn Fn() -> Box<dyn ZendExtensionHandler> + Send + Sync>,
    hook_op_array: bool,
    hook_statements: bool,
    hook_fcalls: bool,
}

impl<'a> ZendExtensionBuilder<'a> {
    pub(crate) fn new<F, H>(module: crate::builders::ModuleBuilder<'a>, factory: F) -> Self
    where
        F: Fn() -> H + Send + Sync + 'static,
        H: ZendExtensionHandler,
    {
        Self {
            module: Some(module),
            factory: Box::new(move || Box::new(factory())),
            hook_op_array: false,
            hook_statements: false,
            hook_fcalls: false,
        }
    }

    /// Enable `on_op_array_compiled`, called after PHP finishes compiling
    /// each function or method.
    ///
    /// Unlike [`Self::hook_statements`] and [`Self::hook_fcalls`], this
    /// opt-in does not change the bytecode PHP emits: the matching flag
    /// (`ZEND_COMPILE_HANDLE_OP_ARRAY`) is already part of PHP's default
    /// `CG(compiler_options)`. The opt-in only controls whether the
    /// dispatcher is registered, so the cost scales with the number of
    /// compiled functions, not with every opcode.
    pub fn hook_op_array_compile(mut self) -> Self {
        self.hook_op_array = true;
        self
    }

    /// Enable `on_statement` -- called for every executed statement.
    ///
    /// Flips `ZEND_COMPILE_EXTENDED_STMT`, which causes PHP to emit extra
    /// `ZEND_EXT_STMT` opcodes in every compiled script. Opt in only if your
    /// profiler actually needs per-statement granularity.
    pub fn hook_statements(mut self) -> Self {
        self.hook_statements = true;
        self
    }

    /// Enable `on_fcall_begin` / `on_fcall_end` -- legacy per-call-site hooks.
    ///
    /// Flips `ZEND_COMPILE_EXTENDED_FCALL`, which causes PHP to emit
    /// `ZEND_EXT_FCALL_BEGIN`/`END` opcodes around every call site.
    pub fn hook_fcalls(mut self) -> Self {
        self.hook_fcalls = true;
        self
    }

    /// Consume the builder, register the extension, and return the outer
    /// [`ModuleBuilder`](crate::builders::ModuleBuilder) so further module
    /// config can be chained.
    ///
    /// # Panics
    ///
    /// Panics if a `ZendExtensionHandler` has already been registered on this
    /// module. Each extension may register at most one handler.
    pub fn finish(self) -> crate::builders::ModuleBuilder<'a> {
        register_config(ZendExtensionConfig {
            factory: self.factory,
            hook_op_array: self.hook_op_array,
            hook_statements: self.hook_statements,
            hook_fcalls: self.hook_fcalls,
        });
        self.module
            .expect("ZendExtensionBuilder::finish called on test instance")
    }

    #[cfg(test)]
    pub(crate) fn __for_tests<F, H>(factory: F) -> Self
    where
        F: Fn() -> H + Send + Sync + 'static,
        H: ZendExtensionHandler,
    {
        Self {
            module: None,
            factory: Box::new(move || Box::new(factory())),
            hook_op_array: false,
            hook_statements: false,
            hook_fcalls: false,
        }
    }

    #[cfg(test)]
    pub(crate) fn opts(&self) -> (bool, bool, bool) {
        (self.hook_op_array, self.hook_statements, self.hook_fcalls)
    }
}

pub(crate) fn register_config(config: ZendExtensionConfig) {
    assert!(
        ZEND_EXT_CONFIG.set(config).is_ok(),
        "zend_extension can only be registered once per module",
    );
}

// ============================================================================
// extern "C" dispatchers
// ============================================================================

/// # Safety
///
/// Called from PHP's C code.
unsafe extern "C" fn ext_op_array_handler(op_array: *mut ffi::zend_op_array) {
    if let Some(handler) = get_handler()
        && let Some(op) = unsafe { op_array.as_mut() }
    {
        handler.on_op_array_compiled(op);
    }
}

/// # Safety
///
/// Called from PHP's C code.
unsafe extern "C" fn ext_statement_handler(execute_data: *mut ffi::zend_execute_data) {
    if let Some(handler) = get_handler()
        && let Some(ex) = unsafe { execute_data.as_ref() }
    {
        handler.on_statement(ex);
    }
}

/// # Safety
///
/// Called from PHP's C code.
unsafe extern "C" fn ext_fcall_begin_handler(execute_data: *mut ffi::zend_execute_data) {
    if let Some(handler) = get_handler()
        && let Some(ex) = unsafe { execute_data.as_ref() }
    {
        handler.on_fcall_begin(ex);
    }
}

/// # Safety
///
/// Called from PHP's C code.
unsafe extern "C" fn ext_fcall_end_handler(execute_data: *mut ffi::zend_execute_data) {
    if let Some(handler) = get_handler()
        && let Some(ex) = unsafe { execute_data.as_ref() }
    {
        handler.on_fcall_end(ex);
    }
}

/// # Safety
///
/// Called from PHP's C code.
unsafe extern "C" fn ext_op_array_ctor(op_array: *mut ffi::zend_op_array) {
    if let Some(handler) = get_handler()
        && let Some(op) = unsafe { op_array.as_mut() }
    {
        handler.on_op_array_ctor(op);
    }
}

/// # Safety
///
/// Called from PHP's C code.
unsafe extern "C" fn ext_op_array_dtor(op_array: *mut ffi::zend_op_array) {
    if let Some(handler) = get_handler()
        && let Some(op) = unsafe { op_array.as_mut() }
    {
        handler.on_op_array_dtor(op);
    }
}

/// # Safety
///
/// Called from PHP's C code.
unsafe extern "C" fn ext_message_handler(message: i32, arg: *mut c_void) {
    if let Some(handler) = get_handler() {
        handler.on_message(message, arg);
    }
}

/// # Safety
///
/// Called from PHP's C code.
unsafe extern "C" fn ext_activate() {
    if let Some(cfg) = ZEND_EXT_CONFIG.get() {
        apply_compiler_flags(cfg);
    }
    if let Some(handler) = get_handler() {
        handler.on_activate();
    }
}

/// # Safety
///
/// Called from PHP's C code.
unsafe extern "C" fn ext_deactivate() {
    if let Some(handler) = get_handler() {
        handler.on_deactivate();
    }
}

// ============================================================================
// Registration
// ============================================================================

/// # Safety
///
/// Must be called during MINIT phase only.
pub(crate) unsafe fn zend_extension_startup(name: &str, version: &str) {
    let Some(cfg) = ZEND_EXT_CONFIG.get() else {
        return;
    };

    let _ = ZEND_EXT_INSTANCE.set((cfg.factory)());

    let c_name = CString::new(name).expect("zend extension name must not contain nul bytes");
    let c_version =
        CString::new(version).expect("zend extension version must not contain nul bytes");
    let name_ptr = EXT_NAME.get_or_init(|| c_name).as_ptr();
    let version_ptr = EXT_VERSION.get_or_init(|| c_version).as_ptr();

    let mut ext: ffi::zend_extension = unsafe { std::mem::zeroed() };
    ext.name = name_ptr;
    ext.version = version_ptr;

    // Always-on cold-path hooks.
    ext.activate = Some(ext_activate);
    ext.deactivate = Some(ext_deactivate);
    ext.message_handler = Some(ext_message_handler);
    ext.op_array_ctor = Some(ext_op_array_ctor);
    ext.op_array_dtor = Some(ext_op_array_dtor);

    // Opt-in hot-path hooks.
    if cfg.hook_op_array {
        ext.op_array_handler = Some(ext_op_array_handler);
    }
    if cfg.hook_statements {
        ext.statement_handler = Some(ext_statement_handler);
    }
    if cfg.hook_fcalls {
        ext.fcall_begin_handler = Some(ext_fcall_begin_handler);
        ext.fcall_end_handler = Some(ext_fcall_end_handler);
    }

    unsafe {
        crate::zend::register_extension(&raw mut ext);
    }
    apply_compiler_flags(cfg);
}

fn apply_compiler_flags(cfg: &ZendExtensionConfig) {
    let flags = compile_flags_for(cfg);
    if flags != 0 {
        let mut cg = super::globals::CompilerGlobals::get_mut();
        cg.compiler_options |= flags;
    }
}

// ============================================================================
// Tests
// ============================================================================

#[cfg(test)]
mod tests {
    use super::*;

    fn cfg(stmts: bool, fcalls: bool, op_array: bool) -> ZendExtensionConfig {
        ZendExtensionConfig {
            factory: Box::new(|| Box::new(NoopHandler)),
            hook_statements: stmts,
            hook_fcalls: fcalls,
            hook_op_array: op_array,
        }
    }

    struct NoopHandler;
    impl ZendExtensionHandler for NoopHandler {}

    // -- compile_flags_for tests --

    #[test]
    fn compile_flags_for_none_is_zero() {
        assert_eq!(compile_flags_for(&cfg(false, false, false)), 0);
    }

    #[test]
    fn compile_flags_for_statements_only() {
        assert_eq!(
            compile_flags_for(&cfg(true, false, false)),
            ZEND_COMPILE_EXTENDED_STMT,
        );
    }

    #[test]
    fn compile_flags_for_fcalls_only() {
        assert_eq!(
            compile_flags_for(&cfg(false, true, false)),
            ZEND_COMPILE_EXTENDED_FCALL,
        );
    }

    #[test]
    fn compile_flags_for_op_array_only() {
        assert_eq!(
            compile_flags_for(&cfg(false, false, true)),
            ZEND_COMPILE_HANDLE_OP_ARRAY,
        );
    }

    #[test]
    fn compile_flags_for_all_is_or() {
        assert_eq!(
            compile_flags_for(&cfg(true, true, true)),
            ZEND_COMPILE_EXTENDED_STMT | ZEND_COMPILE_EXTENDED_FCALL | ZEND_COMPILE_HANDLE_OP_ARRAY,
        );
    }

    // -- builder state tests --

    #[test]
    fn builder_starts_with_no_hooks() {
        let b = ZendExtensionBuilder::__for_tests(|| NoopHandler);
        assert_eq!(b.opts(), (false, false, false));
    }

    #[test]
    fn builder_hook_statements_flips_only_statements() {
        let b = ZendExtensionBuilder::__for_tests(|| NoopHandler).hook_statements();
        assert_eq!(b.opts(), (false, true, false));
    }

    #[test]
    fn builder_hook_fcalls_flips_only_fcalls() {
        let b = ZendExtensionBuilder::__for_tests(|| NoopHandler).hook_fcalls();
        assert_eq!(b.opts(), (false, false, true));
    }

    #[test]
    fn builder_hook_op_array_compile_flips_only_op_array() {
        let b = ZendExtensionBuilder::__for_tests(|| NoopHandler).hook_op_array_compile();
        assert_eq!(b.opts(), (true, false, false));
    }

    #[test]
    fn builder_hooks_compose() {
        let b = ZendExtensionBuilder::__for_tests(|| NoopHandler)
            .hook_statements()
            .hook_fcalls()
            .hook_op_array_compile();
        assert_eq!(b.opts(), (true, true, true));
    }

    #[test]
    fn builder_hooks_idempotent() {
        let b = ZendExtensionBuilder::__for_tests(|| NoopHandler)
            .hook_statements()
            .hook_statements();
        assert_eq!(b.opts(), (false, true, false));
    }

    #[test]
    fn trait_lifecycle_defaults_are_no_op() {
        // The other six defaults (on_op_array_compiled, on_statement,
        // on_fcall_begin/end, on_op_array_ctor/dtor) require a PHP runtime
        // to construct their arguments; the integration test exercises
        // them end-to-end.
        struct Empty;
        impl ZendExtensionHandler for Empty {}
        let h = Empty;
        h.on_activate();
        h.on_deactivate();
        h.on_message(0, std::ptr::null_mut());
    }
}