Skip to content

Commit 3635d35

Browse files
committed
feat(callable): implement TryFrom<&str> for CachedCallable
1 parent 38c763f commit 3635d35

File tree

22 files changed

+1974
-38
lines changed

22 files changed

+1974
-38
lines changed

Cargo.toml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ ext-php-rs-derive = { version = "=0.11.5", path = "./crates/macros" }
2626

2727
[dev-dependencies]
2828
skeptic = "0.13"
29+
criterion = { version = "0.8", features = ["html_reports"] }
30+
31+
[[bench]]
32+
name = "function_call"
33+
harness = false
34+
required-features = ["embed"]
2935

3036
[build-dependencies]
3137
anyhow = "1"
@@ -79,10 +85,21 @@ path = "tests/module.rs"
7985
name = "sapi_tests"
8086
path = "tests/sapi.rs"
8187

88+
[[test]]
89+
name = "raw_functions_tests"
90+
path = "tests/raw_functions.rs"
91+
required-features = ["embed"]
92+
93+
[[test]]
94+
name = "cached_callable_tests"
95+
path = "tests/cached_callable.rs"
96+
required-features = ["embed"]
97+
8298
# Patch clang-sys and bindgen for preserve_none calling convention support (libclang 19/20)
8399
# Required for PHP 8.5+ on macOS ARM64 which uses TAILCALL VM mode
84100
# - clang-sys: Adds libclang 19/20 bindings (https://github.com/KyleMayes/clang-sys/pull/195)
85101
# - bindgen: Maps CXCallingConv_PreserveNone to C ABI
86102
[patch.crates-io]
87103
clang-sys = { git = "https://github.com/extphprs/clang-sys.git", branch = "preserve-none-support" }
88104
bindgen = { git = "https://github.com/extphprs/rust-bindgen.git", branch = "preserve-none-support" }
105+

