Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion benches/benches/binary_bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,31 @@ binary_benchmark_group!(
benchmarks = callback_calls
);

#[binary_benchmark]
#[bench::single_cached_callback_call(args = ("cached_callback_call.php", 1))]
#[bench::multiple_cached_callback_calls(args = ("cached_callback_call.php", 10))]
#[bench::lots_of_cached_callback_calls(args = ("cached_callback_call.php", 100_000))]
fn cached_callback_calls(script: &str, cnt: usize) -> gungraun::Command {
setup();

gungraun::Command::new("php")
.arg(format!("-dextension={}", *EXT_LIB))
.arg(bench_script(script))
.arg(cnt.to_string())
.build()
}

binary_benchmark_group!(
name = cached_callback;
config = BinaryBenchmarkConfig::default()
.tool(Callgrind::with_args([
CACHE_SIM[0], CACHE_SIM[1], CACHE_SIM[2],
"--collect-atstart=no",
"--toggle-collect=*_internal_bench_cached_callback_function*handler*",
]).flamegraph(FlamegraphConfig::default()));
benchmarks = cached_callback_calls
);

#[binary_benchmark]
#[bench::single_method_call(args = ("method_call.php", 1))]
#[bench::multiple_method_calls(args = ("method_call.php", 10))]
Expand Down Expand Up @@ -177,5 +202,5 @@ binary_benchmark_group!(
);

main!(
binary_benchmark_groups = function, callback, method, static_method
binary_benchmark_groups = function, callback, cached_callback, method, static_method
);
5 changes: 5 additions & 0 deletions benches/benches/cached_callback_call.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php

declare(strict_types=1);

bench_cached_callback_function(fn ($i) => $i * 2, (int) $argv[1]);
11 changes: 11 additions & 0 deletions benches/ext/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ pub fn bench_callback_function(callback: ZendCallable, n: usize) {
}
}

#[php_function]
pub fn bench_cached_callback_function(callback: ZendCallable, n: usize) {
let cached = callback.cache().expect("Failed to cache callback");
for i in 0..n {
cached
.try_call(vec![&i])
.expect("Failed to call cached callback");
}
}

#[php_class]
pub struct BenchClass;

