From 2d0e587c7ef08c3e59dc9d127896ca5150023e85 Mon Sep 17 00:00:00 2001 From: Joel Wurtz Date: Fri, 20 Oct 2023 14:08:10 +0200 Subject: [PATCH] feat(embed): add embed features, add test example which run php inside it (#270) * feat(embed): add embed features, add test example which run php inside it * feat(embed): use a guard to prevent running in parallel * chore(ci): update actions to not build and test with embed, add a specific build for embed testing * feat(embed): correcly start / shutdown embed api * chore(ci): use stable for rust in embed test * feat(embed): add documentation, manage potential errors --- .github/actions/embed/Dockerfile | 15 ++ .github/actions/embed/action.yml | 5 + .github/workflows/build.yml | 12 +- Cargo.toml | 1 + allowed_bindings.rs | 6 +- build.rs | 28 +++- src/embed/embed.c | 11 ++ src/embed/embed.h | 4 + src/embed/ffi.rs | 16 ++ src/embed/mod.rs | 221 ++++++++++++++++++++++++++++ src/embed/test-script-exception.php | 3 + src/embed/test-script.php | 7 + src/lib.rs | 2 + src/types/string.rs | 20 +++ unix_build.rs | 7 + 15 files changed, 354 insertions(+), 4 deletions(-) create mode 100644 .github/actions/embed/Dockerfile create mode 100644 .github/actions/embed/action.yml create mode 100644 src/embed/embed.c create mode 100644 src/embed/embed.h create mode 100644 src/embed/ffi.rs create mode 100644 src/embed/mod.rs create mode 100644 src/embed/test-script-exception.php create mode 100644 src/embed/test-script.php diff --git a/.github/actions/embed/Dockerfile b/.github/actions/embed/Dockerfile new file mode 100644 index 0000000..a92ec5a --- /dev/null +++ b/.github/actions/embed/Dockerfile @@ -0,0 +1,15 @@ +FROM php:8.2-bullseye + +WORKDIR /tmp + +RUN apt update -y && apt upgrade -y +RUN apt install lsb-release wget gnupg software-properties-common -y +RUN bash -c "$(wget -O - https://apt.llvm.org/llvm.sh)" + +ENV RUSTUP_HOME=/rust +ENV CARGO_HOME=/cargo +ENV PATH=/cargo/bin:/rust/bin:$PATH + +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path + +ENTRYPOINT [ "/cargo/bin/cargo", "test", "--lib", "--release", "--all-features" ] diff --git a/.github/actions/embed/action.yml b/.github/actions/embed/action.yml new file mode 100644 index 0000000..c99ebf5 --- /dev/null +++ b/.github/actions/embed/action.yml @@ -0,0 +1,5 @@ +name: 'PHP Embed and Rust' +description: 'Builds the crate after installing the latest PHP with php embed and stable Rust.' +runs: + using: 'docker' + image: 'Dockerfile' diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6176b86..fe68b07 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -83,10 +83,10 @@ jobs: - name: Build env: EXT_PHP_RS_TEST: "" - run: cargo build --release --all-features --all + run: cargo build --release --features closure,anyhow --all # Test & lint - name: Test inline examples - run: cargo test --release --all --all-features + run: cargo test --release --all --features closure,anyhow - name: Run rustfmt if: matrix.rust == 'stable' && matrix.os == 'ubuntu-latest' && matrix.php == '8.2' run: cargo fmt --all -- --check @@ -110,3 +110,11 @@ jobs: uses: actions/checkout@v3 - name: Build uses: ./.github/actions/zts + test-embed: + name: Test with embed + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: Test + uses: ./.github/actions/embed diff --git a/Cargo.toml b/Cargo.toml index eaccb4d..385a5cc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ zip = "0.6" [features] closure = [] +embed = [] [workspace] members = [ diff --git a/allowed_bindings.rs b/allowed_bindings.rs index a74d965..49b1585 100644 --- a/allowed_bindings.rs +++ b/allowed_bindings.rs @@ -256,5 +256,9 @@ bind! { tsrm_get_ls_cache, executor_globals_offset, zend_atomic_bool_store, - zend_interrupt_function + zend_interrupt_function, + zend_eval_string, + zend_file_handle, + zend_stream_init_filename, + php_execute_script } diff --git a/build.rs b/build.rs index 40250a9..4ee8041 100644 --- a/build.rs +++ b/build.rs @@ -151,9 +151,31 @@ fn build_wrapper(defines: &[(&str, &str)], includes: &[PathBuf]) -> Result<()> { Ok(()) } +#[cfg(feature = "embed")] +/// Builds the embed library. +fn build_embed(defines: &[(&str, &str)], includes: &[PathBuf]) -> Result<()> { + let mut build = cc::Build::new(); + for (var, val) in defines { + build.define(var, *val); + } + build + .file("src/embed/embed.c") + .includes(includes) + .try_compile("embed") + .context("Failed to compile ext-php-rs C embed interface")?; + Ok(()) +} + /// Generates bindings to the Zend API. fn generate_bindings(defines: &[(&str, &str)], includes: &[PathBuf]) -> Result { - let mut bindgen = bindgen::Builder::default() + let mut bindgen = bindgen::Builder::default(); + + #[cfg(feature = "embed")] + { + bindgen = bindgen.header("src/embed/embed.h"); + } + + bindgen = bindgen .header("src/wrapper.h") .clang_args( includes @@ -257,6 +279,10 @@ fn main() -> Result<()> { check_php_version(&info)?; build_wrapper(&defines, &includes)?; + + #[cfg(feature = "embed")] + build_embed(&defines, &includes)?; + let bindings = generate_bindings(&defines, &includes)?; let out_file = diff --git a/src/embed/embed.c b/src/embed/embed.c new file mode 100644 index 0000000..6aaa30c --- /dev/null +++ b/src/embed/embed.c @@ -0,0 +1,11 @@ +#include "embed.h" + +// We actually use the PHP embed API to run PHP code in test +// At some point we might want to use our own SAPI to do that +void ext_php_rs_embed_callback(int argc, char** argv, void (*callback)(void *), void *ctx) { + PHP_EMBED_START_BLOCK(argc, argv) + + callback(ctx); + + PHP_EMBED_END_BLOCK() +} \ No newline at end of file diff --git a/src/embed/embed.h b/src/embed/embed.h new file mode 100644 index 0000000..910dcd9 --- /dev/null +++ b/src/embed/embed.h @@ -0,0 +1,4 @@ +#include "zend.h" +#include "sapi/embed/php_embed.h" + +void ext_php_rs_embed_callback(int argc, char** argv, void (*callback)(void *), void *ctx); diff --git a/src/embed/ffi.rs b/src/embed/ffi.rs new file mode 100644 index 0000000..74124a9 --- /dev/null +++ b/src/embed/ffi.rs @@ -0,0 +1,16 @@ +//! Raw FFI bindings to the Zend API. + +#![allow(clippy::all)] +#![allow(warnings)] + +use std::ffi::{c_char, c_int, c_void}; + +#[link(name = "wrapper")] +extern "C" { + pub fn ext_php_rs_embed_callback( + argc: c_int, + argv: *mut *mut c_char, + func: unsafe extern "C" fn(*const c_void), + ctx: *const c_void, + ); +} diff --git a/src/embed/mod.rs b/src/embed/mod.rs new file mode 100644 index 0000000..93e973b --- /dev/null +++ b/src/embed/mod.rs @@ -0,0 +1,221 @@ +//! Provides implementations for running php code from rust. +//! It only works on linux for now and you should have `php-embed` installed +//! +//! This crate was only test with PHP 8.2 please report any issue with other version +//! You should only use this crate for test purpose, it's not production ready + +mod ffi; + +use crate::boxed::ZBox; +use crate::embed::ffi::ext_php_rs_embed_callback; +use crate::ffi::{ + _zend_file_handle__bindgen_ty_1, php_execute_script, zend_eval_string, zend_file_handle, + zend_stream_init_filename, ZEND_RESULT_CODE_SUCCESS, +}; +use crate::types::{ZendObject, Zval}; +use crate::zend::ExecutorGlobals; +use parking_lot::{const_rwlock, RwLock}; +use std::ffi::{c_char, c_void, CString, NulError}; +use std::path::Path; +use std::ptr::null_mut; + +pub struct Embed; + +#[derive(Debug)] +pub enum EmbedError { + InitError, + ExecuteError(Option>), + ExecuteScriptError, + InvalidEvalString(NulError), + InvalidPath, +} + +static RUN_FN_LOCK: RwLock<()> = const_rwlock(()); + +impl Embed { + /// Run a php script from a file + /// + /// This function will only work correctly when used inside the `Embed::run` function + /// otherwise behavior is unexpected + /// + /// # Returns + /// + /// * `Ok(())` - The script was executed successfully + /// * `Err(EmbedError)` - An error occured during the execution of the script + /// + /// # Example + /// + /// ``` + /// use ext_php_rs::embed::Embed; + /// + /// Embed::run(|| { + /// let result = Embed::run_script("src/embed/test-script.php"); + /// + /// assert!(result.is_ok()); + /// }); + /// ``` + pub fn run_script>(path: P) -> Result<(), EmbedError> { + let path = match path.as_ref().to_str() { + Some(path) => match CString::new(path) { + Ok(path) => path, + Err(err) => return Err(EmbedError::InvalidEvalString(err)), + }, + None => return Err(EmbedError::InvalidPath), + }; + + let mut file_handle = zend_file_handle { + handle: _zend_file_handle__bindgen_ty_1 { fp: null_mut() }, + filename: null_mut(), + opened_path: null_mut(), + type_: 0, + primary_script: false, + in_list: false, + buf: null_mut(), + len: 0, + }; + + unsafe { + zend_stream_init_filename(&mut file_handle, path.as_ptr()); + } + + if unsafe { php_execute_script(&mut file_handle) } { + Ok(()) + } else { + Err(EmbedError::ExecuteScriptError) + } + } + + /// Start and run embed sapi engine + /// + /// This function will allow to run php code from rust, the same PHP context is keep between calls + /// inside the function passed to this method. + /// Which means subsequent calls to `Embed::eval` or `Embed::run_script` will be able to access + /// variables defined in previous calls + /// + /// # Example + /// + /// ``` + /// use ext_php_rs::embed::Embed; + /// + /// Embed::run(|| { + /// let _ = Embed::eval("$foo = 'foo';"); + /// let foo = Embed::eval("$foo;"); + /// assert!(foo.is_ok()); + /// assert_eq!(foo.unwrap().string().unwrap(), "foo"); + /// }); + /// ``` + pub fn run(func: F) { + // @TODO handle php thread safe + // + // This is to prevent multiple threads from running php at the same time + // At some point we should detect if php is compiled with thread safety and avoid doing that in this case + let _guard = RUN_FN_LOCK.write(); + + unsafe extern "C" fn wrapper(ctx: *const c_void) { + (*(ctx as *const F))(); + } + + unsafe { + ext_php_rs_embed_callback( + 0, + null_mut(), + wrapper::, + &func as *const F as *const c_void, + ); + } + } + + /// Evaluate a php code + /// + /// This function will only work correctly when used inside the `Embed::run` function + /// + /// # Returns + /// + /// * `Ok(Zval)` - The result of the evaluation + /// * `Err(EmbedError)` - An error occured during the evaluation + /// + /// # Example + /// + /// ``` + /// use ext_php_rs::embed::Embed; + /// + /// Embed::run(|| { + /// let foo = Embed::eval("$foo = 'foo';"); + /// assert!(foo.is_ok()); + /// }); + /// ``` + pub fn eval(code: &str) -> Result { + let cstr = match CString::new(code) { + Ok(cstr) => cstr, + Err(err) => return Err(EmbedError::InvalidEvalString(err)), + }; + + let mut result = Zval::new(); + + // this eval is very limited as it only allow simple code, it's the same eval used by php -r + let exec_result = unsafe { + zend_eval_string( + cstr.as_ptr() as *const c_char, + &mut result, + b"run\0".as_ptr() as *const _, + ) + }; + + let exception = ExecutorGlobals::take_exception(); + + if exec_result != ZEND_RESULT_CODE_SUCCESS { + Err(EmbedError::ExecuteError(exception)) + } else { + Ok(result) + } + } +} + +#[cfg(test)] +mod tests { + use super::Embed; + + #[test] + fn test_run() { + Embed::run(|| { + let result = Embed::eval("$foo = 'foo';"); + + assert!(result.is_ok()); + }); + } + + #[test] + fn test_run_error() { + Embed::run(|| { + let result = Embed::eval("stupid code;"); + + assert!(!result.is_ok()); + }); + } + + #[test] + fn test_run_script() { + Embed::run(|| { + let result = Embed::run_script("src/embed/test-script.php"); + + assert!(result.is_ok()); + + let zval = Embed::eval("$foo;").unwrap(); + + assert!(zval.is_object()); + + let obj = zval.object().unwrap(); + + assert_eq!(obj.get_class_name().unwrap(), "Test"); + }); + } + + #[test] + fn test_run_script_error() { + Embed::run(|| { + let result = Embed::run_script("src/embed/test-script-exception.php"); + + assert!(!result.is_ok()); + }); + } +} diff --git a/src/embed/test-script-exception.php b/src/embed/test-script-exception.php new file mode 100644 index 0000000..6674846 --- /dev/null +++ b/src/embed/test-script-exception.php @@ -0,0 +1,3 @@ + FromZval<'a> for &'a str { zval.str() } } + +#[cfg(test)] +#[cfg(feature = "embed")] +mod tests { + use crate::embed::Embed; + + #[test] + fn test_string() { + Embed::run(|| { + let result = Embed::eval("'foo';"); + + assert!(result.is_ok()); + + let zval = result.as_ref().unwrap(); + + assert!(zval.is_string()); + assert_eq!(zval.string().unwrap(), "foo"); + }); + } +} diff --git a/unix_build.rs b/unix_build.rs index 9bdd446..8be6fd8 100644 --- a/unix_build.rs +++ b/unix_build.rs @@ -55,4 +55,11 @@ impl<'a> PHPProvider<'a> for Provider { fn get_defines(&self) -> Result> { Ok(vec![]) } + + fn print_extra_link_args(&self) -> Result<()> { + #[cfg(feature = "embed")] + println!("cargo:rustc-link-lib=php"); + + Ok(()) + } }