use std::{
ffi::c_int,
fmt, mem,
panic::AssertUnwindSafe,
ptr,
sync::{
OnceLock,
atomic::{AtomicPtr, Ordering},
},
};
use crate::{
error::{Error, Result},
ffi::{MODULE_TEMPORARY, ZEND_RESULT_CODE_SUCCESS, ext_php_rs_sapi_globals},
zend::{
CatchError, ExecutorGlobals, SapiModule, SapiRequestInfo, bailout, globals::lock, try_catch,
},
};
type Activate = unsafe extern "C" fn() -> c_int;
type Shutdown = unsafe extern "C" fn(c_int, c_int) -> c_int;
pub(crate) struct ActivateCallback(Box<dyn Fn(&SapiRequestInfo) + Send + Sync>);
impl ActivateCallback {
pub(crate) fn new<F>(callback: F) -> Self
where
F: Fn(&SapiRequestInfo) + Send + Sync + 'static,
{
Self(Box::new(callback))
}
}
impl fmt::Debug for ActivateCallback {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("ActivateCallback")
}
}
static CALLBACK: OnceLock<ActivateCallback> = OnceLock::new();
static USER_SHUTDOWN: OnceLock<Shutdown> = OnceLock::new();
static PREVIOUS: AtomicPtr<()> = AtomicPtr::new(ptr::null_mut());
pub(crate) fn set_callback(callback: Option<ActivateCallback>) {
if let Some(callback) = callback {
let _ = CALLBACK.set(callback);
}
}
pub(crate) unsafe fn install(ty: c_int) -> Result<()> {
if CALLBACK.get().is_none() {
return Ok(());
}
if ty == MODULE_TEMPORARY.cast_signed() {
return Err(Error::SapiActivateUnderDl);
}
let entry = ExecutorGlobals::get().current_module;
assert!(
!entry.is_null(),
"sapi_activate_function needs EG(current_module): start the module with zend_startup_module_ex"
);
let ours_shutdown: Shutdown = module_shutdown;
unsafe {
if let Some(user) = (*entry)
.module_shutdown_func
.filter(|&user| !ptr::fn_addr_eq(user, ours_shutdown))
{
let _ = USER_SHUTDOWN.set(user);
}
(*entry).module_shutdown_func = Some(ours_shutdown);
}
let ours: Activate = activate;
let mut module = SapiModule::get_mut();
let previous = module
.activate
.filter(|&previous| !ptr::fn_addr_eq(previous, ours))
.map_or(ptr::null_mut(), |previous| previous as *mut ());
PREVIOUS.store(previous, Ordering::Release);
module.activate = Some(ours);
Ok(())
}
unsafe fn uninstall() {
let ours: Activate = activate;
let mut module = SapiModule::get_mut();
if module
.activate
.is_some_and(|current| ptr::fn_addr_eq(current, ours))
{
module.activate = previous();
PREVIOUS.store(ptr::null_mut(), Ordering::Release);
}
}
fn previous() -> Option<Activate> {
let previous = PREVIOUS.load(Ordering::Acquire);
(!previous.is_null()).then(|| unsafe { mem::transmute::<*mut (), Activate>(previous) })
}
fn sapi_globals_borrowed_mut() -> bool {
cfg_if::cfg_if! {
if #[cfg(php_zts)] {
lock::SAPI_GLOBALS_LOCK.with(|lock| lock.is_locked_exclusive())
} else {
lock::SAPI_GLOBALS_LOCK.is_locked_exclusive()
}
}
}
unsafe extern "C" fn activate() -> c_int {
let result = previous().map_or(ZEND_RESULT_CODE_SUCCESS, |previous| unsafe { previous() });
let Some(callback) = CALLBACK.get() else {
return result;
};
if sapi_globals_borrowed_mut() {
return result;
}
let info = unsafe { (*ext_php_rs_sapi_globals()).request_info };
if let Err(CatchError::Bailout) = try_catch(AssertUnwindSafe(|| (callback.0)(&info))) {
unsafe { bailout() }
}
result
}
unsafe extern "C" fn module_shutdown(ty: c_int, module_number: c_int) -> c_int {
unsafe { uninstall() };
USER_SHUTDOWN
.get()
.map_or(ZEND_RESULT_CODE_SUCCESS, |shutdown| {
unsafe { shutdown(ty, module_number) }
})
}
#[cfg(all(test, feature = "embed"))]
mod tests {
use super::*;
use crate::{
embed::Embed,
ffi::MODULE_PERSISTENT,
zend::{ExecutorGlobals, ModuleEntry},
};
#[test]
fn second_install_does_not_chain_to_itself() {
let chained_to_itself = Embed::run(|| {
set_callback(Some(ActivateCallback::new(|_| {})));
let mut entry: ModuleEntry = unsafe { mem::zeroed() };
let current = mem::replace(
&mut ExecutorGlobals::get_mut().current_module,
&raw mut entry,
);
unsafe {
install(MODULE_PERSISTENT.cast_signed()).expect("first install failed");
install(MODULE_PERSISTENT.cast_signed()).expect("second install failed");
}
ExecutorGlobals::get_mut().current_module = current;
let ours: Activate = activate;
let chained = previous().is_some_and(|previous| ptr::fn_addr_eq(previous, ours));
unsafe { uninstall() };
chained
});
assert!(!chained_to_itself);
}
#[test]
fn install_refuses_dl_module() {
let refused = Embed::run(|| {
set_callback(Some(ActivateCallback::new(|_| {})));
let result = unsafe { install(MODULE_TEMPORARY.cast_signed()) };
matches!(result, Err(Error::SapiActivateUnderDl))
});
assert!(refused);
}
}