Expand All @@ -45,5 +55,6 @@ pub fn build_module(module: ModuleBuilder) -> ModuleBuilder {
module
.function(wrap_function!(bench_function))
.function(wrap_function!(bench_callback_function))
.function(wrap_function!(bench_cached_callback_function))
.class::<BenchClass>()
}
85 changes: 85 additions & 0 deletions guide/src/types/functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,88 @@ pub fn test_method() -> () {

# fn main() {}
```

## Cached Callables

When calling the same PHP function repeatedly from Rust, use `CachedCallable`
to avoid re-resolving the function on every call. The first resolution caches
the internal `zend_fcall_info_cache`, and subsequent calls skip all string
lookups and hash table searches.

```rust,no_run
# #![cfg_attr(windows, feature(abi_vectorcall))]
# extern crate ext_php_rs;
use ext_php_rs::prelude::*;
use ext_php_rs::types::ZendCallable;

#[php_function]
pub fn call_many_times(callback: ZendCallable) -> () {
let cached = callback.cache().expect("Failed to cache callable");

for i in 0..1000i64 {
let _ = cached.try_call(vec![&i]);
}
}
# fn main() {}
```

### When to use `CachedCallable`

- **Use `CachedCallable`** when calling the same callable multiple times (loops,
event handlers, iterators like `array_map` patterns).
- **Use `ZendCallable`** for single-shot calls where caching overhead is wasted.

### Error handling

`CachedCallable` returns `CachedCallableError` which provides granular
error variants:

```rust,no_run
# #![cfg_attr(windows, feature(abi_vectorcall))]
# extern crate ext_php_rs;
use ext_php_rs::prelude::*;
use ext_php_rs::error::CachedCallableError;
use ext_php_rs::types::ZendCallable;

#[php_function]
pub fn resilient_caller(callback: ZendCallable) -> () {
let cached = callback.cache().expect("Failed to cache");

match cached.try_call(vec![&42i64]) {
Ok(result) => { /* use result */ },
Err(CachedCallableError::PhpException(_)) => {
// PHP exception — callable is still valid, can retry
let _ = cached.try_call(vec![&0i64]);
},
Err(CachedCallableError::Poisoned) => {
// Engine failure happened before — cannot reuse
},
Err(e) => { /* other errors */ },
}
}
# fn main() {}
```

### Named arguments

`CachedCallable` supports the same named argument methods as `ZendCallable`:

```rust,no_run
# #![cfg_attr(windows, feature(abi_vectorcall))]
# extern crate ext_php_rs;
use ext_php_rs::prelude::*;
use ext_php_rs::types::ZendCallable;

#[php_function]
pub fn cached_with_named() -> () {
let func = ZendCallable::try_from_name("str_replace").unwrap();
let cached = func.cache().unwrap();

let _ = cached.try_call_named(&[
("search", &"world"),
("replace", &"PHP"),
("subject", &"Hello world"),
]);
}
# fn main() {}
```
48 changes: 48 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,54 @@ impl From<Error> for PhpException {
}
}

/// Error type for [`CachedCallable`](crate::types::CachedCallable) operations.
#[derive(Debug)]
pub enum CachedCallableError {
/// The callable could not be resolved at cache time.
ResolutionFailed,
/// The call mechanism itself failed (`zend_call_function` returned < 0).
/// The `CachedCallable` is now poisoned and cannot be reused.
CallFailed,
/// A PHP exception was thrown during execution.
/// The `CachedCallable` remains valid for subsequent calls.
PhpException(ZBox<ZendObject>),
/// The `CachedCallable` was poisoned by a prior engine failure.
Poisoned,
/// Integer overflow when converting parameter count.
IntegerOverflow,
/// A parameter could not be converted to a Zval.
ParamConversion,
}

impl Display for CachedCallableError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ResolutionFailed => write!(f, "Could not resolve callable for caching."),
Self::CallFailed => {
write!(f, "Cached callable call failed; callable is now poisoned.")
}
Self::PhpException(e) => write!(f, "PHP exception thrown during cached call: {e:?}"),
Self::Poisoned => write!(f, "Cached callable is poisoned by a prior engine failure."),
Self::IntegerOverflow => {
write!(f, "Converting integer arguments resulted in an overflow.")
}
Self::ParamConversion => write!(f, "A parameter could not be converted to a Zval."),
}
}
}

impl ErrorTrait for CachedCallableError {}

impl From<CachedCallableError> for Error {
fn from(e: CachedCallableError) -> Self {
match e {
CachedCallableError::PhpException(e) => Error::Exception(e),
CachedCallableError::IntegerOverflow => Error::IntegerOverflow,
_ => Error::Callable,
}
}
}

/// Trigger an error that is reported in PHP the same way `trigger_error()` is.
///
/// See specific error type descriptions at <https://www.php.net/manual/en/errorfunc.constants.php>.
Expand Down
10 changes: 10 additions & 0 deletions src/ffi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@ unsafe extern "C" {
filename: *const c_char,
) -> *mut zend_op_array;
pub fn ext_php_rs_zend_execute(op_array: *mut zend_op_array);

pub fn _ext_php_rs_zend_fcc_addref(fcc: *mut _zend_fcall_info_cache);
pub fn _ext_php_rs_zend_fcc_dtor(fcc: *mut _zend_fcall_info_cache);
pub fn _ext_php_rs_cached_call_function(
fcc: *mut _zend_fcall_info_cache,
retval: *mut zval,
param_count: u32,
params: *mut zval,
named_params: *mut HashTable,
) -> ::std::os::raw::c_int;
}

include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ pub mod prelude {
pub use crate::php_print;
pub use crate::php_println;
pub use crate::php_write;
pub use crate::types::CachedCallable;
pub use crate::types::ZendCallable;
pub use crate::zend::BailoutGuard;
#[cfg(feature = "observer")]
Expand Down
Loading
Loading