Skip to content

Commit 25cfc18

Browse files
committed
feat(zend): add helper for try catch and bailout in PHP
1 parent 1b55652 commit 25cfc18

File tree

9 files changed

+186
-23
lines changed

9 files changed

+186
-23
lines changed

build.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,8 @@ fn main() -> Result<()> {
248248
for path in [
249249
manifest.join("src").join("wrapper.h"),
250250
manifest.join("src").join("wrapper.c"),
251+
manifest.join("src").join("embed").join("embed.h"),
252+
manifest.join("src").join("embed").join("embed.c"),
251253
manifest.join("allowed_bindings.rs"),
252254
manifest.join("windows_build.rs"),
253255
manifest.join("unix_build.rs"),

src/embed/embed.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// We actually use the PHP embed API to run PHP code in test
44
// At some point we might want to use our own SAPI to do that
55
void* ext_php_rs_embed_callback(int argc, char** argv, void* (*callback)(void *), void *ctx) {
6-
void *result;
6+
void *result = NULL;
77

88
PHP_EMBED_START_BLOCK(argc, argv)
99

src/embed/ffi.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ extern "C" {
1010
pub fn ext_php_rs_embed_callback(
1111
argc: c_int,
1212
argv: *mut *mut c_char,
13-
func: unsafe extern "C" fn(*const c_void) -> *mut c_void,
13+
func: unsafe extern "C" fn(*const c_void) -> *const c_void,
1414
ctx: *const c_void,
1515
) -> *mut c_void;
1616
}

src/embed/mod.rs

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ use crate::ffi::{
1313
zend_stream_init_filename, ZEND_RESULT_CODE_SUCCESS,
1414
};
1515
use crate::types::{ZendObject, Zval};
16-
use crate::zend::ExecutorGlobals;
16+
use crate::zend::{panic_wrapper, ExecutorGlobals};
1717
use parking_lot::{const_rwlock, RwLock};
1818
use std::ffi::{c_char, c_void, CString, NulError};
19-
use std::panic::{catch_unwind, resume_unwind, RefUnwindSafe};
19+
use std::panic::{resume_unwind, RefUnwindSafe};
2020
use std::path::Path;
2121
use std::ptr::null_mut;
2222

@@ -93,6 +93,12 @@ impl Embed {
9393
/// Which means subsequent calls to `Embed::eval` or `Embed::run_script` will be able to access
9494
/// variables defined in previous calls
9595
///
96+
/// # Returns
97+
///
98+
/// * R - The result of the function passed to this method
99+
///
100+
/// R must implement [`Default`] so it can be returned in case of a bailout
101+
///
96102
/// # Example
97103
///
98104
/// ```
@@ -105,41 +111,36 @@ impl Embed {
105111
/// assert_eq!(foo.unwrap().string().unwrap(), "foo");
106112
/// });
107113
/// ```
108-
pub fn run<F: Fn() + RefUnwindSafe>(func: F) {
114+
pub fn run<R, F: Fn() -> R + RefUnwindSafe>(func: F) -> R
115+
where
116+
R: Default,
117+
{
109118
// @TODO handle php thread safe
110119
//
111120
// This is to prevent multiple threads from running php at the same time
112121
// At some point we should detect if php is compiled with thread safety and avoid doing that in this case
113122
let _guard = RUN_FN_LOCK.write();
114123

115-
unsafe extern "C" fn wrapper<F: Fn() + RefUnwindSafe>(ctx: *const c_void) -> *mut c_void {
116-
// we try to catch panic here so we correctly shutdown php if it happens
117-
// mandatory when we do assert on test as other test would not run correctly
118-
let panic = catch_unwind(|| {
119-
(*(ctx as *const F))();
120-
});
121-
122-
let panic_ptr = Box::into_raw(Box::new(panic));
123-
124-
panic_ptr as *mut c_void
125-
}
126-
127124
let panic = unsafe {
128125
ext_php_rs_embed_callback(
129126
0,
130127
null_mut(),
131-
wrapper::<F>,
128+
panic_wrapper::<R, F>,
132129
&func as *const F as *const c_void,
133130
)
134131
};
135132

133+
// This can happen if there is a bailout
136134
if panic.is_null() {
137-
return;
135+
return R::default();
138136
}
139137

140-
if let Err(err) = unsafe { *Box::from_raw(panic as *mut std::thread::Result<()>) } {
141-
// we resume the panic here so it can be catched correctly by the test framework
142-
resume_unwind(err);
138+
match unsafe { *Box::from_raw(panic as *mut std::thread::Result<R>) } {
139+
Ok(r) => r,
140+
Err(err) => {
141+
// we resume the panic here so it can be catched correctly by the test framework
142+
resume_unwind(err);
143+
}
143144
}
144145
}
145146

@@ -244,4 +245,13 @@ mod tests {
244245
panic!("test panic");
245246
});
246247
}
248+
249+
#[test]
250+
fn test_return() {
251+
let foo = Embed::run(|| {
252+
return "foo";
253+
});
254+
255+
assert_eq!(foo, "foo");
256+
}
247257
}

src/ffi.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ extern "C" {
2626
pub fn ext_php_rs_zend_object_alloc(obj_size: usize, ce: *mut zend_class_entry) -> *mut c_void;
2727
pub fn ext_php_rs_zend_object_release(obj: *mut zend_object);
2828
pub fn ext_php_rs_executor_globals() -> *mut zend_executor_globals;
29+
pub fn ext_php_rs_zend_try_catch(
30+
func: unsafe extern "C" fn(*const c_void) -> *const c_void,
31+
ctx: *const c_void,
32+
result: *mut *mut c_void,
33+
) -> bool;
34+
pub fn ext_php_rs_zend_bailout();
2935
}
3036

3137
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));

src/wrapper.c

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,17 @@ zend_executor_globals *ext_php_rs_executor_globals() {
3939
return &executor_globals;
4040
#endif
4141
}
42+
43+
bool ext_php_rs_zend_try_catch(void* (*callback)(void *), void *ctx, void **result) {
44+
zend_try {
45+
*result = callback(ctx);
46+
} zend_catch {
47+
return true;
48+
} zend_end_try();
49+
50+
return false;
51+
}
52+
53+
void ext_php_rs_zend_bailout() {
54+
zend_bailout();
55+
}

src/wrapper.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,6 @@ void ext_php_rs_set_known_valid_utf8(zend_string *zs);
3030
const char *ext_php_rs_php_build_id();
3131
void *ext_php_rs_zend_object_alloc(size_t obj_size, zend_class_entry *ce);
3232
void ext_php_rs_zend_object_release(zend_object *obj);
33-
zend_executor_globals *ext_php_rs_executor_globals();
33+
zend_executor_globals *ext_php_rs_executor_globals();
34+
bool ext_php_rs_zend_try_catch(void* (*callback)(void *), void *ctx, void **result);
35+
void ext_php_rs_zend_bailout();

src/zend/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ mod globals;
99
mod handlers;
1010
mod ini_entry_def;
1111
mod module;
12+
mod try_catch;
1213

1314
use crate::{error::Result, ffi::php_printf};
1415
use std::ffi::CString;
@@ -22,6 +23,8 @@ pub use globals::ExecutorGlobals;
2223
pub use handlers::ZendObjectHandlers;
2324
pub use ini_entry_def::IniEntryDef;
2425
pub use module::ModuleEntry;
26+
pub(crate) use try_catch::panic_wrapper;
27+
pub use try_catch::{bailout, try_catch};
2528

2629
// Used as the format string for `php_printf`.
2730
const FORMAT_STR: &[u8] = b"%s\0";

src/zend/try_catch.rs

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
use crate::ffi::{ext_php_rs_zend_bailout, ext_php_rs_zend_try_catch};
2+
use std::ffi::c_void;
3+
use std::panic::{catch_unwind, resume_unwind, RefUnwindSafe};
4+
use std::ptr::null_mut;
5+
6+
#[derive(Debug)]
7+
pub struct CatchError;
8+
9+
pub(crate) unsafe extern "C" fn panic_wrapper<R, F: Fn() -> R + RefUnwindSafe>(
10+
ctx: *const c_void,
11+
) -> *const c_void {
12+
// we try to catch panic here so we correctly shutdown php if it happens
13+
// mandatory when we do assert on test as other test would not run correctly
14+
let panic = catch_unwind(|| (*(ctx as *const F))());
15+
16+
Box::into_raw(Box::new(panic)) as *mut c_void
17+
}
18+
19+
/// PHP propose a try catch mechanism in C using setjmp and longjmp (bailout)
20+
/// It store the arg of setjmp into the bailout field of the global executor
21+
/// If a bailout is triggered, the executor will jump to the setjmp and restore the previous setjmp
22+
///
23+
/// try_catch allow to use this mechanism
24+
///
25+
/// # Returns
26+
///
27+
/// * `Ok(R)` - The result of the function
28+
/// * `Err(CatchError)` - A bailout occurred during the execution
29+
pub fn try_catch<R, F: Fn() -> R + RefUnwindSafe>(func: F) -> Result<R, CatchError> {
30+
let mut panic_ptr = null_mut();
31+
let has_bailout = unsafe {
32+
ext_php_rs_zend_try_catch(
33+
panic_wrapper::<R, F>,
34+
&func as *const F as *const c_void,
35+
(&mut panic_ptr) as *mut *mut c_void,
36+
)
37+
};
38+
39+
let panic = panic_ptr as *mut std::thread::Result<R>;
40+
41+
// can be null if there is a bailout
42+
if panic.is_null() || has_bailout {
43+
return Err(CatchError);
44+
}
45+
46+
match unsafe { *Box::from_raw(panic as *mut std::thread::Result<R>) } {
47+
Ok(r) => Ok(r),
48+
Err(err) => {
49+
// we resume the panic here so it can be catched correctly by the test framework
50+
resume_unwind(err);
51+
}
52+
}
53+
}
54+
55+
/// Trigger a bailout
56+
///
57+
/// This function will stop the execution of the current script
58+
/// and jump to the last try catch block
59+
pub fn bailout() {
60+
unsafe {
61+
ext_php_rs_zend_bailout();
62+
}
63+
}
64+
65+
#[cfg(test)]
66+
mod tests {
67+
use crate::embed::Embed;
68+
use crate::zend::{bailout, try_catch};
69+
70+
#[test]
71+
fn test_catch() {
72+
Embed::run(|| {
73+
let catch = try_catch(|| {
74+
bailout();
75+
assert!(false);
76+
});
77+
78+
assert!(catch.is_err());
79+
});
80+
}
81+
82+
#[test]
83+
fn test_no_catch() {
84+
Embed::run(|| {
85+
let catch = try_catch(|| {
86+
assert!(true);
87+
});
88+
89+
assert!(catch.is_ok());
90+
});
91+
}
92+
93+
#[test]
94+
fn test_bailout() {
95+
Embed::run(|| {
96+
bailout();
97+
98+
assert!(false);
99+
});
100+
}
101+
102+
#[test]
103+
#[should_panic]
104+
fn test_panic() {
105+
Embed::run(|| {
106+
let _ = try_catch(|| {
107+
panic!("should panic");
108+
});
109+
});
110+
}
111+
112+
#[test]
113+
fn test_return() {
114+
let foo = Embed::run(|| {
115+
let result = try_catch(|| {
116+
return "foo";
117+
});
118+
119+
assert!(result.is_ok());
120+
121+
result.unwrap()
122+
});
123+
124+
assert_eq!(foo, "foo");
125+
}
126+
}

0 commit comments

Comments
 (0)