mirror of
https://github.com/danog/ext-php-rs.git
synced 2024-11-30 04:39:04 +01:00
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:
parent
15bed3b0b5
commit
2d0e587c7e
15
.github/actions/embed/Dockerfile
vendored
Normal file
15
.github/actions/embed/Dockerfile
vendored
Normal 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
5
.github/actions/embed/action.yml
vendored
Normal 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'
|
12
.github/workflows/build.yml
vendored
12
.github/workflows/build.yml
vendored
@ -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
|
||||
|
@ -35,6 +35,7 @@ zip = "0.6"
|
||||
|
||||
[features]
|
||||
closure = []
|
||||
embed = []
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
|
@ -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
build.rs
28
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<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
11
src/embed/embed.c
Normal 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
4
src/embed/embed.h
Normal 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
16
src/embed/ffi.rs
Normal 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
221
src/embed/mod.rs
Normal 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());
|
||||
});
|
||||
}
|
||||
}
|
3
src/embed/test-script-exception.php
Normal file
3
src/embed/test-script-exception.php
Normal file
@ -0,0 +1,3 @@
|
||||
<?php
|
||||
|
||||
throw new \RuntimeException('This is a test exception');
|
7
src/embed/test-script.php
Normal file
7
src/embed/test-script.php
Normal file
@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
class Test {
|
||||
public function __construct() {}
|
||||
}
|
||||
|
||||
$foo = new Test();
|
@ -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;
|
||||
|
@ -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");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user