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