1use std::ffi::{CString, c_void};
21use std::sync::OnceLock;
22
23use crate::ffi;
24use crate::zend::ExecuteData;
25
26pub trait ZendExtensionHandler: Send + Sync + 'static {
34 fn on_op_array_compiled(&self, _op_array: &mut ffi::zend_op_array) {}
37
38 fn on_statement(&self, _execute_data: &ExecuteData) {}
41
42 fn on_fcall_begin(&self, _execute_data: &ExecuteData) {}
45
46 fn on_fcall_end(&self, _execute_data: &ExecuteData) {}
49
50 fn on_op_array_ctor(&self, _op_array: &mut ffi::zend_op_array) {}
52
53 fn on_op_array_dtor(&self, _op_array: &mut ffi::zend_op_array) {}
55
56 fn on_message(&self, _message: i32, _arg: *mut c_void) {}
58
59 fn on_activate(&self) {}
61
62 fn on_deactivate(&self) {}
64}
65
66pub(crate) struct ZendExtensionConfig {
71 pub(crate) factory: Box<dyn Fn() -> Box<dyn ZendExtensionHandler> + Send + Sync>,
72 pub(crate) hook_op_array: bool,
73 pub(crate) hook_statements: bool,
74 pub(crate) hook_fcalls: bool,
75}
76
77static ZEND_EXT_CONFIG: OnceLock<ZendExtensionConfig> = OnceLock::new();
78static ZEND_EXT_INSTANCE: OnceLock<Box<dyn ZendExtensionHandler>> = OnceLock::new();
79static EXT_NAME: OnceLock<CString> = OnceLock::new();
80static EXT_VERSION: OnceLock<CString> = OnceLock::new();
81
82fn get_handler() -> Option<&'static dyn ZendExtensionHandler> {
83 ZEND_EXT_INSTANCE.get().map(std::convert::AsRef::as_ref)
84}
85
86const ZEND_COMPILE_EXTENDED_STMT: u32 = 1 << 0;
89const ZEND_COMPILE_EXTENDED_FCALL: u32 = 1 << 1;
90const ZEND_COMPILE_HANDLE_OP_ARRAY: u32 = 1 << 2;
91
92fn compile_flags_for(cfg: &ZendExtensionConfig) -> u32 {
93 let mut flags = 0u32;
94 if cfg.hook_op_array {
95 flags |= ZEND_COMPILE_HANDLE_OP_ARRAY;
96 }
97 if cfg.hook_statements {
98 flags |= ZEND_COMPILE_EXTENDED_STMT;
99 }
100 if cfg.hook_fcalls {
101 flags |= ZEND_COMPILE_EXTENDED_FCALL;
102 }
103 flags
104}
105
106#[must_use = "call .finish() to return to ModuleBuilder"]
124pub struct ZendExtensionBuilder<'a> {
125 module: Option<crate::builders::ModuleBuilder<'a>>,
126 factory: Box<dyn Fn() -> Box<dyn ZendExtensionHandler> + Send + Sync>,
127 hook_op_array: bool,
128 hook_statements: bool,
129 hook_fcalls: bool,
130}
131
132impl<'a> ZendExtensionBuilder<'a> {
133 pub(crate) fn new<F, H>(module: crate::builders::ModuleBuilder<'a>, factory: F) -> Self
134 where
135 F: Fn() -> H + Send + Sync + 'static,
136 H: ZendExtensionHandler,
137 {
138 Self {
139 module: Some(module),
140 factory: Box::new(move || Box::new(factory())),
141 hook_op_array: false,
142 hook_statements: false,
143 hook_fcalls: false,
144 }
145 }
146
147 pub fn hook_op_array_compile(mut self) -> Self {
157 self.hook_op_array = true;
158 self
159 }
160
161 pub fn hook_statements(mut self) -> Self {
167 self.hook_statements = true;
168 self
169 }
170
171 pub fn hook_fcalls(mut self) -> Self {
176 self.hook_fcalls = true;
177 self
178 }
179
180 #[expect(
189 clippy::expect_used,
190 reason = "the module is only absent on the test-only constructor"
191 )]
192 pub fn finish(self) -> crate::builders::ModuleBuilder<'a> {
193 register_config(ZendExtensionConfig {
194 factory: self.factory,
195 hook_op_array: self.hook_op_array,
196 hook_statements: self.hook_statements,
197 hook_fcalls: self.hook_fcalls,
198 });
199 self.module
200 .expect("ZendExtensionBuilder::finish called on test instance")
201 }
202
203 #[cfg(test)]
204 pub(crate) fn __for_tests<F, H>(factory: F) -> Self
205 where
206 F: Fn() -> H + Send + Sync + 'static,
207 H: ZendExtensionHandler,
208 {
209 Self {
210 module: None,
211 factory: Box::new(move || Box::new(factory())),
212 hook_op_array: false,
213 hook_statements: false,
214 hook_fcalls: false,
215 }
216 }
217
218 #[cfg(test)]
219 pub(crate) fn opts(&self) -> (bool, bool, bool) {
220 (self.hook_op_array, self.hook_statements, self.hook_fcalls)
221 }
222}
223
224pub(crate) fn register_config(config: ZendExtensionConfig) {
225 assert!(
226 ZEND_EXT_CONFIG.set(config).is_ok(),
227 "zend_extension can only be registered once per module",
228 );
229}
230
231unsafe extern "C" fn ext_op_array_handler(op_array: *mut ffi::zend_op_array) {
239 if let Some(handler) = get_handler()
240 && let Some(op) = unsafe { op_array.as_mut() }
241 {
242 handler.on_op_array_compiled(op);
243 }
244}
245
246unsafe extern "C" fn ext_statement_handler(execute_data: *mut ffi::zend_execute_data) {
250 if let Some(handler) = get_handler()
251 && let Some(ex) = unsafe { execute_data.as_ref() }
252 {
253 handler.on_statement(ex);
254 }
255}
256
257unsafe extern "C" fn ext_fcall_begin_handler(execute_data: *mut ffi::zend_execute_data) {
261 if let Some(handler) = get_handler()
262 && let Some(ex) = unsafe { execute_data.as_ref() }
263 {
264 handler.on_fcall_begin(ex);
265 }
266}
267
268unsafe extern "C" fn ext_fcall_end_handler(execute_data: *mut ffi::zend_execute_data) {
272 if let Some(handler) = get_handler()
273 && let Some(ex) = unsafe { execute_data.as_ref() }
274 {
275 handler.on_fcall_end(ex);
276 }
277}
278
279unsafe extern "C" fn ext_op_array_ctor(op_array: *mut ffi::zend_op_array) {
283 if let Some(handler) = get_handler()
284 && let Some(op) = unsafe { op_array.as_mut() }
285 {
286 handler.on_op_array_ctor(op);
287 }
288}
289
290unsafe extern "C" fn ext_op_array_dtor(op_array: *mut ffi::zend_op_array) {
294 if let Some(handler) = get_handler()
295 && let Some(op) = unsafe { op_array.as_mut() }
296 {
297 handler.on_op_array_dtor(op);
298 }
299}
300
301unsafe extern "C" fn ext_message_handler(message: i32, arg: *mut c_void) {
305 if let Some(handler) = get_handler() {
306 handler.on_message(message, arg);
307 }
308}
309
310unsafe extern "C" fn ext_activate() {
314 if let Some(cfg) = ZEND_EXT_CONFIG.get() {
315 apply_compiler_flags(cfg);
316 }
317 if let Some(handler) = get_handler() {
318 handler.on_activate();
319 }
320}
321
322unsafe extern "C" fn ext_deactivate() {
326 if let Some(handler) = get_handler() {
327 handler.on_deactivate();
328 }
329}
330
331#[expect(
339 clippy::expect_used,
340 reason = "the name and version come from the module builder, i.e. from Cargo metadata compiled into the extension"
341)]
342pub(crate) unsafe fn zend_extension_startup(name: &str, version: &str) {
343 let Some(cfg) = ZEND_EXT_CONFIG.get() else {
344 return;
345 };
346
347 let _ = ZEND_EXT_INSTANCE.set((cfg.factory)());
348
349 let c_name = CString::new(name).expect("zend extension name must not contain nul bytes");
350 let c_version =
351 CString::new(version).expect("zend extension version must not contain nul bytes");
352 let name_ptr = EXT_NAME.get_or_init(|| c_name).as_ptr();
353 let version_ptr = EXT_VERSION.get_or_init(|| c_version).as_ptr();
354
355 let mut ext: ffi::zend_extension = unsafe { std::mem::zeroed() };
356 ext.name = name_ptr;
357 ext.version = version_ptr;
358
359 ext.activate = Some(ext_activate);
361 ext.deactivate = Some(ext_deactivate);
362 ext.message_handler = Some(ext_message_handler);
363 ext.op_array_ctor = Some(ext_op_array_ctor);
364 ext.op_array_dtor = Some(ext_op_array_dtor);
365
366 if cfg.hook_op_array {
368 ext.op_array_handler = Some(ext_op_array_handler);
369 }
370 if cfg.hook_statements {
371 ext.statement_handler = Some(ext_statement_handler);
372 }
373 if cfg.hook_fcalls {
374 ext.fcall_begin_handler = Some(ext_fcall_begin_handler);
375 ext.fcall_end_handler = Some(ext_fcall_end_handler);
376 }
377
378 unsafe {
379 crate::zend::register_extension(&raw mut ext);
380 }
381 apply_compiler_flags(cfg);
382}
383
384fn apply_compiler_flags(cfg: &ZendExtensionConfig) {
385 let flags = compile_flags_for(cfg);
386 if flags != 0 {
387 let mut cg = super::globals::CompilerGlobals::get_mut();
388 cg.compiler_options |= flags;
389 }
390}
391
392#[cfg(test)]
397mod tests {
398 use super::*;
399
400 fn cfg(stmts: bool, fcalls: bool, op_array: bool) -> ZendExtensionConfig {
401 ZendExtensionConfig {
402 factory: Box::new(|| Box::new(NoopHandler)),
403 hook_statements: stmts,
404 hook_fcalls: fcalls,
405 hook_op_array: op_array,
406 }
407 }
408
409 struct NoopHandler;
410 impl ZendExtensionHandler for NoopHandler {}
411
412 #[test]
415 fn compile_flags_for_none_is_zero() {
416 assert_eq!(compile_flags_for(&cfg(false, false, false)), 0);
417 }
418
419 #[test]
420 fn compile_flags_for_statements_only() {
421 assert_eq!(
422 compile_flags_for(&cfg(true, false, false)),
423 ZEND_COMPILE_EXTENDED_STMT,
424 );
425 }
426
427 #[test]
428 fn compile_flags_for_fcalls_only() {
429 assert_eq!(
430 compile_flags_for(&cfg(false, true, false)),
431 ZEND_COMPILE_EXTENDED_FCALL,
432 );
433 }
434
435 #[test]
436 fn compile_flags_for_op_array_only() {
437 assert_eq!(
438 compile_flags_for(&cfg(false, false, true)),
439 ZEND_COMPILE_HANDLE_OP_ARRAY,
440 );
441 }
442
443 #[test]
444 fn compile_flags_for_all_is_or() {
445 assert_eq!(
446 compile_flags_for(&cfg(true, true, true)),
447 ZEND_COMPILE_EXTENDED_STMT | ZEND_COMPILE_EXTENDED_FCALL | ZEND_COMPILE_HANDLE_OP_ARRAY,
448 );
449 }
450
451 #[test]
454 fn builder_starts_with_no_hooks() {
455 let b = ZendExtensionBuilder::__for_tests(|| NoopHandler);
456 assert_eq!(b.opts(), (false, false, false));
457 }
458
459 #[test]
460 fn builder_hook_statements_flips_only_statements() {
461 let b = ZendExtensionBuilder::__for_tests(|| NoopHandler).hook_statements();
462 assert_eq!(b.opts(), (false, true, false));
463 }
464
465 #[test]
466 fn builder_hook_fcalls_flips_only_fcalls() {
467 let b = ZendExtensionBuilder::__for_tests(|| NoopHandler).hook_fcalls();
468 assert_eq!(b.opts(), (false, false, true));
469 }
470
471 #[test]
472 fn builder_hook_op_array_compile_flips_only_op_array() {
473 let b = ZendExtensionBuilder::__for_tests(|| NoopHandler).hook_op_array_compile();
474 assert_eq!(b.opts(), (true, false, false));
475 }
476
477 #[test]
478 fn builder_hooks_compose() {
479 let b = ZendExtensionBuilder::__for_tests(|| NoopHandler)
480 .hook_statements()
481 .hook_fcalls()
482 .hook_op_array_compile();
483 assert_eq!(b.opts(), (true, true, true));
484 }
485
486 #[test]
487 fn builder_hooks_idempotent() {
488 let b = ZendExtensionBuilder::__for_tests(|| NoopHandler)
489 .hook_statements()
490 .hook_statements();
491 assert_eq!(b.opts(), (false, true, false));
492 }
493
494 #[test]
495 fn trait_lifecycle_defaults_are_no_op() {
496 struct Empty;
501 impl ZendExtensionHandler for Empty {}
502 let h = Empty;
503 h.on_activate();
504 h.on_deactivate();
505 h.on_message(0, std::ptr::null_mut());
506 }
507}