Skip to main content

ripht_php_sapi/sapi/
native.rs

1//! Low-level native PHP function integration.
2//!
3//! Hosts can register native functions before PHP startup, while argument and
4//! return helpers keep the call-frame assumptions centralized in ripht.
5
6use std::marker::PhantomData;
7use std::os::raw::{c_char, c_void};
8use std::ptr::NonNull;
9
10use super::ffi;
11use super::ffi::zend_function_entry;
12use super::ffi::zend_string;
13
14pub type Handler = unsafe extern "C" fn(
15    execute_data: *mut c_void,
16    return_value: *mut ReturnValue,
17);
18
19pub type ReturnValue = ffi::zval;
20
21#[derive(Clone, Copy, Debug)]
22#[repr(transparent)]
23pub struct Function {
24    entry: zend_function_entry,
25}
26
27impl Function {
28    /// Creates a native PHP function entry from raw Zend metadata.
29    ///
30    /// # Safety
31    ///
32    /// `fname` must point to a process-lifetime nul-terminated string.
33    /// `arg_info` must point to process-lifetime Zend-compatible arginfo.
34    /// `handler` must obey PHP's `zif_handler` calling convention.
35    pub const unsafe fn new_unchecked(
36        fname: *const c_char,
37        handler: Handler,
38        arg_info: *const c_void,
39        num_args: u32,
40    ) -> Self {
41        Self {
42            entry: zend_function_entry {
43                fname,
44                handler: Some(handler),
45                arg_info,
46                num_args,
47                flags: 0,
48                frameless_function_infos: std::ptr::null(),
49                doc_comment: std::ptr::null(),
50            },
51        }
52    }
53
54    pub(crate) const fn entry(self) -> zend_function_entry {
55        self.entry
56    }
57}
58
59pub struct Call<'frame> {
60    execute_data: *mut c_void,
61    num_args: u32,
62    _marker: PhantomData<&'frame ReturnValue>,
63}
64
65impl<'frame> Call<'frame> {
66    /// Opens a borrowed view over PHP's current call frame.
67    ///
68    /// # Safety
69    ///
70    /// `execute_data` must be the valid call-frame pointer PHP passed to the
71    /// active native function. The returned value must not outlive that call.
72    pub unsafe fn from_execute_data(execute_data: *mut c_void) -> Self {
73        // SAFETY: guaranteed by the caller.
74        let num_args = unsafe { ffi::call_num_args(execute_data) };
75
76        Self {
77            execute_data,
78            num_args,
79            _marker: PhantomData,
80        }
81    }
82
83    pub fn num_args(&self) -> u32 {
84        self.num_args
85    }
86
87    pub fn arg(&self, n: usize) -> Option<NonNull<ReturnValue>> {
88        if n == 0 || n as u32 > self.num_args {
89            return None;
90        }
91
92        // SAFETY: the argument index was checked against PHP's call-frame count.
93        NonNull::new(unsafe { ffi::call_arg_unchecked(self.execute_data, n) })
94    }
95
96    pub fn arg_string(&'frame self, n: usize) -> Option<&'frame [u8]> {
97        let value = self.arg(n)?;
98
99        // SAFETY: the zval pointer belongs to this call frame, and the returned
100        // borrow is tied to the frame lifetime carried by `Call`.
101        unsafe { value.as_ref().as_str() }
102    }
103}
104
105/// # Safety
106///
107/// `return_value` must be the valid pointer PHP passed to the active native function.
108pub unsafe fn set_null(return_value: *mut ReturnValue) {
109    if !return_value.is_null() {
110        // SAFETY: guaranteed by the caller.
111        unsafe { (*return_value).set_null() };
112    }
113}
114
115/// # Safety
116///
117/// `return_value` must be the valid pointer PHP passed to the active native function.
118pub unsafe fn set_string(return_value: *mut ReturnValue, value: &[u8]) {
119    if !return_value.is_null() {
120        // SAFETY: guaranteed by the caller; PHP owns the allocated zend string.
121        unsafe { (*return_value).set_string(zend_string(value)) };
122    }
123}
124
125/// # Safety
126///
127/// Must be called while the PHP engine allocator is initialized.
128pub unsafe fn zend_string(value: &[u8]) -> *mut zend_string {
129    // SAFETY: guaranteed by the caller.
130    unsafe { ffi::zend_string_init_rust(value) }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    unsafe extern "C" fn fake_handler(
138        _execute_data: *mut c_void,
139        _return_value: *mut ReturnValue,
140    ) {
141    }
142
143    #[test]
144    fn function_wraps_static_zend_metadata() {
145        let name = b"ripht_test_native\0";
146
147        // SAFETY: the function name is static, this test does not invoke the handler,
148        // and a null arginfo pointer is enough to validate metadata wrapping.
149        let function = unsafe {
150            Function::new_unchecked(
151                name.as_ptr() as *const c_char,
152                fake_handler,
153                std::ptr::null(),
154                2,
155            )
156        };
157
158        let entry = function.entry();
159
160        assert_eq!(entry.fname, name.as_ptr() as *const c_char);
161        assert_eq!(entry.num_args, 2);
162        assert!(entry.handler.is_some());
163        assert!(entry.arg_info.is_null());
164    }
165
166    #[test]
167    fn call_bounds_arguments_against_php_frame_count() {
168        let mut frame = (0..7)
169            .map(|_| ffi::zval {
170                value: ffi::zend_value { lval: 0 },
171                type_info: 0,
172                _u2: 0,
173            })
174            .collect::<Vec<_>>();
175
176        frame[2]._u2 = 2;
177
178        // SAFETY: the zvals are locally owned and used only for this synthetic frame test.
179        unsafe {
180            frame[5].set_long(10);
181            frame[6].set_long(20);
182        }
183
184        // SAFETY: `frame` is laid out to satisfy the limited call-frame offsets
185        // used by `Call` for num_args and argument lookup.
186        let call = unsafe {
187            Call::from_execute_data(
188                frame
189                    .as_mut_ptr()
190                    .cast::<c_void>(),
191            )
192        };
193
194        assert_eq!(call.num_args(), 2);
195        assert!(call.arg(0).is_none());
196        assert_eq!(call.arg(1).unwrap().as_ptr(), &mut frame[5] as *mut _);
197        assert_eq!(call.arg(2).unwrap().as_ptr(), &mut frame[6] as *mut _);
198        assert!(call.arg(3).is_none());
199    }
200}