Skip to content

Commit

Permalink
feat(embed): add embed features, add test example which run php insid…
Browse files Browse the repository at this point in the history
…e 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
  • Loading branch information
joelwurtz authored Oct 20, 2023
1 parent 15bed3b commit 2d0e587
Show file tree
Hide file tree
Showing 15 changed files with 354 additions and 4 deletions.
15 changes: 15 additions & 0 deletions .github/actions/embed/Dockerfile
Original file line number Diff line number Diff line change
@@ -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 changes: 5 additions & 0 deletions .github/actions/embed/action.yml
Original file line number Diff line number Diff line change
@@ -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'
12 changes: 10 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ zip = "0.6"

[features]
closure = []
embed = []

[workspace]
members = [
Expand Down
6 changes: 5 additions & 1 deletion allowed_bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
28 changes: 27 additions & 1 deletion build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 =
Expand Down
11 changes: 11 additions & 0 deletions src/embed/embed.c
Original file line number Diff line number Diff line change
@@ -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 changes: 4 additions & 0 deletions src/embed/embed.h
Original file line number Diff line number Diff line change
@@ -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 changes: 16 additions & 0 deletions src/embed/ffi.rs
Original file line number Diff line number Diff line change
@@ -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 changes: 221 additions & 0 deletions src/embed/mod.rs
Original file line number Diff line number Diff line change
@@ -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());
});
}
}
3 changes: 3 additions & 0 deletions src/embed/test-script-exception.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<?php

throw new \RuntimeException('This is a test exception');
7 changes: 7 additions & 0 deletions src/embed/test-script.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

class Test {
public function __construct() {}
}

$foo = new Test();
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading

0 comments on commit 2d0e587

Please sign in to comment.