ext_php_rs/types/callable.rs
1//! Types related to callables in PHP (anonymous functions, functions, etc).
2
3use std::{convert::TryFrom, ops::Deref, ptr};
4
5use crate::{
6 convert::{FromZval, IntoZvalDyn},
7 error::{Error, Result},
8 ffi::_call_user_function_impl,
9 flags::DataType,
10 zend::ExecutorGlobals,
11};
12
13use super::{ZendHashTable, Zval};
14
15/// Acts as a wrapper around a callable [`Zval`]. Allows the owner to call the
16/// [`Zval`] as if it was a PHP function through the [`try_call`] method.
17///
18/// [`try_call`]: #method.try_call
19#[derive(Debug)]
20pub struct ZendCallable<'a>(OwnedZval<'a>);
21
22impl<'a> ZendCallable<'a> {
23 /// Attempts to create a new [`ZendCallable`] from a zval.
24 ///
25 /// # Parameters
26 ///
27 /// * `callable` - The underlying [`Zval`] that is callable.
28 ///
29 /// # Errors
30 ///
31 /// Returns an error if the [`Zval`] was not callable.
32 pub fn new(callable: &'a Zval) -> Result<Self> {
33 if callable.is_callable() {
34 Ok(Self(OwnedZval::Reference(callable)))
35 } else {
36 Err(Error::Callable)
37 }
38 }
39
40 /// Attempts to create a new [`ZendCallable`] by taking ownership of a Zval.
41 /// Returns a result containing the callable if the zval was callable.
42 ///
43 /// # Parameters
44 ///
45 /// * `callable` - The underlying [`Zval`] that is callable.
46 ///
47 /// # Errors
48 ///
49 /// * [`Error::Callable`] - If the zval was not callable.
50 pub fn new_owned(callable: Zval) -> Result<Self> {
51 if callable.is_callable() {
52 Ok(Self(OwnedZval::Owned(callable)))
53 } else {
54 Err(Error::Callable)
55 }
56 }
57
58 /// Attempts to create a new [`ZendCallable`] from a function name. Returns
59 /// a result containing the callable if the function existed and was
60 /// callable.
61 ///
62 /// # Parameters
63 ///
64 /// * `name` - Name of the callable function.
65 ///
66 /// # Errors
67 ///
68 /// Returns an error if the function does not exist or is not callable.
69 ///
70 /// # Example
71 ///
72 /// ```no_run
73 /// use ext_php_rs::types::ZendCallable;
74 ///
75 /// let strpos = ZendCallable::try_from_name("strpos").unwrap();
76 /// let result = strpos.try_call(vec![&"hello", &"e"]).unwrap();
77 /// assert_eq!(result.long(), Some(1));
78 /// ```
79 pub fn try_from_name(name: &str) -> Result<Self> {
80 let mut callable = Zval::new();
81 callable.set_string(name, false)?;
82
83 Self::new_owned(callable)
84 }
85
86 /// Attempts to call the callable with a list of arguments to pass to the
87 /// function.
88 ///
89 /// You should not call this function directly, rather through the
90 /// [`call_user_func`] macro.
91 ///
92 /// # Parameters
93 ///
94 /// * `params` - A list of parameters to call the function with.
95 ///
96 /// # Returns
97 ///
98 /// Returns the result wrapped in [`Ok`] upon success.
99 ///
100 /// # Errors
101 ///
102 /// * If calling the callable fails, or an exception is thrown, an [`Err`]
103 /// is returned.
104 /// * If the number of parameters exceeds `u32::MAX`.
105 ///
106 /// # Example
107 ///
108 /// ```no_run
109 /// use ext_php_rs::types::ZendCallable;
110 ///
111 /// let strpos = ZendCallable::try_from_name("strpos").unwrap();
112 /// let result = strpos.try_call(vec![&"hello", &"e"]).unwrap();
113 /// assert_eq!(result.long(), Some(1));
114 /// ```
115 // TODO: Measure this
116 #[allow(clippy::inline_always)]
117 #[inline(always)]
118 pub fn try_call(&self, params: Vec<&dyn IntoZvalDyn>) -> Result<Zval> {
119 if !self.0.is_callable() {
120 return Err(Error::Callable);
121 }
122
123 let mut retval = Zval::new();
124 let len = params.len();
125 let params = params
126 .into_iter()
127 .map(|val| val.as_zval(false))
128 .collect::<Result<Vec<_>>>()?;
129 let packed = params.into_boxed_slice();
130
131 let result = unsafe {
132 #[allow(clippy::used_underscore_items)]
133 _call_user_function_impl(
134 ptr::null_mut(),
135 ptr::from_ref(self.0.as_ref()).cast_mut(),
136 &raw mut retval,
137 len.try_into()?,
138 packed.as_ptr().cast_mut(),
139 ptr::null_mut(),
140 )
141 };
142
143 if result < 0 {
144 Err(Error::Callable)
145 } else if let Some(e) = ExecutorGlobals::take_exception() {
146 Err(Error::Exception(e))
147 } else {
148 Ok(retval)
149 }
150 }
151
152 /// Attempts to call the callable with both positional and named arguments.
153 ///
154 /// This method supports PHP 8.0+ named arguments, allowing you to pass
155 /// arguments by name rather than position. Named arguments are passed
156 /// after positional arguments.
157 ///
158 /// # Parameters
159 ///
160 /// * `params` - A list of positional parameters to call the function with.
161 /// * `named_params` - A list of named parameters as (name, value) tuples.
162 ///
163 /// # Returns
164 ///
165 /// Returns the result wrapped in [`Ok`] upon success.
166 ///
167 /// # Errors
168 ///
169 /// * If calling the callable fails, or an exception is thrown, an [`Err`]
170 /// is returned.
171 /// * If the number of parameters exceeds `u32::MAX`.
172 /// * If a parameter name contains a NUL byte.
173 ///
174 /// # Example
175 ///
176 /// ```no_run
177 /// use ext_php_rs::types::ZendCallable;
178 ///
179 /// // Call str_replace with named arguments
180 /// let str_replace = ZendCallable::try_from_name("str_replace").unwrap();
181 /// let result = str_replace.try_call_with_named(
182 /// &[], // no positional args
183 /// &[("search", &"world"), ("replace", &"PHP"), ("subject", &"Hello world")],
184 /// ).unwrap();
185 /// assert_eq!(result.string(), Some("Hello PHP".into()));
186 /// ```
187 // TODO: Measure this
188 #[allow(clippy::inline_always)]
189 #[inline(always)]
190 pub fn try_call_with_named(
191 &self,
192 params: &[&dyn IntoZvalDyn],
193 named_params: &[(&str, &dyn IntoZvalDyn)],
194 ) -> Result<Zval> {
195 if !self.0.is_callable() {
196 return Err(Error::Callable);
197 }
198
199 let mut retval = Zval::new();
200 let len = params.len();
201 let params = params
202 .iter()
203 .map(|val| val.as_zval(false))
204 .collect::<Result<Vec<_>>>()?;
205 let packed = params.into_boxed_slice();
206
207 // Build the named parameters hash table
208 let named_ht = if named_params.is_empty() {
209 None
210 } else {
211 let mut ht = ZendHashTable::with_capacity(named_params.len().try_into()?);
212 for &(name, val) in named_params {
213 let zval = val.as_zval(false)?;
214 ht.insert(name, zval)?;
215 }
216 Some(ht)
217 };
218
219 let named_ptr = named_ht
220 .as_ref()
221 .map_or(ptr::null_mut(), |ht| ptr::from_ref(&**ht).cast_mut());
222
223 let result = unsafe {
224 #[allow(clippy::used_underscore_items)]
225 _call_user_function_impl(
226 ptr::null_mut(),
227 ptr::from_ref(self.0.as_ref()).cast_mut(),
228 &raw mut retval,
229 len.try_into()?,
230 packed.as_ptr().cast_mut(),
231 named_ptr,
232 )
233 };
234
235 if result < 0 {
236 Err(Error::Callable)
237 } else if let Some(e) = ExecutorGlobals::take_exception() {
238 Err(Error::Exception(e))
239 } else {
240 Ok(retval)
241 }
242 }
243
244 /// Attempts to call the callable with only named arguments.
245 ///
246 /// This is a convenience method equivalent to calling
247 /// [`try_call_with_named`] with an empty positional arguments vector.
248 ///
249 /// # Parameters
250 ///
251 /// * `named_params` - A list of named parameters as (name, value) tuples.
252 ///
253 /// # Returns
254 ///
255 /// Returns the result wrapped in [`Ok`] upon success.
256 ///
257 /// # Errors
258 ///
259 /// * If calling the callable fails, or an exception is thrown, an [`Err`]
260 /// is returned.
261 /// * If a parameter name contains a NUL byte.
262 ///
263 /// # Example
264 ///
265 /// ```no_run
266 /// use ext_php_rs::types::ZendCallable;
267 ///
268 /// // Call array_fill with named arguments only
269 /// let array_fill = ZendCallable::try_from_name("array_fill").unwrap();
270 /// let result = array_fill.try_call_named(&[
271 /// ("start_index", &0i64),
272 /// ("count", &3i64),
273 /// ("value", &"PHP"),
274 /// ]).unwrap();
275 /// ```
276 ///
277 /// [`try_call_with_named`]: #method.try_call_with_named
278 #[inline]
279 pub fn try_call_named(&self, named_params: &[(&str, &dyn IntoZvalDyn)]) -> Result<Zval> {
280 self.try_call_with_named(&[], named_params)
281 }
282}
283
284impl<'a> FromZval<'a> for ZendCallable<'a> {
285 const TYPE: DataType = DataType::Callable;
286
287 fn from_zval(zval: &'a Zval) -> Option<Self> {
288 ZendCallable::new(zval).ok()
289 }
290}
291
292impl TryFrom<Zval> for ZendCallable<'_> {
293 type Error = Error;
294
295 fn try_from(value: Zval) -> Result<Self> {
296 ZendCallable::new_owned(value)
297 }
298}
299
300/// A container for a zval. Either contains a reference to a zval or an owned
301/// zval.
302#[derive(Debug)]
303enum OwnedZval<'a> {
304 Reference(&'a Zval),
305 Owned(Zval),
306}
307
308impl OwnedZval<'_> {
309 fn as_ref(&self) -> &Zval {
310 match self {
311 OwnedZval::Reference(zv) => zv,
312 OwnedZval::Owned(zv) => zv,
313 }
314 }
315}
316
317impl Deref for OwnedZval<'_> {
318 type Target = Zval;
319
320 fn deref(&self) -> &Self::Target {
321 self.as_ref()
322 }
323}