use std::{convert::TryFrom, ffi::CString, mem, ptr};
use super::{ClassBuilder, FunctionBuilder};
use crate::{
PHP_DEBUG, PHP_ZTS,
class::RegisteredClass,
constant::IntoConst,
describe::DocComments,
error::Result,
ffi::{ZEND_MODULE_API_NO, ext_php_rs_php_build_id},
flags::ClassFlags,
zend::{FunctionEntry, ModuleEntry, ModuleGlobal, ModuleGlobals},
};
#[cfg(feature = "enum")]
use crate::{builders::enum_builder::EnumBuilder, enum_::RegisteredEnum};
#[must_use]
#[derive(Debug)]
pub struct ModuleBuilder<'a> {
pub(crate) name: String,
pub(crate) version: String,
pub(crate) functions: Vec<FunctionBuilder<'a>>,
pub(crate) constants: Vec<(String, Box<dyn IntoConst + Send>, DocComments)>,
pub(crate) classes: Vec<fn() -> ClassBuilder>,
pub(crate) interfaces: Vec<fn() -> ClassBuilder>,
#[cfg(feature = "enum")]
pub(crate) enums: Vec<fn() -> EnumBuilder>,
startup_func: Option<StartupShutdownFunc>,
shutdown_func: Option<StartupShutdownFunc>,
request_startup_func: Option<StartupShutdownFunc>,
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<'_> {
pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
Self {
name: name.into(),
version: version.into(),
..Default::default()
}
}
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = name.into();
self
}
pub fn version(mut self, version: impl Into<String>) -> Self {
self.version = version.into();
self
}
pub fn startup_function(mut self, func: StartupShutdownFunc) -> Self {
self.startup_func = Some(func);
self
}
pub fn shutdown_function(mut self, func: StartupShutdownFunc) -> Self {
self.shutdown_func = Some(func);
self
}
pub fn request_startup_function(mut self, func: StartupShutdownFunc) -> Self {
self.request_startup_func = Some(func);
self
}
pub fn request_shutdown_function(mut self, func: StartupShutdownFunc) -> Self {
self.request_shutdown_func = Some(func);
self
}
pub fn post_deactivate_function(mut self, func: unsafe extern "C" fn() -> i32) -> Self {
self.post_deactivate_func = Some(func);
self
}
pub fn info_function(mut self, func: InfoFunc) -> Self {
self.info_func = Some(func);
self
}
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
}
#[cfg(feature = "observer")]
pub fn fcall_observer<F, O>(self, factory: F) -> Self
where
F: Fn() -> O + Send + Sync + 'static,
O: crate::zend::FcallObserver + Send + Sync,
{
let boxed_factory: Box<
dyn Fn() -> Box<dyn crate::zend::FcallObserver + Send + Sync> + Send + Sync,
> = Box::new(move || Box::new(factory()));
crate::zend::observer::register_fcall_observer_factory(boxed_factory);
self
}
#[cfg(feature = "observer")]
pub fn error_observer<F, O>(self, factory: F) -> Self
where
F: Fn() -> O + Send + Sync + 'static,
O: crate::zend::ErrorObserver + Send + Sync,
{
let boxed_factory: Box<
dyn Fn() -> Box<dyn crate::zend::ErrorObserver + Send + Sync> + Send + Sync,
> = Box::new(move || Box::new(factory()));
crate::zend::error_observer::register_error_observer_factory(boxed_factory);
self
}
#[cfg(feature = "observer")]
pub fn exception_observer<F, O>(self, factory: F) -> Self
where
F: Fn() -> O + Send + Sync + 'static,
O: crate::zend::ExceptionObserver + Send + Sync,
{
let boxed_factory: Box<
dyn Fn() -> Box<dyn crate::zend::ExceptionObserver + Send + Sync> + Send + Sync,
> = Box::new(move || Box::new(factory()));
crate::zend::exception_observer::register_exception_observer_factory(boxed_factory);
self
}
pub fn function(mut self, func: FunctionBuilder<'static>) -> Self {
self.functions.push(func);
self
}
pub fn constant(
mut self,
r#const: (&str, impl IntoConst + Send + 'static, DocComments),
) -> Self {
let (name, val, docs) = r#const;
self.constants.push((
name.into(),
Box::new(val) as Box<dyn IntoConst + Send>,
docs,
));
self
}
pub fn interface<T: RegisteredClass>(mut self) -> Self {
self.interfaces.push(|| {
let mut builder = ClassBuilder::new(T::CLASS_NAME);
for (method, flags) in T::method_builders() {
builder = builder.method(method, flags);
}
for interface in T::IMPLEMENTS {
builder = builder.implements(*interface);
}
for (name, value, docs) in T::constants() {
builder = builder
.dyn_constant(*name, *value, docs)
.expect("Failed to register constant");
}
if let Some(modifier) = T::BUILDER_MODIFIER {
builder = modifier(builder);
}
builder = builder.flags(ClassFlags::Interface);
builder
.registration(|ce| {
T::get_metadata().set_ce(ce);
})
.docs(T::DOC_COMMENTS)
});
self
}
pub fn class<T: RegisteredClass>(mut self) -> Self {
self.classes.push(|| {
let mut builder = ClassBuilder::new(T::CLASS_NAME);
for (method, flags) in T::method_builders() {
builder = builder.method(method, flags);
}
for (method, flags) in T::interface_method_implementations() {
builder = builder.method(method, flags);
}
if let Some(parent) = T::EXTENDS {
builder = builder.extends(parent);
}
for interface in T::IMPLEMENTS {
builder = builder.implements(*interface);
}
for interface in T::interface_implementations() {
builder = builder.implements(interface);
}
for (name, value, docs) in T::constants() {
builder = builder
.dyn_constant(*name, *value, docs)
.expect("Failed to register constant");
}
for (name, prop_info) in T::get_properties() {
let default_stub = if prop_info.nullable {
Some("null".into())
} else {
None
};
builder = builder.property(crate::builders::ClassProperty {
name: name.into(),
flags: prop_info.flags,
default: None,
docs: prop_info.docs,
ty: Some(prop_info.ty),
nullable: prop_info.nullable,
readonly: prop_info.readonly,
default_stub,
});
}
for (name, flags, default, docs) in T::static_properties() {
let default_stub = default.map(crate::convert::IntoZvalDyn::stub_value);
let default_fn = default.map(|v| {
Box::new(move || v.as_zval(true))
as Box<dyn FnOnce() -> crate::error::Result<crate::types::Zval>>
});
builder = builder.property(crate::builders::ClassProperty {
name: (*name).into(),
flags: *flags,
default: default_fn,
docs,
ty: None,
nullable: false,
readonly: false,
default_stub,
});
}
if let Some(modifier) = T::BUILDER_MODIFIER {
builder = modifier(builder);
}
builder
.flags(T::FLAGS)
.object_override::<T>()
.registration(|ce| {
T::get_metadata().set_ce(ce);
})
.docs(T::DOC_COMMENTS)
});
self
}
#[cfg(feature = "enum")]
pub fn enumeration<T>(mut self) -> Self
where
T: RegisteredClass + RegisteredEnum,
{
self.enums.push(|| {
let mut builder = EnumBuilder::new(T::CLASS_NAME);
for case in T::CASES {
builder = builder.case(case);
}
for (method, flags) in T::method_builders() {
builder = builder.method(method, flags);
}
builder
.registration(|ce| {
T::get_metadata().set_ce(ce);
})
.docs(T::DOC_COMMENTS)
});
self
}
}
pub struct ModuleStartup {
constants: Vec<(String, Box<dyn IntoConst + Send>)>,
classes: Vec<fn() -> ClassBuilder>,
interfaces: Vec<fn() -> ClassBuilder>,
#[cfg(feature = "enum")]
enums: Vec<fn() -> EnumBuilder>,
}
impl ModuleStartup {
pub fn startup(self, _ty: i32, mod_num: i32) -> Result<()> {
for (name, val) in self.constants {
val.register_constant(&name, mod_num)?;
}
self.interfaces.into_iter().map(|c| c()).for_each(|c| {
c.register().expect("Failed to build interface");
});
self.classes.into_iter().map(|c| c()).for_each(|c| {
c.register().expect("Failed to build class");
});
#[cfg(feature = "enum")]
self.enums
.into_iter()
.map(|builder| builder())
.for_each(|e| {
e.register().expect("Failed to build enum");
});
#[cfg(feature = "observer")]
unsafe {
crate::zend::observer::observer_startup();
crate::zend::error_observer::error_observer_startup();
crate::zend::exception_observer::exception_observer_startup();
}
Ok(())
}
}
pub type StartupShutdownFunc = unsafe extern "C" fn(_type: i32, _module_number: i32) -> i32;
pub type InfoFunc = unsafe extern "C" fn(zend_module: *mut ModuleEntry);
impl TryFrom<ModuleBuilder<'_>> for (ModuleEntry, ModuleStartup) {
type Error = crate::error::Error;
fn try_from(builder: ModuleBuilder) -> Result<Self, Self::Error> {
let mut functions = builder
.functions
.into_iter()
.map(FunctionBuilder::build)
.collect::<Result<Vec<_>>>()?;
functions.push(FunctionEntry::end());
let functions = Box::into_raw(functions.into_boxed_slice()) as *const FunctionEntry;
let name = CString::new(builder.name)?.into_raw();
let version = CString::new(builder.version)?.into_raw();
let startup = ModuleStartup {
constants: builder
.constants
.into_iter()
.map(|(n, v, _)| (n, v))
.collect(),
classes: builder.classes,
interfaces: builder.interfaces,
#[cfg(feature = "enum")]
enums: builder.enums,
};
#[cfg(not(php_zts))]
let module_entry = ModuleEntry {
size: mem::size_of::<ModuleEntry>().try_into()?,
zend_api: ZEND_MODULE_API_NO,
zend_debug: u8::from(PHP_DEBUG),
zts: u8::from(PHP_ZTS),
ini_entry: ptr::null(),
deps: ptr::null(),
name,
functions,
module_startup_func: builder.startup_func,
module_shutdown_func: builder.shutdown_func,
request_startup_func: builder.request_startup_func,
request_shutdown_func: builder.request_shutdown_func,
info_func: builder.info_func,
version,
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,
handle: ptr::null_mut(),
module_number: 0,
build_id: unsafe { ext_php_rs_php_build_id() },
};
#[cfg(php_zts)]
let module_entry = ModuleEntry {
size: mem::size_of::<ModuleEntry>().try_into()?,
zend_api: ZEND_MODULE_API_NO,
zend_debug: u8::from(PHP_DEBUG),
zts: u8::from(PHP_ZTS),
ini_entry: ptr::null(),
deps: ptr::null(),
name,
functions,
module_startup_func: builder.startup_func,
module_shutdown_func: builder.shutdown_func,
request_startup_func: builder.request_startup_func,
request_shutdown_func: builder.request_shutdown_func,
info_func: builder.info_func,
version,
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,
handle: ptr::null_mut(),
module_number: 0,
build_id: unsafe { ext_php_rs_php_build_id() },
};
Ok((module_entry, startup))
}
}
#[cfg(test)]
mod tests {
use crate::test::{
test_deactivate_function, test_function, test_info_function, test_startup_shutdown_function,
};
use super::*;
#[test]
fn test_new() {
let builder = ModuleBuilder::new("test", "1.0");
assert_eq!(builder.name, "test");
assert_eq!(builder.version, "1.0");
assert!(builder.functions.is_empty());
assert!(builder.constants.is_empty());
assert!(builder.classes.is_empty());
assert!(builder.interfaces.is_empty());
assert!(builder.startup_func.is_none());
assert!(builder.shutdown_func.is_none());
assert!(builder.request_startup_func.is_none());
assert!(builder.request_shutdown_func.is_none());
assert!(builder.post_deactivate_func.is_none());
assert!(builder.info_func.is_none());
#[cfg(feature = "enum")]
assert!(builder.enums.is_empty());
}
#[test]
fn test_name() {
let builder = ModuleBuilder::new("test", "1.0").name("new_test");
assert_eq!(builder.name, "new_test");
}
#[test]
fn test_version() {
let builder = ModuleBuilder::new("test", "1.0").version("2.0");
assert_eq!(builder.version, "2.0");
}
#[test]
fn test_startup_function() {
let builder =
ModuleBuilder::new("test", "1.0").startup_function(test_startup_shutdown_function);
assert!(builder.startup_func.is_some());
}
#[test]
fn test_shutdown_function() {
let builder =
ModuleBuilder::new("test", "1.0").shutdown_function(test_startup_shutdown_function);
assert!(builder.shutdown_func.is_some());
}
#[test]
fn test_request_startup_function() {
let builder = ModuleBuilder::new("test", "1.0")
.request_startup_function(test_startup_shutdown_function);
assert!(builder.request_startup_func.is_some());
}
#[test]
fn test_request_shutdown_function() {
let builder = ModuleBuilder::new("test", "1.0")
.request_shutdown_function(test_startup_shutdown_function);
assert!(builder.request_shutdown_func.is_some());
}
#[test]
fn test_set_post_deactivate_function() {
let builder =
ModuleBuilder::new("test", "1.0").post_deactivate_function(test_deactivate_function);
assert!(builder.post_deactivate_func.is_some());
}
#[test]
fn test_set_info_function() {
let builder = ModuleBuilder::new("test", "1.0").info_function(test_info_function);
assert!(builder.info_func.is_some());
}
#[test]
fn test_add_function() {
let builder =
ModuleBuilder::new("test", "1.0").function(FunctionBuilder::new("test", test_function));
assert_eq!(builder.functions.len(), 1);
}
#[test]
#[cfg(feature = "embed")]
fn test_add_constant() {
let builder =
ModuleBuilder::new("test", "1.0").constant(("TEST_CONST", 42, DocComments::default()));
assert_eq!(builder.constants.len(), 1);
assert_eq!(builder.constants[0].0, "TEST_CONST");
assert_eq!(builder.constants[0].2, DocComments::default());
}
}