diff --git a/build.rs b/build.rs index 8c845e8..79451b6 100644 --- a/build.rs +++ b/build.rs @@ -34,23 +34,27 @@ fn main() { panic!("Error running `php-config`: {}", stderr); } - let includes = String::from_utf8(includes_cmd.stdout) - .expect("unable to parse `php-config` stdout"); + let includes = + String::from_utf8(includes_cmd.stdout).expect("unable to parse `php-config` stdout"); - let ignore_math_h_macros = IgnoreMacros(vec![ - // math.h:914 - enum which uses #define for values - "FP_NAN".into(), - "FP_INFINITE".into(), - "FP_ZERO".into(), - "FP_SUBNORMAL".into(), - "FP_NORMAL".into(), - // math.h:237 - enum which uses #define for values - "FP_INT_UPWARD".into(), - "FP_INT_DOWNWARD".into(), - "FP_INT_TOWARDZERO".into(), - "FP_INT_TONEARESTFROMZERO".into(), - "FP_INT_TONEAREST".into(), - ].into_iter().collect()); + let ignore_math_h_macros = IgnoreMacros( + vec![ + // math.h:914 - enum which uses #define for values + "FP_NAN".into(), + "FP_INFINITE".into(), + "FP_ZERO".into(), + "FP_SUBNORMAL".into(), + "FP_NORMAL".into(), + // math.h:237 - enum which uses #define for values + "FP_INT_UPWARD".into(), + "FP_INT_DOWNWARD".into(), + "FP_INT_TOWARDZERO".into(), + "FP_INT_TONEARESTFROMZERO".into(), + "FP_INT_TONEAREST".into(), + ] + .into_iter() + .collect(), + ); let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); bindgen::Builder::default() diff --git a/example/skel/.gitignore b/example/skel/.gitignore new file mode 100644 index 0000000..72b1d08 --- /dev/null +++ b/example/skel/.gitignore @@ -0,0 +1,4 @@ +/target +Cargo.lock +/.vscode +expand.rs \ No newline at end of file diff --git a/example/skel/Cargo.toml b/example/skel/Cargo.toml new file mode 100644 index 0000000..7195fe4 --- /dev/null +++ b/example/skel/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "example" +version = "0.1.0" +authors = ["David Cole "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +php-rs = { path = "../../" } + +[lib] +name = "skel" +crate-type = ["dylib"] diff --git a/example/skel/src/lib.rs b/example/skel/src/lib.rs new file mode 100644 index 0000000..fcd64f2 --- /dev/null +++ b/example/skel/src/lib.rs @@ -0,0 +1,19 @@ +use php_rs::{ + info_table_end, info_table_row, info_table_start, + php::module::{ModuleBuilder, ModuleEntry}, +}; + +#[no_mangle] +pub extern "C" fn php_module_info(_module: *mut ModuleEntry) { + info_table_start!(); + info_table_row!("skeleton extension", "enabled"); + info_table_end!(); +} + +#[no_mangle] +pub extern "C" fn get_module() -> *mut php_rs::php::module::ModuleEntry { + ModuleBuilder::new("ext-skel", "0.1.0") + .info_function(php_module_info) + .build() + .into_raw() +} diff --git a/src/bindings.rs b/src/bindings.rs new file mode 100644 index 0000000..44ae78f --- /dev/null +++ b/src/bindings.rs @@ -0,0 +1 @@ +include!(concat!(env!("OUT_DIR"), "/bindings.rs")); diff --git a/src/functions.rs b/src/functions.rs index f7553b9..e6a1a86 100644 --- a/src/functions.rs +++ b/src/functions.rs @@ -1,9 +1,46 @@ -use std::ffi::CString; +use crate::bindings::{ZEND_BUILD_TS, ZEND_MODULE_API_NO}; +use std::ffi::{CStr, CString}; -pub fn c_str(s: T) -> *const i8 +/// Takes a Rust string object, converts it into a C string +/// and then releases the string to the C world. +/// +/// Note that strings produced by this function *will not* be freed by +/// Rust, and this can cause memory leaks. +/// +/// # Examples +/// ``` +/// use std::ffi::CString; +/// use php_rs::functions::c_str; +/// +/// let mut ptr = c_str("Hello"); +/// +/// unsafe { +/// assert_eq!(b'H', *ptr as u8); +/// assert_eq!(b'e', *ptr.offset(1) as u8); +/// assert_eq!(b'l', *ptr.offset(2) as u8); +/// assert_eq!(b'l', *ptr.offset(3) as u8); +/// assert_eq!(b'o', *ptr.offset(4) as u8); +/// assert_eq!(b'\0', *ptr.offset(5) as u8); +/// +/// // reclaim string and release memory +/// let _ = CString::from_raw(ptr); +/// } +/// ``` +pub fn c_str(s: S) -> *const i8 where - T: Into> + S: Into, { - let x = CString::new(s).unwrap(); - x.as_ptr() -} \ No newline at end of file + CString::into_raw(CString::new(s.into()).unwrap()) +} + +/// Fetches the `build_id` for a Zend extension module. +pub(crate) fn build_id() -> String { + // UNSAFE: reading a constant which has been translated from C, only reading and not + // modifying. + let zend_build_ts = unsafe { CStr::from_ptr(ZEND_BUILD_TS.as_ptr() as *const i8) }; + format!( + "API{}{}", + ZEND_MODULE_API_NO, + zend_build_ts.to_str().unwrap() + ) +} diff --git a/src/lib.rs b/src/lib.rs index 701066b..b2f8006 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,23 +4,14 @@ #[macro_use] pub mod macros; +pub(crate) mod bindings; pub mod functions; -pub mod module; +pub mod php; -include!(concat!(env!("OUT_DIR"), "/bindings.rs")); - -#[cfg(test)] -mod tests { - use crate::info_table_start; - - #[test] - fn test() { - info_table_start!(); - - info_table_header!("Hello", "World", "From", "Macro"); - info_table_header!(); - info_table_row!("Hello", "World!"); - - info_table_end!(); - } +// Bindings used by macros. Used so that the rest of the bindings can be hidden with `pub(crate)`. +extern "C" { + pub fn php_info_print_table_header(num_cols: ::std::os::raw::c_int, ...); + pub fn php_info_print_table_row(num_cols: ::std::os::raw::c_int, ...); + pub fn php_info_print_table_start(); + pub fn php_info_print_table_end(); } diff --git a/src/macros.rs b/src/macros.rs index 09c5798..9c9fdb3 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -1,32 +1,38 @@ +/// Starts the PHP extension information table displayed when running `phpinfo();` +/// Must be run *before* rows are inserted into the table. #[macro_export] macro_rules! info_table_start { () => { - unsafe { crate::php_info_print_table_start() }; + unsafe { $crate::php_info_print_table_start() }; }; } +/// Ends the PHP extension information table. Must be run *after* all rows have been inserted into the table. #[macro_export] macro_rules! info_table_end { () => { - unsafe { crate::php_info_print_table_end() } + unsafe { $crate::php_info_print_table_end() } }; } +/// Sets the header for the PHP extension information table. Takes as many string arguments as required. #[macro_export] macro_rules! info_table_header { ($($element:expr),*) => {$crate::_info_table_row!(php_info_print_table_header, $($element),*)}; } +/// Adds a row to the PHP extension information table. Takes as many string arguments as required. #[macro_export] macro_rules! info_table_row { ($($element:expr),*) => {$crate::_info_table_row!(php_info_print_table_row, $($element),*)}; } +/// INTERNAL: Calls a variadic C function with the number of parameters, then following with the parameters. #[macro_export] macro_rules! _info_table_row { ($fn: ident, $($element: expr),*) => { unsafe { - crate::$fn($crate::_info_table_row!(@COUNT; $($element),*) as i32, $(crate::functions::c_str($element)),*); + $crate::$fn($crate::_info_table_row!(@COUNT; $($element),*) as i32, $($crate::functions::c_str($element)),*); } }; diff --git a/src/module/info.rs b/src/module/info.rs deleted file mode 100644 index 006b9b8..0000000 --- a/src/module/info.rs +++ /dev/null @@ -1,50 +0,0 @@ -use crate::{ - functions::c_str, php_info_print_table_end, php_info_print_table_header, - php_info_print_table_row, php_info_print_table_start, -}; - -pub struct InfoTable { - rows: Vec<(&'static str, &'static str)>, - header: Option<(&'static str, &'static str)>, -} - -impl InfoTable { - pub fn new(cols: u32) -> Self { - InfoTable { - rows: vec![], - header: None, - } - } - - pub fn header(&mut self, key: S, value: S) -> &mut Self - where - S: Into<&'static str>, - { - self.header = Some((key.into(), value.into())); - self - } - - pub fn row(&mut self, key: S, value: S) -> &mut Self - where - S: Into<&'static str>, - { - self.rows.push((key.into(), value.into())); - self - } - - pub fn build(&self) { - unsafe { php_info_print_table_start() }; - - if let Some(header) = self.header { - unsafe { - php_info_print_table_header(2, c_str(header.0), c_str(header.1)); - } - } - - for row in self.rows.iter() { - unsafe { php_info_print_table_row(2, c_str(row.0), c_str(row.1)) }; - } - - unsafe { php_info_print_table_end() }; - } -} diff --git a/src/module/mod.rs b/src/module/mod.rs deleted file mode 100644 index 55cc091..0000000 --- a/src/module/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod info; \ No newline at end of file diff --git a/src/php/function.rs b/src/php/function.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/php/function.rs @@ -0,0 +1 @@ + diff --git a/src/php/mod.rs b/src/php/mod.rs new file mode 100644 index 0000000..5eb15a0 --- /dev/null +++ b/src/php/mod.rs @@ -0,0 +1,2 @@ +pub mod function; +pub mod module; diff --git a/src/php/module.rs b/src/php/module.rs new file mode 100644 index 0000000..8ae2930 --- /dev/null +++ b/src/php/module.rs @@ -0,0 +1,169 @@ +use std::{ + ffi::c_void, + mem, + os::raw::{c_char, c_int}, + ptr, +}; + +use crate::{ + bindings::{ + zend_function_entry, zend_module_entry, zend_result, USING_ZTS, ZEND_DEBUG, + ZEND_MODULE_API_NO, + }, + functions::{build_id, c_str}, +}; + +pub type ModuleEntry = zend_module_entry; +pub type FunctionEntry = zend_function_entry; +pub type StartupShutdownFunc = extern "C" fn(type_: c_int, module_number: c_int) -> zend_result; +pub type InfoFunc = extern "C" fn(zend_module: *mut ModuleEntry); + +/// Builds a Zend extension. Must be called from within an external function called `get_module`, +/// returning a mutable pointer to a `ModuleEntry`. +/// +/// ``` +/// #[no_mangle] +/// pub extern "C" fn php_module_info(_module: *mut ModuleEntry) { +/// print_table_start(); +/// print_table_row("column 1", "column 2"); +/// print_table_end(); +/// } +/// +/// #[no_mangle] +/// pub extern "C" fn get_module() -> *mut ModuleEntry { +/// ModuleBuilder::new("ext-name", "ext-version") +/// .info_function(php_module_info) +/// .build() +/// .into_raw() +/// } +/// ``` +pub struct ModuleBuilder { + module: ModuleEntry, + functions: Vec, +} + +impl ModuleBuilder { + /// Creates a new module builder with a given name and version. + /// + /// # Arguments + /// + /// * `name` - The name of the extension. + /// * `version` - The current version of the extension. TBD: Deprecate in favour of the `Cargo.toml` version? + pub fn new(name: N, version: V) -> Self + where + N: Into, + V: Into, + { + Self { + module: ModuleEntry { + size: mem::size_of::() as u16, + zend_api: ZEND_MODULE_API_NO, + zend_debug: ZEND_DEBUG as u8, + zts: USING_ZTS as u8, + ini_entry: ptr::null(), + deps: ptr::null(), + name: c_str(name), + functions: ptr::null(), + module_startup_func: None, + module_shutdown_func: None, + request_startup_func: None, + request_shutdown_func: None, + info_func: None, + version: c_str(version), + globals_size: 0, + globals_ptr: ptr::null::() as *mut c_void, + globals_ctor: None, + globals_dtor: None, + post_deactivate_func: None, + module_started: 0, + type_: 0, + handle: ptr::null::() as *mut c_void, + module_number: 0, + build_id: c_str(build_id()), + }, + functions: vec![], + } + } + + /// Sets the startup function for the extension. + /// + /// # Arguments + /// + /// * `func` - The function to be called on startup. + pub fn startup_function(mut self, func: StartupShutdownFunc) -> Self { + self.module.module_startup_func = Some(func); + self + } + + /// Sets the shutdown function for the extension. + /// + /// # Arguments + /// + /// * `func` - The function to be called on shutdown. + pub fn shutdown_function(mut self, func: StartupShutdownFunc) -> Self { + self.module.module_shutdown_func = Some(func); + self + } + + /// Sets the request startup function for the extension. + /// + /// # Arguments + /// + /// * `func` - The function to be called when startup is requested. + pub fn request_startup_function(mut self, func: StartupShutdownFunc) -> Self { + self.module.module_startup_func = Some(func); + self + } + + /// Sets the request shutdown function for the extension. + /// + /// # Arguments + /// + /// * `func` - The function to be called when shutdown is requested. + pub fn request_shutdown_function(mut self, func: StartupShutdownFunc) -> Self { + self.module.module_shutdown_func = Some(func); + self + } + + /// Sets the extension information function for the extension. + /// + /// # Arguments + /// + /// * `func` - The function to be called to retrieve the information about the extension. + pub fn info_function(mut self, func: InfoFunc) -> Self { + self.module.info_func = Some(func); + self + } + + /// Adds a function to the extension. + /// + /// # Arguments + /// + /// * `func` - The function to be added to the extension. + pub fn function(mut self, func: FunctionEntry) -> Self { + self.functions.push(func); + self + } + + /// Builds the extension and returns a `ModuleEntry`. + pub fn build(mut self) -> ModuleEntry { + // TODO: move to seperate function + self.functions.push(FunctionEntry { + fname: ptr::null() as *const c_char, + handler: None, + arg_info: ptr::null(), + num_args: 0, + flags: 0, + }); + self.module.functions = + Box::into_raw(self.functions.into_boxed_slice()) as *const FunctionEntry; + self.module + } +} + +impl ModuleEntry { + /// Converts the module entry into a raw pointer, releasing it to the C world. + pub fn into_raw(self) -> *mut Self { + Box::into_raw(Box::new(self)) + } +}