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