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_eval_string(code: *const std::ffi::c_char) -> c_int;
24    fn folk_zts_is_enabled() -> c_int;
25}
26
27/// Returns true if PHP was compiled with ZTS (thread safety).
28pub fn is_zts() -> bool {
29    unsafe { folk_zts_is_enabled() != 0 }
30}
31
32/// RAII guard for a PHP ZTS thread context.
33///
34/// Creates TSRM resources on construction, frees them on drop.
35/// Must be created from a dedicated OS thread (not main PHP thread).
36pub struct ZtsThreadGuard {
37    _private: (),
38}
39
40impl ZtsThreadGuard {
41    /// Register the current thread with PHP TSRM.
42    pub fn new() -> Self {
43        unsafe {
44            folk_zts_thread_init();
45        }
46        Self { _private: () }
47    }
48}
49
50impl Default for ZtsThreadGuard {
51    fn default() -> Self {
52        Self::new()
53    }
54}
55
56impl Drop for ZtsThreadGuard {
57    fn drop(&mut self) {
58        unsafe {
59            folk_zts_thread_shutdown();
60        }
61    }
62}
63
64/// Start a PHP request on the current thread.
65///
66/// # Errors
67/// Returns error if `php_request_startup()` fails.
68pub fn request_startup() -> anyhow::Result<()> {
69    let ret = unsafe { folk_zts_request_startup() };
70    if ret == 0 {
71        Ok(())
72    } else {
73        anyhow::bail!("php_request_startup failed (code {ret})")
74    }
75}
76
77/// Shut down the PHP request on the current thread.
78pub fn request_shutdown() {
79    unsafe {
80        folk_zts_request_shutdown();
81    }
82}
83
84/// Evaluate a PHP code string on the current thread.
85///
86/// The code must NOT include the opening `<?php` tag.
87///
88/// # Errors
89/// Returns error if evaluation fails.
90pub fn eval_string(code: &str) -> anyhow::Result<()> {
91    let c_code = CString::new(code).map_err(|_| anyhow::anyhow!("code contains null byte"))?;
92    let ret = unsafe { folk_zts_eval_string(c_code.as_ptr()) };
93    if ret != 0 {
94        Ok(())
95    } else {
96        anyhow::bail!("zend_eval_string failed")
97    }
98}
99
100/// Execute a PHP script on the current thread.
101///
102/// # Errors
103/// Returns error if the script fails to execute.
104pub fn execute_script(filename: &str) -> anyhow::Result<()> {
105    let c_filename =
106        CString::new(filename).map_err(|_| anyhow::anyhow!("filename contains null byte"))?;
107    let ret = unsafe { folk_zts_execute_script(c_filename.as_ptr()) };
108    if ret != 0 {
109        Ok(())
110    } else {
111        anyhow::bail!("php_execute_script failed for {filename}")
112    }
113}
114
115/// Call a named PHP function with (method, params) arguments.
116///
117/// Used to call the dispatch function registered by the PHP worker script.
118/// Returns the PHP function's return value as a `serde_json::Value`.
119///
120/// # Errors
121/// Returns error if the function call fails.
122pub fn call_dispatch(
123    func_name: &str,
124    method: &str,
125    params: &serde_json::Value,
126) -> anyhow::Result<serde_json::Value> {
127    let c_func =
128        CString::new(func_name).map_err(|_| anyhow::anyhow!("func_name contains null byte"))?;
129
130    // Convert method and params to Zvals.
131    let mut method_zval = Zval::new();
132    method_zval
133        .set_string(method, false)
134        .map_err(|e| anyhow::anyhow!("set_string failed: {e}"))?;
135
136    let mut params_zval = crate::zval_convert::value_to_zval(params);
137    let mut retval = Zval::new();
138
139    let ret = unsafe {
140        folk_zts_call_dispatch(
141            c_func.as_ptr(),
142            &raw mut method_zval,
143            &raw mut params_zval,
144            &raw mut retval,
145        )
146    };
147
148    if ret != 0 {
149        anyhow::bail!("call_user_function({func_name}) failed (code {ret})");
150    }
151
152    Ok(crate::zval_convert::zval_to_value(&retval))
153}