Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(embed): add embed features, add test example which run php inside it #270

Merged
merged 6 commits into from
Oct 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 = []
joelwurtz marked this conversation as resolved.
Show resolved Hide resolved

[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,
ptondereau marked this conversation as resolved.
Show resolved Hide resolved
primary_script: false,
in_list: false,
buf: null_mut(),
len: 0,
};

unsafe {
zend_stream_init_filename(&mut file_handle, path.as_ptr());
ptondereau marked this conversation as resolved.
Show resolved Hide resolved
}

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