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
This commit is contained in:
Joel Wurtz 2023-10-20 14:08:10 +02:00 committed by GitHub
parent 15bed3b0b5
commit 2d0e587c7e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 354 additions and 4 deletions

15
.github/actions/embed/Dockerfile vendored Normal file
View File

@ -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" ]

5
.github/actions/embed/action.yml vendored Normal file
View File

@ -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'

View File

@ -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

View File

@ -35,6 +35,7 @@ zip = "0.6"
[features]
closure = []
embed = []
[workspace]
members = [

View File

@ -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
}

View File

@ -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<String> {
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 =

11
src/embed/embed.c Normal file
View File

@ -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()
}

4
src/embed/embed.h Normal file
View File

@ -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);

16
src/embed/ffi.rs Normal file
View File

@ -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,
);
}

221
src/embed/mod.rs Normal file
View File

@ -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<ZBox<ZendObject>>),
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<P: AsRef<Path>>(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<F: Fn()>(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<F: Fn()>(ctx: *const c_void) {
(*(ctx as *const F))();
}
unsafe {
ext_php_rs_embed_callback(
0,
null_mut(),
wrapper::<F>,
&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<Zval, EmbedError> {
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());
});
}
}

View File

@ -0,0 +1,3 @@
<?php
throw new \RuntimeException('This is a test exception');

View File

@ -0,0 +1,7 @@
<?php
class Test {
public function __construct() {}
}
$foo = new Test();

View File

@ -25,6 +25,8 @@ pub mod class;
pub mod closure;
pub mod constant;
pub mod describe;
#[cfg(feature = "embed")]
pub mod embed;
#[doc(hidden)]
pub mod internal;
pub mod props;

View File

@ -453,3 +453,23 @@ impl<'a> 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");
});
}
}

View File

@ -55,4 +55,11 @@ impl<'a> PHPProvider<'a> for Provider {
fn get_defines(&self) -> Result<Vec<(&'static str, &'static str)>> {
Ok(vec![])
}
fn print_extra_link_args(&self) -> Result<()> {
#[cfg(feature = "embed")]
println!("cargo:rustc-link-lib=php");
Ok(())
}
}