Skip to main content

folk_runtime_embed/
php.rs

1//! Safe Rust wrappers around PHP embed SAPI FFI.
2//!
3//! These are low-level but safe wrappers. Higher-level runtime logic
4//! (worker threads, channels, etc.) lives in other modules.
5#![allow(unsafe_code)]
6
7use std::ffi::CString;
8use std::ptr;
9
10use anyhow::{Context, Result, bail};
11use tracing::{debug, warn};
12
13use crate::ffi;
14
15/// A PHP interpreter instance. One per thread.
16///
17/// # Safety
18///
19/// Must be created and used on a single OS thread. PHP globals are
20/// thread-local (with NTS builds) but NOT safe to share across threads.
21/// Each worker thread must own its own `PhpInstance`.
22pub struct PhpInstance {
23    /// Whether we're inside a request cycle.
24    in_request: bool,
25    /// Whether this instance uses the custom Folk SAPI (vs. embed SAPI).
26    custom_sapi: bool,
27    /// Whether this instance owns the module (should shutdown on drop).
28    owns_module: bool,
29    /// TSRM context for ZTS builds (NULL for NTS or module owner).
30    tsrm_ctx: *mut std::ffi::c_void,
31}
32
33// SAFETY: PhpInstance is only used on the thread that created it.
34// The raw pointer is a TSRM context owned by this thread.
35// Sync is needed because EmbedRuntime holds the module owner instance
36// while worker threads have their own attached instances.
37unsafe impl Send for PhpInstance {}
38unsafe impl Sync for PhpInstance {}
39
40impl PhpInstance {
41    /// Boot the PHP interpreter using the default embed SAPI.
42    ///
43    /// This is the original POC path — simple but no HTTP request support.
44    pub fn boot() -> Result<Self> {
45        debug!("booting PHP embed SAPI");
46
47        let result = unsafe { ffi::php_embed_init(0, ptr::null_mut()) };
48
49        if result != 0 {
50            bail!("php_embed_init() failed with code {result}");
51        }
52
53        // Override output handler for embed SAPI
54        unsafe { ffi::folk_install_output_handler() };
55
56        debug!("PHP embed SAPI initialized");
57
58        Ok(Self {
59            in_request: false,
60            custom_sapi: false,
61            owns_module: true,
62            tsrm_ctx: ptr::null_mut(),
63        })
64    }
65
66    /// Boot the PHP interpreter using the custom Folk SAPI.
67    ///
68    /// Calls `folk_sapi_init()` which does `php_module_startup()`.
69    /// Must be called exactly ONCE per process. Worker threads should
70    /// use `attach()` instead.
71    pub fn boot_custom_sapi() -> Result<Self> {
72        debug!("booting PHP with Folk custom SAPI");
73
74        // Save signal handlers before PHP init (PHP overwrites them)
75        unsafe { ffi::folk_signals_save() };
76
77        let result = unsafe { ffi::folk_sapi_init() };
78
79        // Restore signal handlers (tokio needs SIGTERM/SIGINT)
80        unsafe { ffi::folk_signals_restore() };
81
82        // Install our SIGSEGV handler for worker thread protection
83        unsafe { ffi::folk_sigsegv_handler_install() };
84
85        if result != 0 {
86            bail!("folk_sapi_init() failed with code {result}");
87        }
88
89        debug!("Folk custom SAPI initialized");
90
91        Ok(Self {
92            in_request: false,
93            custom_sapi: true,
94            owns_module: true,
95            tsrm_ctx: ptr::null_mut(),
96        })
97    }
98
99    /// Attach to an already-initialized PHP module (for worker threads).
100    ///
101    /// For ZTS builds: allocates a new TSRM interpreter context so this
102    /// thread gets its own copy of PHP globals. For NTS builds: no-op
103    /// (globals are shared, only works with single worker thread).
104    pub fn attach() -> Self {
105        debug!("attaching to existing PHP module");
106
107        let ctx = unsafe { ffi::folk_thread_init() };
108        if !ctx.is_null() {
109            unsafe { ffi::folk_thread_set_ctx(ctx) };
110            debug!("TSRM context allocated for worker thread");
111        }
112
113        Self {
114            in_request: false,
115            custom_sapi: true,
116            owns_module: false,
117            tsrm_ctx: ctx,
118        }
119    }
120
121    /// Set the request context before calling `request_startup`.
122    ///
123    /// The `RequestContext` must live until `request_shutdown` is called.
124    pub fn set_request_context(&self, ctx: &mut RequestContext) {
125        ctx.build_ffi();
126        unsafe { ffi::folk_request_context_set(&mut ctx.ffi) };
127    }
128
129    /// Clear the request context (called after `request_shutdown`).
130    pub fn clear_request_context(&self) {
131        unsafe { ffi::folk_request_context_clear() };
132    }
133
134    /// Start a new request cycle.
135    pub fn request_startup(&mut self) -> Result<()> {
136        if self.in_request {
137            warn!("request_startup called while already in request — shutting down first");
138            self.request_shutdown();
139        }
140
141        unsafe { ffi::folk_clear_output() };
142
143        if self.custom_sapi {
144            unsafe { ffi::folk_response_clear() };
145        }
146
147        let result = unsafe { ffi::folk_request_startup_safe() };
148        match result {
149            0 => {
150                self.in_request = true;
151                Ok(())
152            },
153            -1 => bail!("php_request_startup: fatal error (bailout)"),
154            -2 => bail!("php_request_startup: startup failed"),
155            code => bail!("php_request_startup: unknown error {code}"),
156        }
157    }
158
159    /// End the current request cycle.
160    pub fn request_shutdown(&mut self) {
161        if !self.in_request {
162            return;
163        }
164
165        let result = unsafe { ffi::folk_request_shutdown_safe() };
166        if result != 0 {
167            warn!("php_request_shutdown returned {result}");
168        }
169
170        if self.custom_sapi {
171            self.clear_request_context();
172        }
173
174        self.in_request = false;
175    }
176
177    /// Execute a PHP script file (proper file execution, not eval).
178    pub fn execute_script(&mut self, filename: &str) -> Result<String> {
179        let c_filename = CString::new(filename).context("filename contains null byte")?;
180
181        let result = unsafe { ffi::folk_execute_script_safe(c_filename.as_ptr()) };
182        let output = self.take_output();
183
184        match result {
185            0 => Ok(output),
186            -1 => {
187                if output.is_empty() {
188                    bail!("PHP script fatal error (bailout) in: {filename}")
189                } else {
190                    bail!("PHP script fatal error: {output}")
191                }
192            },
193            other => bail!("PHP script error {other} in: {filename}"),
194        }
195    }
196
197    /// Evaluate a PHP code string and return the output.
198    pub fn eval(&mut self, code: &str) -> Result<EvalResult> {
199        let c_code = CString::new(code).context("PHP code contains null byte")?;
200        let mut retval = ffi::zval::new_undef();
201
202        let result = unsafe { ffi::folk_eval_string_safe(c_code.as_ptr(), &mut retval) };
203
204        let output = self.take_output();
205
206        let return_value = if result == 0 {
207            ZvalValue::from_raw(&mut retval)
208        } else {
209            ZvalValue::Null
210        };
211
212        unsafe { ffi::folk_zval_dtor(&mut retval) };
213
214        match result {
215            0 => Ok(EvalResult {
216                output,
217                return_value,
218            }),
219            -1 => {
220                if output.is_empty() {
221                    bail!("PHP eval fatal error (bailout) in: {code}")
222                } else {
223                    bail!("PHP eval fatal error (bailout): {output}")
224                }
225            },
226            other => bail!("PHP eval error {other} in: {code}"),
227        }
228    }
229
230    /// Call a PHP function by name with the given string arguments.
231    pub fn call(&mut self, func_name: &str, args: &[&str]) -> Result<ZvalValue> {
232        let c_func = CString::new(func_name).context("function name contains null byte")?;
233
234        let c_args: Vec<CString> = args.iter().map(|a| CString::new(*a).unwrap()).collect();
235
236        let mut params: Vec<ffi::zval> = c_args
237            .iter()
238            .map(|s| {
239                let mut z = ffi::zval::new_undef();
240                unsafe {
241                    ffi::folk_zval_set_string(&mut z, s.as_ptr(), s.as_bytes().len());
242                }
243                z
244            })
245            .collect();
246
247        let mut retval = ffi::zval::new_undef();
248
249        let result = unsafe {
250            ffi::folk_call_function_safe(
251                c_func.as_ptr(),
252                &mut retval,
253                u32::try_from(params.len()).expect("too many params"),
254                if params.is_empty() {
255                    ptr::null_mut()
256                } else {
257                    params.as_mut_ptr()
258                },
259            )
260        };
261
262        let return_value = ZvalValue::from_raw(&mut retval);
263
264        unsafe { ffi::folk_zval_dtor(&mut retval) };
265        for p in &mut params {
266            unsafe { ffi::folk_zval_dtor(p) };
267        }
268
269        match result {
270            0 => Ok(return_value),
271            -1 => bail!("PHP call_user_function fatal error (bailout) in: {func_name}"),
272            -2 => bail!("PHP call_user_function failed: {func_name}"),
273            code => bail!("PHP call_user_function error {code}: {func_name}"),
274        }
275    }
276
277    /// Evaluate PHP code with SIGSEGV protection.
278    ///
279    /// Like `eval()` but also catches segfaults in C extensions.
280    /// Returns error on SIGSEGV instead of crashing the process.
281    pub fn eval_protected(&mut self, code: &str) -> Result<EvalResult> {
282        let c_code = CString::new(code).context("PHP code contains null byte")?;
283        let mut retval = ffi::zval::new_undef();
284
285        let result = unsafe { ffi::folk_eval_string_protected(c_code.as_ptr(), &mut retval) };
286
287        let output = self.take_output();
288
289        let return_value = if result == 0 {
290            ZvalValue::from_raw(&mut retval)
291        } else {
292            ZvalValue::Null
293        };
294
295        unsafe { ffi::folk_zval_dtor(&mut retval) };
296
297        match result {
298            0 => Ok(EvalResult {
299                output,
300                return_value,
301            }),
302            -1 => bail!("PHP eval fatal error (bailout) in: {code}"),
303            -3 => bail!("PHP eval SIGSEGV caught in: {code}"),
304            code => bail!("PHP eval error {code} in: {code}"),
305        }
306    }
307
308    /// Call a PHP function with SIGSEGV protection.
309    ///
310    /// Like `call()` but also catches segfaults in C extensions.
311    pub fn call_protected(&mut self, func_name: &str, args: &[&str]) -> Result<ZvalValue> {
312        let c_func = CString::new(func_name).context("function name contains null byte")?;
313
314        let c_args: Vec<CString> = args.iter().map(|a| CString::new(*a).unwrap()).collect();
315
316        let mut params: Vec<ffi::zval> = c_args
317            .iter()
318            .map(|s| {
319                let mut z = ffi::zval::new_undef();
320                unsafe {
321                    ffi::folk_zval_set_string(&mut z, s.as_ptr(), s.as_bytes().len());
322                }
323                z
324            })
325            .collect();
326
327        let mut retval = ffi::zval::new_undef();
328
329        let result = unsafe {
330            ffi::folk_call_function_protected(
331                c_func.as_ptr(),
332                &mut retval,
333                u32::try_from(params.len()).expect("too many params"),
334                if params.is_empty() {
335                    ptr::null_mut()
336                } else {
337                    params.as_mut_ptr()
338                },
339            )
340        };
341
342        let return_value = ZvalValue::from_raw(&mut retval);
343
344        unsafe { ffi::folk_zval_dtor(&mut retval) };
345        for p in &mut params {
346            unsafe { ffi::folk_zval_dtor(p) };
347        }
348
349        match result {
350            0 => Ok(return_value),
351            -1 => bail!("PHP call fatal error (bailout) in: {func_name}"),
352            -2 => bail!("PHP call failed: {func_name}"),
353            -3 => bail!("PHP call SIGSEGV caught in: {func_name}"),
354            code => bail!("PHP call error {code}: {func_name}"),
355        }
356    }
357
358    /// Call a PHP function with raw binary data (Phase A — no base64).
359    ///
360    /// Passes method name and binary params directly via FFI pointers.
361    /// PHP function signature: `function($method, $params): string`
362    /// where `$params` is raw binary (e.g. msgpack).
363    pub fn call_binary(&mut self, func_name: &str, method: &str, params: &[u8]) -> Result<Vec<u8>> {
364        let c_func = CString::new(func_name).context("func_name contains null byte")?;
365
366        let mut response_buf: *mut std::ffi::c_char = ptr::null_mut();
367        let mut response_len: usize = 0;
368
369        let result = unsafe {
370            ffi::folk_call_with_binary(
371                c_func.as_ptr(),
372                method.as_ptr().cast(),
373                method.len(),
374                params.as_ptr().cast(),
375                params.len(),
376                &mut response_buf,
377                &mut response_len,
378            )
379        };
380
381        let response = if !response_buf.is_null() && response_len > 0 {
382            let bytes =
383                unsafe { std::slice::from_raw_parts(response_buf.cast::<u8>(), response_len) };
384            let owned = bytes.to_vec();
385            unsafe { ffi::folk_free_buffer(response_buf) };
386            owned
387        } else {
388            if !response_buf.is_null() {
389                unsafe { ffi::folk_free_buffer(response_buf) };
390            }
391            Vec::new()
392        };
393
394        match result {
395            0 => Ok(response),
396            -1 => {
397                let output = self.take_output();
398                if output.is_empty() {
399                    bail!("PHP call fatal error (bailout) in: {func_name}")
400                } else {
401                    bail!("PHP call fatal error (bailout): {output}")
402                }
403            },
404            -2 => bail!("PHP call failed: {func_name}"),
405            -3 => bail!("PHP call SIGSEGV caught in: {func_name}"),
406            code => bail!("PHP call error {code}: {func_name}"),
407        }
408    }
409
410    /// Take captured output, returning it as a string and clearing the buffer.
411    pub fn take_output(&self) -> String {
412        let mut len: usize = 0;
413        let ptr = unsafe { ffi::folk_get_output(&mut len) };
414        let output = if ptr.is_null() || len == 0 {
415            String::new()
416        } else {
417            let bytes = unsafe { std::slice::from_raw_parts(ptr.cast::<u8>(), len) };
418            String::from_utf8_lossy(bytes).into_owned()
419        };
420        unsafe { ffi::folk_clear_output() };
421        output
422    }
423
424    /// Get the captured response data (custom SAPI only).
425    pub fn take_response(&self) -> ResponseData {
426        let status = unsafe { ffi::folk_response_status_code() };
427        let status = u16::try_from(status).unwrap_or(500);
428        let header_count = unsafe { ffi::folk_response_header_count() };
429
430        let mut headers = Vec::with_capacity(header_count);
431        for i in 0..header_count {
432            let mut len: usize = 0;
433            let ptr = unsafe { ffi::folk_response_header_get(i, &mut len) };
434            if !ptr.is_null() && len > 0 {
435                let bytes = unsafe { std::slice::from_raw_parts(ptr.cast::<u8>(), len) };
436                headers.push(String::from_utf8_lossy(bytes).into_owned());
437            }
438        }
439
440        let body = self.take_output();
441
442        ResponseData {
443            status_code: status,
444            headers,
445            body,
446        }
447    }
448}
449
450impl Drop for PhpInstance {
451    fn drop(&mut self) {
452        if self.in_request {
453            self.request_shutdown();
454        }
455
456        unsafe {
457            ffi::folk_free_output();
458
459            if self.custom_sapi {
460                ffi::folk_response_free();
461            }
462        }
463
464        // Free TSRM context for worker threads
465        if !self.tsrm_ctx.is_null() {
466            unsafe { ffi::folk_thread_shutdown(self.tsrm_ctx) };
467            self.tsrm_ctx = ptr::null_mut();
468        }
469
470        // Only shutdown the module if we own it (boot, not attach)
471        if self.owns_module {
472            debug!("shutting down PHP SAPI");
473            unsafe {
474                if self.custom_sapi {
475                    ffi::folk_sapi_shutdown();
476                } else {
477                    ffi::php_embed_shutdown();
478                }
479            }
480        }
481    }
482}
483
484/// HTTP request context for the custom Folk SAPI.
485///
486/// Build this in Rust, pass it to `PhpInstance::set_request_context()`,
487/// then call `request_startup()`. PHP will see the request data in
488/// `$_SERVER`, `$_GET`, `$_POST`, `$_COOKIE`.
489pub struct RequestContext {
490    // Owned strings (kept alive for the duration of the request)
491    method: CString,
492    uri: CString,
493    query_string: Option<CString>,
494    content_type: Option<CString>,
495    content_length: usize,
496    path_translated: Option<CString>,
497    post_data: Vec<u8>,
498    cookie: Option<CString>,
499    server_name: Option<CString>,
500    server_port: i32,
501    protocol: Option<CString>,
502
503    // Headers
504    header_names_c: Vec<CString>,
505    header_values_c: Vec<CString>,
506    header_name_ptrs: Vec<*const std::ffi::c_char>,
507    header_value_ptrs: Vec<*const std::ffi::c_char>,
508
509    // The FFI struct we pass to C
510    ffi: ffi::folk_request_context,
511}
512
513impl RequestContext {
514    pub fn new(method: &str, uri: &str) -> Self {
515        Self {
516            method: CString::new(method).expect("method contains null"),
517            uri: CString::new(uri).expect("uri contains null"),
518            query_string: None,
519            content_type: None,
520            content_length: 0,
521            path_translated: None,
522            post_data: Vec::new(),
523            cookie: None,
524            server_name: None,
525            server_port: 0,
526            protocol: None,
527            header_names_c: Vec::new(),
528            header_values_c: Vec::new(),
529            header_name_ptrs: Vec::new(),
530            header_value_ptrs: Vec::new(),
531            ffi: unsafe { std::mem::zeroed() },
532        }
533    }
534
535    #[must_use]
536    pub fn query_string(mut self, qs: &str) -> Self {
537        self.query_string = Some(CString::new(qs).expect("query_string contains null"));
538        self
539    }
540
541    #[must_use]
542    pub fn content_type(mut self, ct: &str) -> Self {
543        self.content_type = Some(CString::new(ct).expect("content_type contains null"));
544        self
545    }
546
547    #[must_use]
548    pub fn body(mut self, data: &[u8]) -> Self {
549        self.post_data = data.to_vec();
550        self.content_length = data.len();
551        self
552    }
553
554    #[must_use]
555    pub fn cookie(mut self, cookie: &str) -> Self {
556        self.cookie = Some(CString::new(cookie).expect("cookie contains null"));
557        self
558    }
559
560    #[must_use]
561    pub fn path_translated(mut self, path: &str) -> Self {
562        self.path_translated = Some(CString::new(path).expect("path_translated contains null"));
563        self
564    }
565
566    #[must_use]
567    pub fn server(mut self, name: &str, port: i32) -> Self {
568        self.server_name = Some(CString::new(name).expect("server_name contains null"));
569        self.server_port = port;
570        self
571    }
572
573    #[must_use]
574    pub fn protocol(mut self, proto: &str) -> Self {
575        self.protocol = Some(CString::new(proto).expect("protocol contains null"));
576        self
577    }
578
579    #[must_use]
580    pub fn header(mut self, name: &str, value: &str) -> Self {
581        self.header_names_c
582            .push(CString::new(name).expect("header name contains null"));
583        self.header_values_c
584            .push(CString::new(value).expect("header value contains null"));
585        self
586    }
587
588    /// Build the FFI struct from our owned data.
589    /// Must be called before passing to C — after all builder methods.
590    fn build_ffi(&mut self) {
591        // Build pointer arrays for headers
592        self.header_name_ptrs = self.header_names_c.iter().map(|s| s.as_ptr()).collect();
593        self.header_value_ptrs = self.header_values_c.iter().map(|s| s.as_ptr()).collect();
594
595        self.ffi = ffi::folk_request_context {
596            request_method: self.method.as_ptr(),
597            request_uri: self.uri.as_ptr(),
598            query_string: self
599                .query_string
600                .as_ref()
601                .map_or(ptr::null(), |s| s.as_ptr()),
602            content_type: self
603                .content_type
604                .as_ref()
605                .map_or(ptr::null(), |s| s.as_ptr()),
606            content_length: self.content_length,
607            path_translated: self
608                .path_translated
609                .as_ref()
610                .map_or(ptr::null(), |s| s.as_ptr()),
611            post_data: if self.post_data.is_empty() {
612                ptr::null()
613            } else {
614                self.post_data.as_ptr().cast()
615            },
616            post_data_len: self.post_data.len(),
617            post_data_read: 0,
618            cookie_data: self.cookie.as_ref().map_or(ptr::null(), |s| s.as_ptr()),
619            header_names: if self.header_name_ptrs.is_empty() {
620                ptr::null()
621            } else {
622                self.header_name_ptrs.as_ptr()
623            },
624            header_values: if self.header_value_ptrs.is_empty() {
625                ptr::null()
626            } else {
627                self.header_value_ptrs.as_ptr()
628            },
629            header_count: self.header_names_c.len(),
630            server_name: self
631                .server_name
632                .as_ref()
633                .map_or(ptr::null(), |s| s.as_ptr()),
634            server_port: self.server_port,
635            protocol: self.protocol.as_ref().map_or(ptr::null(), |s| s.as_ptr()),
636        };
637    }
638}
639
640/// Captured response data from PHP.
641#[derive(Debug, Clone)]
642pub struct ResponseData {
643    /// HTTP status code (200, 404, etc.)
644    pub status_code: u16,
645    /// Response headers as "Name: Value" strings.
646    pub headers: Vec<String>,
647    /// Response body (output from echo/print).
648    pub body: String,
649}
650
651/// Result of evaluating PHP code.
652#[derive(Debug)]
653pub struct EvalResult {
654    /// Captured output from echo/print.
655    pub output: String,
656    /// The return value of the expression (if any).
657    pub return_value: ZvalValue,
658}
659
660/// A Rust-native representation of a PHP zval value.
661#[derive(Debug, Clone, PartialEq)]
662pub enum ZvalValue {
663    Null,
664    Bool(bool),
665    Long(i64),
666    Double(f64),
667    String(String),
668    /// Type we don't handle yet (array, object, etc.)
669    Other(i32),
670}
671
672impl ZvalValue {
673    fn from_raw(z: &mut ffi::zval) -> Self {
674        let ztype = unsafe { ffi::folk_zval_type(z) };
675        match ztype {
676            ffi::IS_UNDEF | ffi::IS_NULL => Self::Null,
677            ffi::IS_FALSE => Self::Bool(false),
678            ffi::IS_TRUE => Self::Bool(true),
679            ffi::IS_LONG => {
680                let v = unsafe { ffi::folk_zval_get_long(z) };
681                Self::Long(v)
682            },
683            ffi::IS_STRING => {
684                let mut len: usize = 0;
685                let ptr = unsafe { ffi::folk_zval_get_string(z, &mut len) };
686                if ptr.is_null() {
687                    Self::Null
688                } else {
689                    let bytes = unsafe { std::slice::from_raw_parts(ptr.cast::<u8>(), len) };
690                    Self::String(String::from_utf8_lossy(bytes).into_owned())
691                }
692            },
693            other => Self::Other(other),
694        }
695    }
696
697    pub fn as_str(&self) -> Option<&str> {
698        match self {
699            Self::String(s) => Some(s),
700            _ => None,
701        }
702    }
703
704    pub fn as_long(&self) -> Option<i64> {
705        match self {
706            Self::Long(v) => Some(*v),
707            _ => None,
708        }
709    }
710}