use std::collections::HashMap;
use crate::{
args::{Arg, ArgParser},
builders::{ClassBuilder, FunctionBuilder},
class::{ClassMetadata, RegisteredClass},
convert::{FromZval, IntoZval},
exception::PhpException,
flags::{DataType, MethodFlags},
props::Property,
types::Zval,
zend::ExecuteData,
zend_fastcall,
};
static CLOSURE_META: ClassMetadata<Closure> = ClassMetadata::new();
pub struct Closure(Box<dyn PhpClosure>);
unsafe impl Send for Closure {}
unsafe impl Sync for Closure {}
impl Closure {
pub fn wrap<T>(func: T) -> Self
where
T: PhpClosure + 'static,
{
Self(Box::new(func) as Box<dyn PhpClosure>)
}
pub fn wrap_once<T>(func: T) -> Self
where
T: PhpOnceClosure + 'static,
{
func.into_closure()
}
pub fn build() {
if CLOSURE_META.has_ce() {
panic!("Closure has already been built.");
}
let ce = ClassBuilder::new("RustClosure")
.method(
FunctionBuilder::new("__invoke", Self::invoke)
.not_required()
.arg(Arg::new("args", DataType::Mixed).is_variadic())
.returns(DataType::Mixed, false, true)
.build()
.expect("Failed to build `RustClosure` PHP class."),
MethodFlags::Public,
)
.object_override::<Self>()
.build()
.expect("Failed to build `RustClosure` PHP class.");
CLOSURE_META.set_ce(ce);
}
zend_fastcall! {
extern "C" fn invoke(ex: &mut ExecuteData, ret: &mut Zval) {
let (parser, this) = ex.parser_method::<Self>();
let this = this.expect("Internal closure function called on non-closure class");
this.0.invoke(parser, ret)
}
}
}
impl RegisteredClass for Closure {
const CLASS_NAME: &'static str = "RustClosure";
fn get_metadata() -> &'static ClassMetadata<Self> {
&CLOSURE_META
}
fn get_properties<'a>() -> HashMap<&'static str, Property<'a, Self>> {
HashMap::new()
}
}
class_derives!(Closure);
pub unsafe trait PhpClosure {
fn invoke<'a>(&'a mut self, parser: ArgParser<'a, '_>, ret: &mut Zval);
}
pub trait PhpOnceClosure {
fn into_closure(self) -> Closure;
}
unsafe impl<R> PhpClosure for Box<dyn Fn() -> R>
where
R: IntoZval,
{
fn invoke(&mut self, _: ArgParser, ret: &mut Zval) {
if let Err(e) = self().set_zval(ret, false) {
let _ = PhpException::default(format!("Failed to return closure result to PHP: {}", e))
.throw();
}
}
}
unsafe impl<R> PhpClosure for Box<dyn FnMut() -> R>
where
R: IntoZval,
{
fn invoke(&mut self, _: ArgParser, ret: &mut Zval) {
if let Err(e) = self().set_zval(ret, false) {
let _ = PhpException::default(format!("Failed to return closure result to PHP: {}", e))
.throw();
}
}
}
impl<R> PhpOnceClosure for Box<dyn FnOnce() -> R>
where
R: IntoZval + 'static,
{
fn into_closure(self) -> Closure {
let mut this = Some(self);
Closure::wrap(Box::new(move || {
let this = match this.take() {
Some(this) => this,
None => {
let _ = PhpException::default(
"Attempted to call `FnOnce` closure more than once.".into(),
)
.throw();
return Option::<R>::None;
}
};
Some(this())
}) as Box<dyn FnMut() -> Option<R>>)
}
}
macro_rules! php_closure_impl {
($($gen: ident),*) => {
php_closure_impl!(Fn; $($gen),*);
php_closure_impl!(FnMut; $($gen),*);
impl<$($gen),*, Ret> PhpOnceClosure for Box<dyn FnOnce($($gen),*) -> Ret>
where
$(for<'a> $gen: FromZval<'a> + 'static,)*
Ret: IntoZval + 'static,
{
fn into_closure(self) -> Closure {
let mut this = Some(self);
Closure::wrap(Box::new(move |$($gen),*| {
let this = match this.take() {
Some(this) => this,
None => {
let _ = PhpException::default(
"Attempted to call `FnOnce` closure more than once.".into(),
)
.throw();
return Option::<Ret>::None;
}
};
Some(this($($gen),*))
}) as Box<dyn FnMut($($gen),*) -> Option<Ret>>)
}
}
};
($fnty: ident; $($gen: ident),*) => {
unsafe impl<$($gen),*, Ret> PhpClosure for Box<dyn $fnty($($gen),*) -> Ret>
where
$(for<'a> $gen: FromZval<'a>,)*
Ret: IntoZval
{
fn invoke(&mut self, parser: ArgParser, ret: &mut Zval) {
$(
let mut $gen = Arg::new(stringify!($gen), $gen::TYPE);
)*
let parser = parser
$(.arg(&mut $gen))*
.parse();
if parser.is_err() {
return;
}
let result = self(
$(
match $gen.consume() {
Ok(val) => val,
_ => {
let _ = PhpException::default(concat!("Invalid parameter type for `", stringify!($gen), "`.").into()).throw();
return;
}
}
),*
);
if let Err(e) = result.set_zval(ret, false) {
let _ = PhpException::default(format!("Failed to return closure result to PHP: {}", e)).throw();
}
}
}
};
}
php_closure_impl!(A);
php_closure_impl!(A, B);
php_closure_impl!(A, B, C);
php_closure_impl!(A, B, C, D);
php_closure_impl!(A, B, C, D, E);
php_closure_impl!(A, B, C, D, E, F);
php_closure_impl!(A, B, C, D, E, F, G);
php_closure_impl!(A, B, C, D, E, F, G, H);