allowed_bindings.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@ bind! {
6363
zend_array_destroy,
6464
zend_array_dup,
6565
zend_call_known_function,
66+
zend_call_function,
67+
zend_fcall_info,
68+
zend_fcall_info_cache,
69+
_zend_fcall_info_cache,
70+
zend_is_callable_ex,
6671
zend_fetch_function_str,
6772
zend_hash_str_find_ptr_lc,
6873
zend_ce_argument_count_error,

benches/function_call.rs

Lines changed: 329 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
1+
//! Benchmarks for PHP function call overhead in ext-php-rs.
2+
//!
3+
//! This benchmark suite measures the performance overhead of calling PHP
4+
//! functions from Rust using various approaches:
5+
//!
6+
//! - Standard `#[php_function]` with type conversion
7+
//! - Raw function access (direct `zend_execute_data` access)
8+
//! - Different argument types (primitives, strings, arrays)
9+
10+
#![cfg_attr(windows, feature(abi_vectorcall))]
11+
#![allow(
12+
missing_docs,
13+
deprecated,
14+
clippy::uninlined_format_args,
15+
clippy::cast_sign_loss,
16+
clippy::cast_possible_wrap,
17+
clippy::semicolon_if_nothing_returned,
18+
clippy::explicit_iter_loop,
19+
clippy::must_use_candidate,
20+
clippy::needless_pass_by_value,
21+
clippy::implicit_hasher,
22+
clippy::doc_markdown
23+
)]
24+
25+
use criterion::{BenchmarkId, Criterion, Throughput, black_box, criterion_group, criterion_main};
26+
use ext_php_rs::builders::SapiBuilder;
27+
use ext_php_rs::embed::{Embed, ext_php_rs_sapi_startup};
28+
use ext_php_rs::ffi::{
29+
php_module_startup, php_request_shutdown, php_request_startup, sapi_startup,
30+
};
31+
use ext_php_rs::prelude::*;
32+
use ext_php_rs::zend::try_catch_first;
33+
use std::collections::HashMap;
34+
use std::panic::RefUnwindSafe;
35+
use std::sync::Once;
36+
37+
static INIT: Once = Once::new();
38+
static mut INITIALIZED: bool = false;
39+
40+
/// Initialize PHP SAPI for benchmarks
41+
fn ensure_php_initialized() {
42+
INIT.call_once(|| {
43+
let builder = SapiBuilder::new("bench", "Benchmark");
44+
let sapi = builder.build().unwrap().into_raw();
45+
let module = get_module();
46+
47+
unsafe {
48+
ext_php_rs_sapi_startup();
49+
sapi_startup(sapi);
50+
php_module_startup(sapi, module);
51+
INITIALIZED = true;
52+
}
53+
});
54+
}
55+
56+
/// Start a PHP request context for benchmarks
57+
fn with_php_request<R: Default, F: FnMut() -> R + RefUnwindSafe>(mut f: F) -> R {
58+
ensure_php_initialized();
59+
60+
unsafe {
61+
php_request_startup();
62+
}
63+
64+
let result = try_catch_first(&mut f).unwrap_or_default();
65+
66+
unsafe {
67+
php_request_shutdown(std::ptr::null_mut());
68+
}
69+
70+
result
71+
}
72+
73+
// ============================================================================
74+
// Standard #[php_function] implementations
75+
// ============================================================================
76+
77+
/// Simple function that returns a constant - baseline for function call
78+
/// overhead
79+
#[php_function]
80+
pub fn bench_noop() -> i64 {
81+
42
82+
}
83+
84+
/// Function taking a single i64 argument
85+
#[php_function]
86+
pub fn bench_single_int(n: i64) -> i64 {
87+
n + 1
88+
}
89+
90+
/// Function taking two i64 arguments
91+
#[php_function]
92+
pub fn bench_two_ints(a: i64, b: i64) -> i64 {
93+
a + b
94+
}
95+
96+
/// Function taking a String argument
97+
#[php_function]
98+
pub fn bench_string(s: String) -> i64 {
99+
s.len() as i64
100+
}
101+
102+
/// Function taking a Vec argument
103+
#[php_function]
104+
pub fn bench_vec(v: Vec<i64>) -> i64 {
105+
v.iter().sum()
106+
}
107+
108+
/// Function taking a HashMap argument
109+
#[php_function]
110+
pub fn bench_hashmap(m: HashMap<String, i64>) -> i64 {
111+
m.values().sum()
112+
}
113+
114+
/// Function taking multiple mixed arguments
115+
#[php_function]
116+
pub fn bench_mixed(a: i64, s: String, b: i64) -> i64 {
117+
a + b + s.len() as i64
118+
}
119+
120+
// ============================================================================
121+
// Raw function implementations using #[php(raw)] - zero overhead
122+
// ============================================================================
123+
124+
use ext_php_rs::types::Zval;
125+
use ext_php_rs::zend::ExecuteData;
126+
127+
/// Raw function - direct access to ExecuteData and Zval
128+
/// This bypasses all argument parsing and type conversion
129+
#[php_function]
130+
#[php(raw)]
131+
pub fn bench_raw_noop(_ex: &mut ExecuteData, retval: &mut Zval) {
132+
retval.set_long(42);
133+
}
134+
135+
/// Raw function taking a single int - manual argument extraction
136+
#[php_function]
137+
#[php(raw)]
138+
pub fn bench_raw_single_int(ex: &mut ExecuteData, retval: &mut Zval) {
139+
// Get the first argument using ExecuteData's new get_arg method
140+
let n = unsafe { ex.get_arg(0) }
141+
.and_then(|zv| zv.long())
142+
.unwrap_or(0);
143+
retval.set_long(n + 1);
144+
}
145+
146+
/// Raw function that avoids all allocation - demonstrates zero-copy access
147+
#[php_function]
148+
#[php(raw)]
149+
pub fn bench_raw_two_ints(ex: &mut ExecuteData, retval: &mut Zval) {
150+
unsafe {
151+
let a = ex.get_arg(0).and_then(|zv| zv.long()).unwrap_or(0);
152+
let b = ex.get_arg(1).and_then(|zv| zv.long()).unwrap_or(0);
153+
retval.set_long(a + b);
154+
}
155+
}
156+
157+
// ============================================================================
158+
// Module registration
159+
// ============================================================================
160+
161+
#[php_module]
162+
pub fn build_module(module: ModuleBuilder) -> ModuleBuilder {
163+
module
164+
// Standard functions with type conversion
165+
.function(wrap_function!(bench_noop))
166+
.function(wrap_function!(bench_single_int))
167+
.function(wrap_function!(bench_two_ints))
168+
.function(wrap_function!(bench_string))
169+
.function(wrap_function!(bench_vec))
170+
.function(wrap_function!(bench_hashmap))
171+
.function(wrap_function!(bench_mixed))
172+
// Raw functions - zero overhead
173+
.function(wrap_function!(bench_raw_noop))
174+
.function(wrap_function!(bench_raw_single_int))
175+
.function(wrap_function!(bench_raw_two_ints))
176+
}
177+
178+
// ============================================================================
179+
// Benchmarks
180+
// ============================================================================
181+
182+
fn bench_function_call_overhead(c: &mut Criterion) {
183+
let mut group = c.benchmark_group("function_call_overhead");
184+
185+
// ---- Standard functions (with type conversion) ----
186+
187+
// Benchmark: noop function (baseline)
188+
group.bench_function("noop_standard", |b| {
189+
b.iter(|| {
190+
with_php_request(|| {
191+
let result = Embed::eval("bench_noop();").unwrap();
192+
black_box(result.long().unwrap())
193+
})
194+
})
195+
});
196+
197+
// Benchmark: single int argument
198+
group.bench_function("single_int_standard", |b| {
199+
b.iter(|| {
200+
with_php_request(|| {
201+
let result = Embed::eval("bench_single_int(42);").unwrap();
202+
black_box(result.long().unwrap())
203+
})
204+
})
205+
});
206+
207+
// Benchmark: two int arguments
208+
group.bench_function("two_ints_standard", |b| {
209+
b.iter(|| {
210+
with_php_request(|| {
211+
let result = Embed::eval("bench_two_ints(21, 21);").unwrap();
212+
black_box(result.long().unwrap())
213+
})
214+
})
215+
});
216+
217+
// ---- Raw functions (zero overhead) ----
218+
219+
// Benchmark: raw noop function
220+
group.bench_function("noop_raw", |b| {
221+
b.iter(|| {
222+
with_php_request(|| {
223+
let result = Embed::eval("bench_raw_noop();").unwrap();
224+
black_box(result.long().unwrap())
225+
})
226+
})
227+
});
228+
229+
// Benchmark: raw single int argument
230+
group.bench_function("single_int_raw", |b| {
231+
b.iter(|| {
232+
with_php_request(|| {
233+
let result = Embed::eval("bench_raw_single_int(42);").unwrap();
234+
black_box(result.long().unwrap())
235+
})
236+
})
237+
});
238+
239+
// Benchmark: raw two int arguments
240+
group.bench_function("two_ints_raw", |b| {
241+
b.iter(|| {
242+
with_php_request(|| {
243+
let result = Embed::eval("bench_raw_two_ints(21, 21);").unwrap();
244+
black_box(result.long().unwrap())
245+
})
246+
})
247+
});
248+
249+
group.finish();
250+
}
251+
252+
fn bench_type_conversion_overhead(c: &mut Criterion) {
253+
let mut group = c.benchmark_group("type_conversion");
254+
255+
// String conversion
256+
group.bench_function("string_short", |b| {
257+
b.iter(|| {
258+
with_php_request(|| {
259+
let result = Embed::eval("bench_string('hello');").unwrap();
260+
black_box(result.long().unwrap())
261+
})
262+
})
263+
});
264+
265+
group.bench_function("string_long", |b| {
266+
b.iter(|| {
267+
with_php_request(|| {
268+
let result = Embed::eval("bench_string(str_repeat('x', 1000));").unwrap();
269+
black_box(result.long().unwrap())
270+
})
271+
})
272+
});
273+
274+
// Vec conversion with different sizes
275+
for size in [1, 10, 100, 1000].iter() {
276+
group.throughput(Throughput::Elements(*size as u64));
277+
group.bench_with_input(BenchmarkId::new("vec", size), size, |b, &size| {
278+
b.iter(|| {
279+
with_php_request(|| {
280+
let code = format!("bench_vec(range(1, {}));", size);
281+
let result = Embed::eval(&code).unwrap();
282+
black_box(result.long().unwrap())
283+
})
284+
})
285+
});
286+
}
287+
288+
// HashMap conversion with different sizes
289+
for size in [1, 10, 100].iter() {
290+
group.throughput(Throughput::Elements(*size as u64));
291+
group.bench_with_input(BenchmarkId::new("hashmap", size), size, |b, &size| {
292+
b.iter(|| {
293+
with_php_request(|| {
294+
let code = format!(
295+
"$arr = []; for ($i = 0; $i < {}; $i++) {{ $arr['key'.$i] = $i; }} bench_hashmap($arr);",
296+
size
297+
);
298+
let result = Embed::eval(&code).unwrap();
299+
black_box(result.long().unwrap_or(0))
300+
})
301+
})
302+
});
303+
}
304+
305+
group.finish();
306+
}
307+
308+
fn bench_mixed_arguments(c: &mut Criterion) {
309+
let mut group = c.benchmark_group("mixed_arguments");
310+
311+
group.bench_function("mixed_3args", |b| {
312+
b.iter(|| {
313+
with_php_request(|| {
314+
let result = Embed::eval("bench_mixed(10, 'hello', 20);").unwrap();
315+
black_box(result.long().unwrap())
316+
})
317+
})
318+
});
319+
320+
group.finish();
321+
}
322+
323+
criterion_group!(
324+
benches,
325+
bench_function_call_overhead,
326+
bench_type_conversion_overhead,
327+
bench_mixed_arguments,
328+
);
329+
criterion_main!(benches);

0 commit comments

Comments
 (0)