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 => bail!("PHP call fatal error (bailout) in: {func_name}"),
397            -2 => bail!("PHP call failed: {func_name}"),
398            -3 => bail!("PHP call SIGSEGV caught in: {func_name}"),
399            code => bail!("PHP call error {code}: {func_name}"),
400        }
401    }
402
403    /// Take captured output, returning it as a string and clearing the buffer.
404    pub fn take_output(&self) -> String {
405        let mut len: usize = 0;
406        let ptr = unsafe { ffi::folk_get_output(&mut len) };
407        let output = if ptr.is_null() || len == 0 {
408            String::new()
409        } else {
410            let bytes = unsafe { std::slice::from_raw_parts(ptr.cast::<u8>(), len) };
411            String::from_utf8_lossy(bytes).into_owned()
412        };
413        unsafe { ffi::folk_clear_output() };
414        output
415    }
416
417    /// Get the captured response data (custom SAPI only).
418    pub fn take_response(&self) -> ResponseData {
419        let status = unsafe { ffi::folk_response_status_code() };
420        let status = u16::try_from(status).unwrap_or(500);
421        let header_count = unsafe { ffi::folk_response_header_count() };
422
423        let mut headers = Vec::with_capacity(header_count);
424        for i in 0..header_count {
425            let mut len: usize = 0;
426            let ptr = unsafe { ffi::folk_response_header_get(i, &mut len) };
427            if !ptr.is_null() && len > 0 {
428                let bytes = unsafe { std::slice::from_raw_parts(ptr.cast::<u8>(), len) };
429                headers.push(String::from_utf8_lossy(bytes).into_owned());
430            }
431        }
432
433        let body = self.take_output();
434
435        ResponseData {
436            status_code: status,
437            headers,
438            body,
439        }
440    }
441}
442
443impl Drop for PhpInstance {
444    fn drop(&mut self) {
445        if self.in_request {
446            self.request_shutdown();
447        }
448
449        unsafe {
450            ffi::folk_free_output();
451
452            if self.custom_sapi {
453                ffi::folk_response_free();
454            }
455        }
456
457        // Free TSRM context for worker threads
458        if !self.tsrm_ctx.is_null() {
459            unsafe { ffi::folk_thread_shutdown(self.tsrm_ctx) };
460            self.tsrm_ctx = ptr::null_mut();
461        }
462
463        // Only shutdown the module if we own it (boot, not attach)
464        if self.owns_module {
465            debug!("shutting down PHP SAPI");
466            unsafe {
467                if self.custom_sapi {
468                    ffi::folk_sapi_shutdown();
469                } else {
470                    ffi::php_embed_shutdown();
471                }
472            }
473        }
474    }
475}
476
477/// HTTP request context for the custom Folk SAPI.
478///
479/// Build this in Rust, pass it to `PhpInstance::set_request_context()`,
480/// then call `request_startup()`. PHP will see the request data in
481/// `$_SERVER`, `$_GET`, `$_POST`, `$_COOKIE`.
482pub struct RequestContext {
483    // Owned strings (kept alive for the duration of the request)
484    method: CString,
485    uri: CString,
486    query_string: Option<CString>,
487    content_type: Option<CString>,
488    content_length: usize,
489    path_translated: Option<CString>,
490    post_data: Vec<u8>,
491    cookie: Option<CString>,
492    server_name: Option<CString>,
493    server_port: i32,
494    protocol: Option<CString>,
495
496    // Headers
497    header_names_c: Vec<CString>,
498    header_values_c: Vec<CString>,
499    header_name_ptrs: Vec<*const std::ffi::c_char>,
500    header_value_ptrs: Vec<*const std::ffi::c_char>,
501
502    // The FFI struct we pass to C
503    ffi: ffi::folk_request_context,
504}
505
506impl RequestContext {
507    pub fn new(method: &str, uri: &str) -> Self {
508        Self {
509            method: CString::new(method).expect("method contains null"),
510            uri: CString::new(uri).expect("uri contains null"),
511            query_string: None,
512            content_type: None,
513            content_length: 0,
514            path_translated: None,
515            post_data: Vec::new(),
516            cookie: None,
517            server_name: None,
518            server_port: 0,
519            protocol: None,
520            header_names_c: Vec::new(),
521            header_values_c: Vec::new(),
522            header_name_ptrs: Vec::new(),
523            header_value_ptrs: Vec::new(),
524            ffi: unsafe { std::mem::zeroed() },
525        }
526    }
527
528    #[must_use]
529    pub fn query_string(mut self, qs: &str) -> Self {
530        self.query_string = Some(CString::new(qs).expect("query_string contains null"));
531        self
532    }
533
534    #[must_use]
535    pub fn content_type(mut self, ct: &str) -> Self {
536        self.content_type = Some(CString::new(ct).expect("content_type contains null"));
537        self
538    }
539
540    #[must_use]
541    pub fn body(mut self, data: &[u8]) -> Self {
542        self.post_data = data.to_vec();
543        self.content_length = data.len();
544        self
545    }
546
547    #[must_use]
548    pub fn cookie(mut self, cookie: &str) -> Self {
549        self.cookie = Some(CString::new(cookie).expect("cookie contains null"));
550        self
551    }
552
553    #[must_use]
554    pub fn path_translated(mut self, path: &str) -> Self {
555        self.path_translated = Some(CString::new(path).expect("path_translated contains null"));
556        self
557    }
558
559    #[must_use]
560    pub fn server(mut self, name: &str, port: i32) -> Self {
561        self.server_name = Some(CString::new(name).expect("server_name contains null"));
562        self.server_port = port;
563        self
564    }
565
566    #[must_use]
567    pub fn protocol(mut self, proto: &str) -> Self {
568        self.protocol = Some(CString::new(proto).expect("protocol contains null"));
569        self
570    }
571
572    #[must_use]
573    pub fn header(mut self, name: &str, value: &str) -> Self {
574        self.header_names_c
575            .push(CString::new(name).expect("header name contains null"));
576        self.header_values_c
577            .push(CString::new(value).expect("header value contains null"));
578        self
579    }
580
581    /// Build the FFI struct from our owned data.
582    /// Must be called before passing to C — after all builder methods.
583    fn build_ffi(&mut self) {
584        // Build pointer arrays for headers
585        self.header_name_ptrs = self.header_names_c.iter().map(|s| s.as_ptr()).collect();
586        self.header_value_ptrs = self.header_values_c.iter().map(|s| s.as_ptr()).collect();
587
588        self.ffi = ffi::folk_request_context {
589            request_method: self.method.as_ptr(),
590            request_uri: self.uri.as_ptr(),
591            query_string: self
592                .query_string
593                .as_ref()
594                .map_or(ptr::null(), |s| s.as_ptr()),
595            content_type: self
596                .content_type
597                .as_ref()
598                .map_or(ptr::null(), |s| s.as_ptr()),
599            content_length: self.content_length,
600            path_translated: self
601                .path_translated
602                .as_ref()
603                .map_or(ptr::null(), |s| s.as_ptr()),
604            post_data: if self.post_data.is_empty() {
605                ptr::null()
606            } else {
607                self.post_data.as_ptr().cast()
608            },
609            post_data_len: self.post_data.len(),
610            post_data_read: 0,
611            cookie_data: self.cookie.as_ref().map_or(ptr::null(), |s| s.as_ptr()),
612            header_names: if self.header_name_ptrs.is_empty() {
613                ptr::null()
614            } else {
615                self.header_name_ptrs.as_ptr()
616            },
617            header_values: if self.header_value_ptrs.is_empty() {
618                ptr::null()
619            } else {
620                self.header_value_ptrs.as_ptr()
621            },
622            header_count: self.header_names_c.len(),
623            server_name: self
624                .server_name
625                .as_ref()
626                .map_or(ptr::null(), |s| s.as_ptr()),
627            server_port: self.server_port,
628            protocol: self.protocol.as_ref().map_or(ptr::null(), |s| s.as_ptr()),
629        };
630    }
631}
632
633/// Captured response data from PHP.
634#[derive(Debug, Clone)]
635pub struct ResponseData {
636    /// HTTP status code (200, 404, etc.)
637    pub status_code: u16,
638    /// Response headers as "Name: Value" strings.
639    pub headers: Vec<String>,
640    /// Response body (output from echo/print).
641    pub body: String,
642}
643
644/// Result of evaluating PHP code.
645#[derive(Debug)]
646pub struct EvalResult {
647    /// Captured output from echo/print.
648    pub output: String,
649    /// The return value of the expression (if any).
650    pub return_value: ZvalValue,
651}
652
653/// A Rust-native representation of a PHP zval value.
654#[derive(Debug, Clone, PartialEq)]
655pub enum ZvalValue {
656    Null,
657    Bool(bool),
658    Long(i64),
659    Double(f64),
660    String(String),
661    /// Type we don't handle yet (array, object, etc.)
662    Other(i32),
663}
664
665impl ZvalValue {
666    fn from_raw(z: &mut ffi::zval) -> Self {
667        let ztype = unsafe { ffi::folk_zval_type(z) };
668        match ztype {
669            ffi::IS_UNDEF | ffi::IS_NULL => Self::Null,
670            ffi::IS_FALSE => Self::Bool(false),
671            ffi::IS_TRUE => Self::Bool(true),
672            ffi::IS_LONG => {
673                let v = unsafe { ffi::folk_zval_get_long(z) };
674                Self::Long(v)
675            },
676            ffi::IS_STRING => {
677                let mut len: usize = 0;
678                let ptr = unsafe { ffi::folk_zval_get_string(z, &mut len) };
679                if ptr.is_null() {
680                    Self::Null
681                } else {
682                    let bytes = unsafe { std::slice::from_raw_parts(ptr.cast::<u8>(), len) };
683                    Self::String(String::from_utf8_lossy(bytes).into_owned())
684                }
685            },
686            other => Self::Other(other),
687        }
688    }
689
690    pub fn as_str(&self) -> Option<&str> {
691        match self {
692            Self::String(s) => Some(s),
693            _ => None,
694        }
695    }
696
697    pub fn as_long(&self) -> Option<i64> {
698        match self {
699            Self::Long(v) => Some(*v),
700            _ => None,
701        }
702    }
703}