Skip to content

Commit 61c60c6

Browse files
committed
feat(functions): CachedCallable and raw functions #310
1 parent 8f71582 commit 61c60c6

15 files changed

Lines changed: 1988 additions & 41 deletions

File tree

Cargo.toml

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

2929
[dev-dependencies]
3030
skeptic = "0.13"
31+
criterion = { version = "0.8", features = ["html_reports"] }
32+
33+
[[bench]]
34+
name = "function_call"
35+
harness = false
36+
required-features = ["embed"]
3137

3238
[build-dependencies]
3339
anyhow = "1"
@@ -88,3 +94,13 @@ path = "tests/module.rs"
8894
[[test]]
8995
name = "sapi_tests"
9096
path = "tests/sapi.rs"
97+
98+
[[test]]
99+
name = "raw_functions_tests"
100+
path = "tests/raw_functions.rs"
101+
required-features = ["embed"]
102+
103+
[[test]]
104+
name = "cached_callable_tests"
105+
path = "tests/cached_callable.rs"
106+
required-features = ["embed"]

allowed_bindings.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ bind! {
6363
zend_array_destroy,
6464
zend_array_dup,
6565
zend_call_known_function,
66+
zend_call_function,
6667
zend_fetch_function_str,
6768
zend_hash_str_find_ptr_lc,
6869
zend_ce_argument_count_error,
@@ -108,6 +109,7 @@ bind! {
108109
zend_internal_arg_info,
109110
zend_is_callable,
110111
zend_is_callable_ex,
112+
zend_fcall_info,
111113
zend_fcall_info_cache,
112114
_zend_fcall_info_cache,
113115
zend_is_identical,

benches/function_call.rs

Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
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::AssertUnwindSafe;
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>(mut f: F) -> R {
58+
ensure_php_initialized();
59+
60+
unsafe {
61+
php_request_startup();
62+
}
63+
64+
let result = try_catch_first(AssertUnwindSafe(&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+
let n = unsafe { ex.get_arg(0) }
140+
.and_then(|zv| zv.long())
141+
.unwrap_or(0);
142+
retval.set_long(n + 1);
143+
}
144+
145+
/// Raw function that avoids all allocation - demonstrates zero-copy access
146+
#[php_function]
147+
#[php(raw)]
148+
pub fn bench_raw_two_ints(ex: &mut ExecuteData, retval: &mut Zval) {
149+
unsafe {
150+
let a = ex.get_arg(0).and_then(|zv| zv.long()).unwrap_or(0);
151+
let b = ex.get_arg(1).and_then(|zv| zv.long()).unwrap_or(0);
152+
retval.set_long(a + b);
153+
}
154+
}
155+
156+
// ============================================================================
157+
// Module registration
158+
// ============================================================================
159+
160+
#[php_module]
161+
pub fn build_module(module: ModuleBuilder) -> ModuleBuilder {
162+
module
163+
// Standard functions with type conversion
164+
.function(wrap_function!(bench_noop))
165+
.function(wrap_function!(bench_single_int))
166+
.function(wrap_function!(bench_two_ints))
167+
.function(wrap_function!(bench_string))
168+
.function(wrap_function!(bench_vec))
169+
.function(wrap_function!(bench_hashmap))
170+
.function(wrap_function!(bench_mixed))
171+
// Raw functions - zero overhead
172+
.function(wrap_function!(bench_raw_noop))
173+
.function(wrap_function!(bench_raw_single_int))
174+
.function(wrap_function!(bench_raw_two_ints))
175+
}
176+
177+
// ============================================================================
178+
// Benchmarks
179+
// ============================================================================
180+
181+
fn bench_function_call_overhead(c: &mut Criterion) {
182+
let mut group = c.benchmark_group("function_call_overhead");
183+
184+
// ---- Standard functions (with type conversion) ----
185+
186+
// Benchmark: noop function (baseline)
187+
group.bench_function("noop_standard", |b| {
188+
b.iter(|| {
189+
with_php_request(|| {
190+
let result = Embed::eval("bench_noop();").unwrap();
191+
black_box(result.long().unwrap())
192+
})
193+
})
194+
});
195+
196+
// Benchmark: single int argument
197+
group.bench_function("single_int_standard", |b| {
198+
b.iter(|| {
199+
with_php_request(|| {
200+
let result = Embed::eval("bench_single_int(42);").unwrap();
201+
black_box(result.long().unwrap())
202+
})
203+
})
204+
});
205+
206+
// Benchmark: two int arguments
207+
group.bench_function("two_ints_standard", |b| {
208+
b.iter(|| {
209+
with_php_request(|| {
210+
let result = Embed::eval("bench_two_ints(21, 21);").unwrap();
211+
black_box(result.long().unwrap())
212+
})
213+
})
214+
});
215+
216+
// ---- Raw functions (zero overhead) ----
217+
218+
// Benchmark: raw noop function
219+
group.bench_function("noop_raw", |b| {
220+
b.iter(|| {
221+
with_php_request(|| {
222+
let result = Embed::eval("bench_raw_noop();").unwrap();
223+
black_box(result.long().unwrap())
224+
})
225+
})
226+
});
227+
228+
// Benchmark: raw single int argument
229+
group.bench_function("single_int_raw", |b| {
230+
b.iter(|| {
231+
with_php_request(|| {
232+
let result = Embed::eval("bench_raw_single_int(42);").unwrap();
233+
black_box(result.long().unwrap())
234+
})
235+
})
236+
});
237+
238+
// Benchmark: raw two int arguments
239+
group.bench_function("two_ints_raw", |b| {
240+
b.iter(|| {
241+
with_php_request(|| {
242+
let result = Embed::eval("bench_raw_two_ints(21, 21);").unwrap();
243+
black_box(result.long().unwrap())
244+
})
245+
})
246+
});
247+
248+
group.finish();
249+
}
250+
251+
fn bench_type_conversion_overhead(c: &mut Criterion) {
252+
let mut group = c.benchmark_group("type_conversion");
253+
254+
// String conversion
255+
group.bench_function("string_short", |b| {
256+
b.iter(|| {
257+
with_php_request(|| {
258+
let result = Embed::eval("bench_string('hello');").unwrap();
259+
black_box(result.long().unwrap())
260+
})
261+
})
262+
});
263+
264+
group.bench_function("string_long", |b| {
265+
b.iter(|| {
266+
with_php_request(|| {
267+
let result = Embed::eval("bench_string(str_repeat('x', 1000));").unwrap();
268+
black_box(result.long().unwrap())
269+
})
270+
})
271+
});
272+
273+
// Vec conversion with different sizes
274+
for size in [1, 10, 100, 1000].iter() {
275+
group.throughput(Throughput::Elements(*size as u64));
276+
group.bench_with_input(BenchmarkId::new("vec", size), size, |b, &size| {
277+
b.iter(|| {
278+
with_php_request(|| {
279+
let code = format!("bench_vec(range(1, {}));", size);
280+
let result = Embed::eval(&code).unwrap();
281+
black_box(result.long().unwrap())
282+
})
283+
})
284+
});
285+
}
286+
287+
// HashMap conversion with different sizes
288+
for size in [1, 10, 100].iter() {
289+
group.throughput(Throughput::Elements(*size as u64));
290+
group.bench_with_input(BenchmarkId::new("hashmap", size), size, |b, &size| {
291+
b.iter(|| {
292+
with_php_request(|| {
293+
let code = format!(
294+
"$arr = []; for ($i = 0; $i < {}; $i++) {{ $arr['key'.$i] = $i; }} bench_hashmap($arr);",
295+
size
296+
);
297+
let result = Embed::eval(&code).unwrap();
298+
black_box(result.long().unwrap_or(0))
299+
})
300+
})
301+
});
302+
}
303+
304+
group.finish();
305+
}
306+
307+
fn bench_mixed_arguments(c: &mut Criterion) {
308+
let mut group = c.benchmark_group("mixed_arguments");
309+
310+
group.bench_function("mixed_3args", |b| {
311+
b.iter(|| {
312+
with_php_request(|| {
313+
let result = Embed::eval("bench_mixed(10, 'hello', 20);").unwrap();
314+
black_box(result.long().unwrap())
315+
})
316+
})
317+
});
318+
319+
group.finish();
320+
}
321+
322+
criterion_group!(
323+
benches,
324+
bench_function_call_overhead,
325+
bench_type_conversion_overhead,
326+
bench_mixed_arguments,
327+
);
328+
criterion_main!(benches);

0 commit comments

Comments
 (0)