Skip to main content

folk_ext/
zts.rs

1//! ZTS (Thread-Safe) PHP FFI wrappers.
2//!
3//! Thin Rust wrappers around `folk_zts.c` which in turn wraps PHP TSRM macros.
4
5#![allow(unsafe_code)]
6
7use std::ffi::{CString, c_int};
8
9use ext_php_rs::types::Zval;
10
11unsafe extern "C" {
12    fn folk_zts_thread_init();
13    fn folk_zts_thread_shutdown();
14    fn folk_zts_request_startup() -> c_int;
15    fn folk_zts_request_shutdown();
16    fn folk_zts_execute_script(filename: *const std::ffi::c_char) -> c_int;
17    fn folk_zts_call_dispatch(
18        func_name: *const std::ffi::c_char,
19        method_zval: *mut Zval,
20        params_zval: *mut Zval,
21        retval: *mut Zval,
22    ) -> c_int;
23    fn folk_zts_is_enabled() -> c_int;
24}
25
26/// Returns true if PHP was compiled with ZTS (thread safety).
27pub fn is_zts() -> bool {
28    unsafe { folk_zts_is_enabled() != 0 }
29}
30
31/// RAII guard for a PHP ZTS thread context.
32///
33/// Creates TSRM resources on construction, frees them on drop.
34/// Must be created from a dedicated OS thread (not main PHP thread).
35pub struct ZtsThreadGuard {
36    _private: (),
37}
38
39impl ZtsThreadGuard {
40    /// Register the current thread with PHP TSRM.
41    pub fn new() -> Self {
42        unsafe {
43            folk_zts_thread_init();
44        }
45        Self { _private: () }
46    }
47}
48
49impl Default for ZtsThreadGuard {
50    fn default() -> Self {
51        Self::new()
52    }
53}
54
55impl Drop for ZtsThreadGuard {
56    fn drop(&mut self) {
57        unsafe {
58            folk_zts_thread_shutdown();
59        }
60    }
61}
62
63/// Start a PHP request on the current thread.
64///
65/// # Errors
66/// Returns error if `php_request_startup()` fails.
67pub fn request_startup() -> anyhow::Result<()> {
68    let ret = unsafe { folk_zts_request_startup() };
69    if ret == 0 {
70        Ok(())
71    } else {
72        anyhow::bail!("php_request_startup failed (code {ret})")
73    }
74}
75
76/// Shut down the PHP request on the current thread.
77pub fn request_shutdown() {
78    unsafe {
79        folk_zts_request_shutdown();
80    }
81}
82
83/// Execute a PHP script on the current thread.
84///
85/// # Errors
86/// Returns error if the script fails to execute.
87pub fn execute_script(filename: &str) -> anyhow::Result<()> {
88    let c_filename =
89        CString::new(filename).map_err(|_| anyhow::anyhow!("filename contains null byte"))?;
90    let ret = unsafe { folk_zts_execute_script(c_filename.as_ptr()) };
91    if ret != 0 {
92        Ok(())
93    } else {
94        anyhow::bail!("php_execute_script failed for {filename}")
95    }
96}
97
98/// Call a named PHP function with (method, params) arguments.
99///
100/// Used to call the dispatch function registered by the PHP worker script.
101/// Returns the PHP function's return value as a `serde_json::Value`.
102///
103/// # Errors
104/// Returns error if the function call fails.
105pub fn call_dispatch(
106    func_name: &str,
107    method: &str,
108    params: &serde_json::Value,
109) -> anyhow::Result<serde_json::Value> {
110    let c_func =
111        CString::new(func_name).map_err(|_| anyhow::anyhow!("func_name contains null byte"))?;
112
113    // Convert method and params to Zvals.
114    let mut method_zval = Zval::new();
115    method_zval
116        .set_string(method, false)
117        .map_err(|e| anyhow::anyhow!("set_string failed: {e}"))?;
118
119    let mut params_zval = crate::zval_convert::value_to_zval(params);
120    let mut retval = Zval::new();
121
122    let ret = unsafe {
123        folk_zts_call_dispatch(
124            c_func.as_ptr(),
125            &raw mut method_zval,
126            &raw mut params_zval,
127            &raw mut retval,
128        )
129    };
130
131    if ret != 0 {
132        anyhow::bail!("call_user_function({func_name}) failed (code {ret})");
133    }
134
135    Ok(crate::zval_convert::zval_to_value(&retval))
136}